forked from tsai/budibase
committed by
Martin McKeaveney
26 changed files with 243 additions and 164 deletions
@ -0,0 +1,32 @@ |
|||||
|
export const getNewRecordValidationRule = ( |
||||
|
invalidField, |
||||
|
messageWhenInvalid, |
||||
|
expressionWhenValid |
||||
|
) => ({ |
||||
|
invalidField, |
||||
|
messageWhenInvalid, |
||||
|
expressionWhenValid, |
||||
|
}) |
||||
|
|
||||
|
export const commonRecordValidationRules = { |
||||
|
fieldNotEmpty: fieldName => |
||||
|
getNewRecordValidationRule( |
||||
|
fieldName, |
||||
|
`${fieldName} is empty`, |
||||
|
`record['${fieldName}'] && record['${fieldName}'].length > 0` |
||||
|
), |
||||
|
|
||||
|
fieldBetween: (fieldName, min, max) => |
||||
|
getNewRecordValidationRule( |
||||
|
fieldName, |
||||
|
`${fieldName} must be between ${min.toString()} and ${max.toString()}`, |
||||
|
`record['${fieldName}'] >= ${min} && record['${fieldName}'] <= ${max} ` |
||||
|
), |
||||
|
|
||||
|
fieldGreaterThan: (fieldName, min, max) => |
||||
|
getNewRecordValidationRule( |
||||
|
fieldName, |
||||
|
`${fieldName} must be greater than ${min.toString()} and ${max.toString()}`, |
||||
|
`record['${fieldName}'] >= ${min} ` |
||||
|
), |
||||
|
} |
||||
@ -1,95 +1,55 @@ |
|||||
import { |
|
||||
setupApphierarchy, |
|
||||
stubEventHandler, |
|
||||
basicAppHierarchyCreator_WithFields, |
|
||||
basicAppHierarchyCreator_WithFields_AndIndexes, |
|
||||
hierarchyFactory, |
|
||||
withFields, |
|
||||
} from "./specHelpers" |
|
||||
import { find } from "lodash" |
|
||||
import { addHours } from "date-fns" |
import { addHours } from "date-fns" |
||||
import { events } from "../src/common" |
import { events } from "../src/common" |
||||
|
import { testSchema } from "./testSchema.mjs" |
||||
|
import { validateRecord } from "../src/records/validateRecord.mjs" |
||||
|
import { getNewRecord } from "../src/records/getNewRecord.mjs" |
||||
|
|
||||
describe("recordApi > validate", () => { |
describe("recordApi > validate", () => { |
||||
it("should return errors when any fields do not parse", async () => { |
it("should return errors when any fields do not parse", () => { |
||||
const { recordApi } = await setupApphierarchy( |
const schema = testSchema() |
||||
basicAppHierarchyCreator_WithFields |
const record = getNewRecord(schema, "Contact") |
||||
) |
|
||||
const record = recordApi.getNew("/customers", "customer") |
|
||||
|
|
||||
record.surname = "Ledog" |
record.Name = "Ledog" |
||||
record.isalive = "hello" |
record["Is Active"] = "hello" |
||||
record.age = "nine" |
record.Created = "not a date" |
||||
record.createddate = "blah" |
|
||||
|
|
||||
const validationResult = await recordApi.validate(record) |
const validationResult = validateRecord(schema, record) |
||||
|
|
||||
expect(validationResult.isValid).toBe(false) |
expect(validationResult.isValid).toBe(false) |
||||
expect(validationResult.errors.length).toBe(3) |
expect(validationResult.errors.length).toBe(2) |
||||
}) |
}) |
||||
|
|
||||
it("should return errors when mandatory field is empty", async () => { |
it("should return errors when mandatory field is empty", () => { |
||||
const withValidationRule = (hierarchy, templateApi) => { |
const schema = testSchema() |
||||
templateApi.addRecordValidationRule(hierarchy.customerRecord)( |
const record = getNewRecord(schema, "Contact") |
||||
templateApi.commonRecordValidationRules.fieldNotEmpty("surname") |
record.Name = "" |
||||
) |
|
||||
} |
|
||||
|
|
||||
const hierarchyCreator = hierarchyFactory(withFields, withValidationRule) |
|
||||
const { recordApi } = await setupApphierarchy(hierarchyCreator) |
|
||||
|
|
||||
const record = recordApi.getNew("/customers", "customer") |
|
||||
|
|
||||
record.surname = "" |
|
||||
|
|
||||
const validationResult = await recordApi.validate(record) |
const validationResult = validateRecord(schema, record) |
||||
|
|
||||
expect(validationResult.isValid).toBe(false) |
expect(validationResult.isValid).toBe(false) |
||||
expect(validationResult.errors.length).toBe(1) |
expect(validationResult.errors.length).toBe(1) |
||||
}) |
}) |
||||
|
|
||||
it("should return error when string field is beyond maxLength", async () => { |
it("should return error when string field is beyond maxLength", () => { |
||||
const withFieldWithMaxLength = hierarchy => { |
const schema = testSchema() |
||||
const surname = find( |
schema.findField("Contact", "Name").typeOptions.maxLength = 5 |
||||
hierarchy.customerRecord.fields, |
const record = getNewRecord(schema, "Contact") |
||||
f => f.name === "surname" |
record.name = "more than 5 characters" |
||||
) |
|
||||
surname.typeOptions.maxLength = 5 |
|
||||
} |
|
||||
|
|
||||
const hierarchyCreator = hierarchyFactory( |
|
||||
withFields, |
|
||||
withFieldWithMaxLength |
|
||||
) |
|
||||
const { recordApi } = await setupApphierarchy(hierarchyCreator) |
|
||||
|
|
||||
const record = recordApi.getNew("/customers", "customer") |
const validationResult = validateRecord(schema, record) |
||||
record.surname = "more than 5 chars" |
|
||||
|
|
||||
const validationResult = await recordApi.validate(record) |
|
||||
expect(validationResult.isValid).toBe(false) |
expect(validationResult.isValid).toBe(false) |
||||
expect(validationResult.errors.length).toBe(1) |
expect(validationResult.errors.length).toBe(1) |
||||
}) |
}) |
||||
|
|
||||
it("should return error when number field is > maxValue", async () => { |
it("should return error when number field is > maxValue", () => { |
||||
const withFieldWithMaxLength = hierarchy => { |
const schema = testSchema() |
||||
const age = find(hierarchy.customerRecord.fields, f => f.name === "age") |
schema.findField("Deal", "Estimated Value").typeOptions.maxValue = 5 |
||||
age.typeOptions.maxValue = 10 |
const record = getNewRecord(schema, "Deal") |
||||
age.typeOptions.minValue = 5 |
record["Estimated Value"] = 10 |
||||
} |
|
||||
|
|
||||
const hierarchyCreator = hierarchyFactory( |
|
||||
withFields, |
|
||||
withFieldWithMaxLength |
|
||||
) |
|
||||
const { recordApi } = await setupApphierarchy(hierarchyCreator) |
|
||||
|
|
||||
const tooOldRecord = recordApi.getNew("/customers", "customer") |
|
||||
tooOldRecord.age = 11 |
|
||||
|
|
||||
const tooOldResult = await recordApi.validate(tooOldRecord) |
const validationResult = recordApi.validate(schema, record) |
||||
expect(tooOldResult.isValid).toBe(false) |
expect(validationResult.isValid).toBe(false) |
||||
expect(tooOldResult.errors.length).toBe(1) |
expect(validationResult.errors.length).toBe(1) |
||||
}) |
}) |
||||
|
|
||||
it("should return error when number field is < minValue", async () => { |
it("should return error when number field is < minValue", async () => { |
||||
@ -1,21 +1,115 @@ |
|||||
const couchdb = require("../../db"); |
const couchdb = require("../../db") |
||||
|
const { cloneDeep, mapValues, keyBy, filter, includes } = require("lodash/fp") |
||||
|
const { |
||||
|
validateRecord, |
||||
|
} = require("../../../common/src/records/validateRecord.mjs") |
||||
|
const { events } = require("../../../common/src/common/events.mjs") |
||||
|
const { $, isNonEmptyString } = require("../../../common/src/common") |
||||
|
import { safeParseField } from "../../../common/src/schema/types" |
||||
|
|
||||
const controller = { |
async function save(ctx) { |
||||
save: async ctx => { |
const db = couchdb.use(ctx.databaseId) |
||||
}, |
const record = cloneDeep(ctx.body) |
||||
fetch: async ctx => { |
|
||||
const db = couchdb.db.use(ctx.params.databaseId) |
|
||||
|
|
||||
ctx.body = await db.view("database", "all_somemodel", { |
if (!ctx.schema.findModel(record._modelId)) { |
||||
include_docs: true, |
ctx.status = 400 |
||||
key: ["app"] |
ctx.message = `do not recognise modelId : ${record._modelId}` |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
const validationResult = await validateRecord(ctx.schema, record) |
||||
|
if (!validationResult.isValid) { |
||||
|
await app.publish(events.recordApi.save.onInvalid, { |
||||
|
record, |
||||
|
validationResult, |
||||
|
}) |
||||
|
ctx.status = 400 |
||||
|
ctx.message = "record failed validation rules" |
||||
|
ctx.body = validationResult |
||||
|
} |
||||
|
|
||||
|
if (!record._rev) { |
||||
|
await db.insert(record) |
||||
|
await app.publish(events.recordApi.save.onRecordCreated, { |
||||
|
record: record, |
||||
}) |
}) |
||||
}, |
} else { |
||||
destroy: async ctx => { |
const oldRecord = await _findRecord(db, ctx.schema, record._id) |
||||
const databaseId = ctx.params.databaseId; |
await db.insert(record) |
||||
const database = couchdb.db.use(databaseId) |
await app.publish(events.recordApi.save.onRecordUpdated, { |
||||
ctx.body = await database.destroy(ctx.params.recordId); |
old: oldRecord, |
||||
}, |
new: record, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
const savedHead = await db.head(record._id) |
||||
|
record._rev = savedHead._rev |
||||
|
return record |
||||
|
} |
||||
|
|
||||
|
async function fetch(ctx) { |
||||
|
const db = couchdb.db.use(ctx.params.databaseId) |
||||
|
|
||||
|
ctx.body = await db.view("database", "all_somemodel", { |
||||
|
include_docs: true, |
||||
|
key: ["app"] |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
async function find(ctx) { |
||||
|
const db = couchdb.db.use(ctx.params.databaseId) |
||||
|
const { body, status } = await _findRecord(db, ctx.schema, ctx.params.id) |
||||
|
ctx.status = status |
||||
|
ctx.body = body |
||||
|
} |
||||
|
|
||||
|
async function _findRecord(db, schema, id) { |
||||
|
let storedData |
||||
|
try { |
||||
|
storedData = await db.get(id) |
||||
|
} catch (err) { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
const model = schema.findModel(storedData._modelId) |
||||
|
|
||||
|
const loadedRecord = $(model.fields, [ |
||||
|
keyBy("name"), |
||||
|
mapValues(f => safeParseField(f, storedData)), |
||||
|
]) |
||||
|
|
||||
|
const links = $(model.fields, [ |
||||
|
filter( |
||||
|
f => f.type === "reference" && isNonEmptyString(loadedRecord[f.name].key) |
||||
|
), |
||||
|
map(f => ({ |
||||
|
promise: _findRecord(db, schema, loadedRecord[f.name]._id), |
||||
|
index: getNode(app.hierarchy, f.typeOptions.indexNodeKey), |
||||
|
field: f, |
||||
|
})), |
||||
|
]) |
||||
|
|
||||
|
if (links.length > 0) { |
||||
|
const refRecords = await Promise.all(map(p => p.promise)(links)) |
||||
|
|
||||
|
for (const ref of links) { |
||||
|
loadedRecord[ref.field.name] = mapRecord( |
||||
|
refRecords[links.indexOf(ref)], |
||||
|
ref.index |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
loadedRecord._rev = storedData._rev |
||||
|
loadedRecord._id = storedData._id |
||||
|
loadedRecord._modelId = storedData._modelId |
||||
|
return loadedRecord |
||||
|
} |
||||
|
|
||||
|
async function destroy(ctx) { |
||||
|
const databaseId = ctx.params.databaseId; |
||||
|
const database = couchdb.db.use(databaseId) |
||||
|
ctx.body = await database.destroy(ctx.params.recordId); |
||||
} |
} |
||||
|
|
||||
module.exports = controller; |
module.exports = {dave, fetch, destroy, find}; |
||||
@ -0,0 +1,9 @@ |
|||||
|
const { testSchema } = require("../../common/test/testSchema") |
||||
|
|
||||
|
describe("record persistence", () => { |
||||
|
it("should save a record", async () => { |
||||
|
|
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
|
||||
@ -1,7 +0,0 @@ |
|||||
const { testSchema } = require("../../common/test/testSchema") |
|
||||
|
|
||||
describe("record persistence", async () => { |
|
||||
it("should ") |
|
||||
}) |
|
||||
|
|
||||
|
|
||||
Loading…
Reference in new issue