mirror of https://github.com/Budibase/budibase.git
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 { 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", () => { |
|||
it("should return errors when any fields do not parse", async () => { |
|||
const { recordApi } = await setupApphierarchy( |
|||
basicAppHierarchyCreator_WithFields |
|||
) |
|||
const record = recordApi.getNew("/customers", "customer") |
|||
it("should return errors when any fields do not parse", () => { |
|||
const schema = testSchema() |
|||
const record = getNewRecord(schema, "Contact") |
|||
|
|||
record.surname = "Ledog" |
|||
record.isalive = "hello" |
|||
record.age = "nine" |
|||
record.createddate = "blah" |
|||
record.Name = "Ledog" |
|||
record["Is Active"] = "hello" |
|||
record.Created = "not a date" |
|||
|
|||
const validationResult = await recordApi.validate(record) |
|||
const validationResult = validateRecord(schema, record) |
|||
|
|||
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 () => { |
|||
const withValidationRule = (hierarchy, templateApi) => { |
|||
templateApi.addRecordValidationRule(hierarchy.customerRecord)( |
|||
templateApi.commonRecordValidationRules.fieldNotEmpty("surname") |
|||
) |
|||
} |
|||
|
|||
const hierarchyCreator = hierarchyFactory(withFields, withValidationRule) |
|||
const { recordApi } = await setupApphierarchy(hierarchyCreator) |
|||
|
|||
const record = recordApi.getNew("/customers", "customer") |
|||
|
|||
record.surname = "" |
|||
it("should return errors when mandatory field is empty", () => { |
|||
const schema = testSchema() |
|||
const record = getNewRecord(schema, "Contact") |
|||
record.Name = "" |
|||
|
|||
const validationResult = await recordApi.validate(record) |
|||
const validationResult = validateRecord(schema, record) |
|||
|
|||
expect(validationResult.isValid).toBe(false) |
|||
expect(validationResult.errors.length).toBe(1) |
|||
}) |
|||
|
|||
it("should return error when string field is beyond maxLength", async () => { |
|||
const withFieldWithMaxLength = hierarchy => { |
|||
const surname = find( |
|||
hierarchy.customerRecord.fields, |
|||
f => f.name === "surname" |
|||
) |
|||
surname.typeOptions.maxLength = 5 |
|||
} |
|||
|
|||
const hierarchyCreator = hierarchyFactory( |
|||
withFields, |
|||
withFieldWithMaxLength |
|||
) |
|||
const { recordApi } = await setupApphierarchy(hierarchyCreator) |
|||
it("should return error when string field is beyond maxLength", () => { |
|||
const schema = testSchema() |
|||
schema.findField("Contact", "Name").typeOptions.maxLength = 5 |
|||
const record = getNewRecord(schema, "Contact") |
|||
record.name = "more than 5 characters" |
|||
|
|||
const record = recordApi.getNew("/customers", "customer") |
|||
record.surname = "more than 5 chars" |
|||
|
|||
const validationResult = await recordApi.validate(record) |
|||
const validationResult = validateRecord(schema, record) |
|||
expect(validationResult.isValid).toBe(false) |
|||
expect(validationResult.errors.length).toBe(1) |
|||
}) |
|||
|
|||
it("should return error when number field is > maxValue", async () => { |
|||
const withFieldWithMaxLength = hierarchy => { |
|||
const age = find(hierarchy.customerRecord.fields, f => f.name === "age") |
|||
age.typeOptions.maxValue = 10 |
|||
age.typeOptions.minValue = 5 |
|||
} |
|||
|
|||
const hierarchyCreator = hierarchyFactory( |
|||
withFields, |
|||
withFieldWithMaxLength |
|||
) |
|||
const { recordApi } = await setupApphierarchy(hierarchyCreator) |
|||
|
|||
const tooOldRecord = recordApi.getNew("/customers", "customer") |
|||
tooOldRecord.age = 11 |
|||
it("should return error when number field is > maxValue", () => { |
|||
const schema = testSchema() |
|||
schema.findField("Deal", "Estimated Value").typeOptions.maxValue = 5 |
|||
const record = getNewRecord(schema, "Deal") |
|||
record["Estimated Value"] = 10 |
|||
|
|||
const tooOldResult = await recordApi.validate(tooOldRecord) |
|||
expect(tooOldResult.isValid).toBe(false) |
|||
expect(tooOldResult.errors.length).toBe(1) |
|||
const validationResult = recordApi.validate(schema, record) |
|||
expect(validationResult.isValid).toBe(false) |
|||
expect(validationResult.errors.length).toBe(1) |
|||
}) |
|||
|
|||
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 = { |
|||
save: async ctx => { |
|||
}, |
|||
fetch: async ctx => { |
|||
const db = couchdb.db.use(ctx.params.databaseId) |
|||
async function save(ctx) { |
|||
const db = couchdb.use(ctx.databaseId) |
|||
const record = cloneDeep(ctx.body) |
|||
|
|||
ctx.body = await db.view("database", "all_somemodel", { |
|||
include_docs: true, |
|||
key: ["app"] |
|||
if (!ctx.schema.findModel(record._modelId)) { |
|||
ctx.status = 400 |
|||
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, |
|||
}) |
|||
}, |
|||
destroy: async ctx => { |
|||
const databaseId = ctx.params.databaseId; |
|||
const database = couchdb.db.use(databaseId) |
|||
ctx.body = await database.destroy(ctx.params.recordId); |
|||
}, |
|||
} else { |
|||
const oldRecord = await _findRecord(db, ctx.schema, record._id) |
|||
await db.insert(record) |
|||
await app.publish(events.recordApi.save.onRecordUpdated, { |
|||
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