record controllers...
This commit is contained in:
parent
ebc1c44343
commit
84c3e287d5
|
@ -22,10 +22,10 @@ const validateAllFieldParse = (record, model) =>
|
||||||
}, []),
|
}, []),
|
||||||
])
|
])
|
||||||
|
|
||||||
const validateAllTypeConstraints = async (record, model) => {
|
const validateAllTypeConstraints = (record, model) => {
|
||||||
const errors = []
|
const errors = []
|
||||||
for (const field of model.fields) {
|
for (const field of model.fields) {
|
||||||
$(await validateTypeConstraints(field, record), [
|
$(validateTypeConstraints(field, record), [
|
||||||
filter(isNonEmptyString),
|
filter(isNonEmptyString),
|
||||||
map(m => ({ message: m, fields: [field.name] })),
|
map(m => ({ message: m, fields: [field.name] })),
|
||||||
each(e => errors.push(e)),
|
each(e => errors.push(e)),
|
||||||
|
@ -55,8 +55,8 @@ const runRecordValidationRules = (record, model) => {
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
export const validateRecord = async (schema, record) => {
|
export const validateRecord = (schema, record) => {
|
||||||
const model = schema.findModel(record.modelId)
|
const model = schema.findModel(record._modelId)
|
||||||
const fieldParseFails = validateAllFieldParse(record, model)
|
const fieldParseFails = validateAllFieldParse(record, model)
|
||||||
|
|
||||||
// non parsing would cause further issues - exit here
|
// non parsing would cause further issues - exit here
|
||||||
|
@ -65,7 +65,7 @@ export const validateRecord = async (schema, record) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const recordValidationRuleFails = runRecordValidationRules(record, model)
|
const recordValidationRuleFails = runRecordValidationRules(record, model)
|
||||||
const typeContraintFails = await validateAllTypeConstraints(record, model)
|
const typeContraintFails = validateAllTypeConstraints(record, model)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isEmpty(fieldParseFails) &&
|
isEmpty(fieldParseFails) &&
|
||||||
|
|
|
@ -47,11 +47,11 @@ const options = {
|
||||||
|
|
||||||
const typeConstraints = [
|
const typeConstraints = [
|
||||||
makerule(
|
makerule(
|
||||||
async (val, opts) => val === null || val.length >= opts.minLength,
|
(val, opts) => val === null || val.length >= opts.minLength,
|
||||||
(val, opts) => `must choose ${opts.minLength} or more options`
|
(val, opts) => `must choose ${opts.minLength} or more options`
|
||||||
),
|
),
|
||||||
makerule(
|
makerule(
|
||||||
async (val, opts) => val === null || val.length <= opts.maxLength,
|
(val, opts) => val === null || val.length <= opts.maxLength,
|
||||||
(val, opts) => `cannot choose more than ${opts.maxLength} options`
|
(val, opts) => `cannot choose more than ${opts.maxLength} options`
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,7 +6,12 @@ import {
|
||||||
parsedSuccess,
|
parsedSuccess,
|
||||||
getDefaultExport,
|
getDefaultExport,
|
||||||
} from "./typeHelpers"
|
} from "./typeHelpers"
|
||||||
import { switchCase, defaultCase, isOneOf, toBoolOrNull } from "../../common/index.mjs"
|
import {
|
||||||
|
switchCase,
|
||||||
|
defaultCase,
|
||||||
|
isOneOf,
|
||||||
|
toBoolOrNull,
|
||||||
|
} from "../../common/index.mjs"
|
||||||
|
|
||||||
const boolFunctions = typeFunctions({
|
const boolFunctions = typeFunctions({
|
||||||
default: constant(null),
|
default: constant(null),
|
||||||
|
@ -31,7 +36,7 @@ const options = {
|
||||||
|
|
||||||
const typeConstraints = [
|
const typeConstraints = [
|
||||||
makerule(
|
makerule(
|
||||||
async (val, opts) => opts.allowNulls === true || val !== null,
|
(val, opts) => opts.allowNulls === true || val !== null,
|
||||||
() => "field cannot be null"
|
() => "field cannot be null"
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -50,7 +50,7 @@ const options = {
|
||||||
|
|
||||||
const typeConstraints = [
|
const typeConstraints = [
|
||||||
makerule(
|
makerule(
|
||||||
async (val, opts) =>
|
(val, opts) =>
|
||||||
val === null || isNullOrEmpty(opts.minValue) || val >= opts.minValue,
|
val === null || isNullOrEmpty(opts.minValue) || val >= opts.minValue,
|
||||||
(val, opts) =>
|
(val, opts) =>
|
||||||
`value (${val.toString()}) must be greater than or equal to ${
|
`value (${val.toString()}) must be greater than or equal to ${
|
||||||
|
@ -58,7 +58,7 @@ const typeConstraints = [
|
||||||
}`
|
}`
|
||||||
),
|
),
|
||||||
makerule(
|
makerule(
|
||||||
async (val, opts) =>
|
(val, opts) =>
|
||||||
val === null || isNullOrEmpty(opts.maxValue) || val <= opts.maxValue,
|
val === null || isNullOrEmpty(opts.maxValue) || val <= opts.maxValue,
|
||||||
(val, opts) =>
|
(val, opts) =>
|
||||||
`value (${val.toString()}) must be less than or equal to ${
|
`value (${val.toString()}) must be less than or equal to ${
|
||||||
|
|
|
@ -5,7 +5,13 @@ import {
|
||||||
parsedSuccess,
|
parsedSuccess,
|
||||||
getDefaultExport,
|
getDefaultExport,
|
||||||
} from "./typeHelpers"
|
} from "./typeHelpers"
|
||||||
import { switchCase, defaultCase, none, $, splitKey } from "../../common/index.mjs"
|
import {
|
||||||
|
switchCase,
|
||||||
|
defaultCase,
|
||||||
|
none,
|
||||||
|
$,
|
||||||
|
splitKey,
|
||||||
|
} from "../../common/index.mjs"
|
||||||
|
|
||||||
const illegalCharacters = "*?\\/:<>|\0\b\f\v"
|
const illegalCharacters = "*?\\/:<>|\0\b\f\v"
|
||||||
|
|
||||||
|
|
|
@ -67,8 +67,8 @@ export const validateFieldParse = (field, record) =>
|
||||||
|
|
||||||
export const getDefaultOptions = type => getType(type).getDefaultOptions()
|
export const getDefaultOptions = type => getType(type).getDefaultOptions()
|
||||||
|
|
||||||
export const validateTypeConstraints = async (field, record) =>
|
export const validateTypeConstraints = (field, record) =>
|
||||||
await getType(field.type).validateTypeConstraints(field, record)
|
getType(field.type).validateTypeConstraints(field, record)
|
||||||
|
|
||||||
export const detectType = value => {
|
export const detectType = value => {
|
||||||
if (isString(value)) return string
|
if (isString(value)) return string
|
||||||
|
|
|
@ -1,65 +1,30 @@
|
||||||
import { isString, isObjectLike, isNull, has } from "lodash/fp"
|
import { isString, isUndefined, isNull } from "lodash/fp"
|
||||||
import {
|
import { typeFunctions, parsedSuccess, getDefaultExport } from "./typeHelpers"
|
||||||
typeFunctions,
|
|
||||||
parsedSuccess,
|
|
||||||
getDefaultExport,
|
|
||||||
parsedFailed,
|
|
||||||
} from "./typeHelpers"
|
|
||||||
import {
|
import {
|
||||||
switchCase,
|
switchCase,
|
||||||
defaultCase,
|
defaultCase,
|
||||||
isNonEmptyString,
|
isNonEmptyString,
|
||||||
isArrayOfString,
|
|
||||||
} from "../../common/index.mjs"
|
} from "../../common/index.mjs"
|
||||||
|
|
||||||
const linkNothing = () => ({ key: "" })
|
const linkNothing = () => ""
|
||||||
|
|
||||||
const linkFunctions = typeFunctions({
|
const linkFunctions = typeFunctions({
|
||||||
default: linkNothing,
|
default: linkNothing,
|
||||||
})
|
})
|
||||||
|
|
||||||
const hasStringValue = (ob, path) => has(path)(ob) && isString(ob[path])
|
|
||||||
|
|
||||||
const isObjectWithKey = v => isObjectLike(v) && hasStringValue(v, "key")
|
|
||||||
|
|
||||||
const tryParseFromString = s => {
|
|
||||||
try {
|
|
||||||
const asObj = JSON.parse(s)
|
|
||||||
if (isObjectWithKey) {
|
|
||||||
return parsedSuccess(asObj)
|
|
||||||
}
|
|
||||||
} catch (_) {
|
|
||||||
// EMPTY
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsedFailed(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
const linkTryParse = v =>
|
const linkTryParse = v =>
|
||||||
switchCase(
|
switchCase(
|
||||||
[isObjectWithKey, parsedSuccess],
|
[isString, s => parsedSuccess(s)],
|
||||||
[isString, tryParseFromString],
|
|
||||||
[isNull, () => parsedSuccess(linkNothing())],
|
[isNull, () => parsedSuccess(linkNothing())],
|
||||||
[defaultCase, parsedFailed]
|
[isUndefined, () => parsedSuccess(linkNothing())],
|
||||||
|
[defaultCase, s => parsedSuccess(s.toString())]
|
||||||
)(v)
|
)(v)
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
indexNodeKey: {
|
modelId: {
|
||||||
defaultValue: null,
|
|
||||||
isValid: isNonEmptyString,
|
|
||||||
requirementDescription: "must be a non-empty string",
|
|
||||||
parse: s => s,
|
|
||||||
},
|
|
||||||
displayValue: {
|
|
||||||
defaultValue: "",
|
defaultValue: "",
|
||||||
isValid: isNonEmptyString,
|
isValid: isNonEmptyString,
|
||||||
requirementDescription: "must be a non-empty string",
|
requirementDescription: "must choose a model",
|
||||||
parse: s => s,
|
|
||||||
},
|
|
||||||
reverseIndexNodeKeys: {
|
|
||||||
defaultValue: null,
|
|
||||||
isValid: v => isArrayOfString(v) && v.length > 0,
|
|
||||||
requirementDescription: "must be a non-empty array of strings",
|
|
||||||
parse: s => s,
|
parse: s => s,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -72,6 +37,6 @@ export default getDefaultExport(
|
||||||
linkFunctions,
|
linkFunctions,
|
||||||
options,
|
options,
|
||||||
typeConstraints,
|
typeConstraints,
|
||||||
{ key: "key", value: "value" },
|
"abcd1234",
|
||||||
JSON.stringify
|
JSON.stringify
|
||||||
)
|
)
|
||||||
|
|
|
@ -58,7 +58,7 @@ const getDecimalPlaces = val => {
|
||||||
|
|
||||||
const typeConstraints = [
|
const typeConstraints = [
|
||||||
makerule(
|
makerule(
|
||||||
async (val, opts) =>
|
(val, opts) =>
|
||||||
val === null || opts.minValue === null || val >= opts.minValue,
|
val === null || opts.minValue === null || val >= opts.minValue,
|
||||||
(val, opts) =>
|
(val, opts) =>
|
||||||
`value (${val.toString()}) must be greater than or equal to ${
|
`value (${val.toString()}) must be greater than or equal to ${
|
||||||
|
@ -66,7 +66,7 @@ const typeConstraints = [
|
||||||
}`
|
}`
|
||||||
),
|
),
|
||||||
makerule(
|
makerule(
|
||||||
async (val, opts) =>
|
(val, opts) =>
|
||||||
val === null || opts.maxValue === null || val <= opts.maxValue,
|
val === null || opts.maxValue === null || val <= opts.maxValue,
|
||||||
(val, opts) =>
|
(val, opts) =>
|
||||||
`value (${val.toString()}) must be less than or equal to ${
|
`value (${val.toString()}) must be less than or equal to ${
|
||||||
|
@ -74,7 +74,7 @@ const typeConstraints = [
|
||||||
} options`
|
} options`
|
||||||
),
|
),
|
||||||
makerule(
|
makerule(
|
||||||
async (val, opts) =>
|
(val, opts) =>
|
||||||
val === null || opts.decimalPlaces >= getDecimalPlaces(val),
|
val === null || opts.decimalPlaces >= getDecimalPlaces(val),
|
||||||
(val, opts) =>
|
(val, opts) =>
|
||||||
`value (${val.toString()}) must have ${
|
`value (${val.toString()}) must have ${
|
||||||
|
|
|
@ -50,12 +50,12 @@ const options = {
|
||||||
|
|
||||||
const typeConstraints = [
|
const typeConstraints = [
|
||||||
makerule(
|
makerule(
|
||||||
async (val, opts) =>
|
(val, opts) =>
|
||||||
val === null || opts.maxLength === null || val.length <= opts.maxLength,
|
val === null || opts.maxLength === null || val.length <= opts.maxLength,
|
||||||
(val, opts) => `value exceeds maximum length of ${opts.maxLength}`
|
(val, opts) => `value exceeds maximum length of ${opts.maxLength}`
|
||||||
),
|
),
|
||||||
makerule(
|
makerule(
|
||||||
async (val, opts) =>
|
(val, opts) =>
|
||||||
val === null ||
|
val === null ||
|
||||||
opts.allowDeclaredValuesOnly === false ||
|
opts.allowDeclaredValuesOnly === false ||
|
||||||
includes(val)(opts.values),
|
includes(val)(opts.values),
|
||||||
|
|
|
@ -46,19 +46,16 @@ export const typeFunctions = specificFunctions =>
|
||||||
specificFunctions
|
specificFunctions
|
||||||
)
|
)
|
||||||
|
|
||||||
export const validateTypeConstraints = validationRules => async (
|
export const validateTypeConstraints = validationRules => (field, record) => {
|
||||||
field,
|
|
||||||
record
|
|
||||||
) => {
|
|
||||||
const fieldValue = record[field.name]
|
const fieldValue = record[field.name]
|
||||||
const validateRule = async r =>
|
const validateRule = r =>
|
||||||
!(await r.isValid(fieldValue, field.typeOptions))
|
!r.isValid(fieldValue, field.typeOptions)
|
||||||
? r.getMessage(fieldValue, field.typeOptions)
|
? r.getMessage(fieldValue, field.typeOptions)
|
||||||
: ""
|
: ""
|
||||||
|
|
||||||
const errors = []
|
const errors = []
|
||||||
for (const r of validationRules) {
|
for (const r of validationRules) {
|
||||||
const err = await validateRule(r)
|
const err = validateRule(r)
|
||||||
if (isNotEmpty(err)) errors.push(err)
|
if (isNotEmpty(err)) errors.push(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,7 @@ import { newModel } from "../src/schema/models.mjs"
|
||||||
import { newView } from "../src/schema/views.mjs"
|
import { newView } from "../src/schema/views.mjs"
|
||||||
import { getNewField } from "../src/schema/fields.mjs"
|
import { getNewField } from "../src/schema/fields.mjs"
|
||||||
import { fullSchema } from "../src/schema/fullSchema.mjs"
|
import { fullSchema } from "../src/schema/fullSchema.mjs"
|
||||||
import {
|
import { commonRecordValidationRules } from "../src/records/recordValidationRules.mjs"
|
||||||
recordValidationRules,
|
|
||||||
commonRecordValidationRules,
|
|
||||||
} from "../src/schema/recordValidationRules.mjs"
|
|
||||||
|
|
||||||
export function testSchema() {
|
export function testSchema() {
|
||||||
const addFieldToModel = (model, { type, name }) => {
|
const addFieldToModel = (model, { type, name }) => {
|
||||||
|
@ -21,9 +18,10 @@ export function testSchema() {
|
||||||
addFieldToModel(contactModel, { name: "Name" })
|
addFieldToModel(contactModel, { name: "Name" })
|
||||||
addFieldToModel(contactModel, { name: "Is Active", type: "bool" })
|
addFieldToModel(contactModel, { name: "Is Active", type: "bool" })
|
||||||
addFieldToModel(contactModel, { name: "Created", type: "datetime" })
|
addFieldToModel(contactModel, { name: "Created", type: "datetime" })
|
||||||
|
addFieldToModel(contactModel, { name: "Status", type: "string" })
|
||||||
|
|
||||||
contactModel.validationRules.push(
|
contactModel.validationRules.push(
|
||||||
recordValidationRules(commonRecordValidationRules.fieldNotEmpty)
|
commonRecordValidationRules.fieldNotEmpty("Name")
|
||||||
)
|
)
|
||||||
|
|
||||||
const activeContactsView = newView(contactModel.id)
|
const activeContactsView = newView(contactModel.id)
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import { addHours } from "date-fns"
|
|
||||||
import { events } from "../src/common"
|
|
||||||
import { testSchema } from "./testSchema.mjs"
|
import { testSchema } from "./testSchema.mjs"
|
||||||
import { validateRecord } from "../src/records/validateRecord.mjs"
|
import { validateRecord } from "../src/records/validateRecord.mjs"
|
||||||
import { getNewRecord } from "../src/records/getNewRecord.mjs"
|
import { getNewRecord } from "../src/records/getNewRecord.mjs"
|
||||||
|
|
||||||
describe("recordApi > validate", () => {
|
describe("validateRecord", () => {
|
||||||
it("should return errors when any fields do not parse", () => {
|
it("should return errors when any fields do not parse", () => {
|
||||||
const schema = testSchema()
|
const schema = testSchema()
|
||||||
const record = getNewRecord(schema, "Contact")
|
const record = getNewRecord(schema, "Contact")
|
||||||
|
@ -34,7 +32,7 @@ describe("recordApi > validate", () => {
|
||||||
const schema = testSchema()
|
const schema = testSchema()
|
||||||
schema.findField("Contact", "Name").typeOptions.maxLength = 5
|
schema.findField("Contact", "Name").typeOptions.maxLength = 5
|
||||||
const record = getNewRecord(schema, "Contact")
|
const record = getNewRecord(schema, "Contact")
|
||||||
record.name = "more than 5 characters"
|
record.Name = "more than 5 characters"
|
||||||
|
|
||||||
const validationResult = validateRecord(schema, record)
|
const validationResult = validateRecord(schema, record)
|
||||||
expect(validationResult.isValid).toBe(false)
|
expect(validationResult.isValid).toBe(false)
|
||||||
|
@ -47,213 +45,93 @@ describe("recordApi > validate", () => {
|
||||||
const record = getNewRecord(schema, "Deal")
|
const record = getNewRecord(schema, "Deal")
|
||||||
record["Estimated Value"] = 10
|
record["Estimated Value"] = 10
|
||||||
|
|
||||||
const validationResult = recordApi.validate(schema, 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 number field is < minValue", async () => {
|
it("should return error when number field is < minValue", () => {
|
||||||
const withFieldWithMaxLength = hierarchy => {
|
const schema = testSchema()
|
||||||
const age = find(hierarchy.customerRecord.fields, f => f.name === "age")
|
schema.findField("Deal", "Estimated Value").typeOptions.minValue = 5
|
||||||
age.typeOptions.minValue = 5
|
const record = getNewRecord(schema, "Deal")
|
||||||
}
|
record["Estimated Value"] = 1
|
||||||
|
|
||||||
const hierarchyCreator = hierarchyFactory(
|
const result = validateRecord(schema, record)
|
||||||
withFields,
|
|
||||||
withFieldWithMaxLength
|
|
||||||
)
|
|
||||||
const { recordApi } = await setupApphierarchy(hierarchyCreator)
|
|
||||||
|
|
||||||
const tooYoungRecord = recordApi.getNew("/customers", "customer")
|
|
||||||
tooYoungRecord.age = 3
|
|
||||||
|
|
||||||
const tooYoungResult = await recordApi.validate(tooYoungRecord)
|
|
||||||
expect(tooYoungResult.isValid).toBe(false)
|
|
||||||
expect(tooYoungResult.errors.length).toBe(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should return error when number has too many decimal places", async () => {
|
|
||||||
const withFieldWithMaxLength = (hierarchy, templateApi) => {
|
|
||||||
const age = find(hierarchy.customerRecord.fields, f => f.name === "age")
|
|
||||||
age.typeOptions.decimalPlaces = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
const hierarchyCreator = hierarchyFactory(
|
|
||||||
withFields,
|
|
||||||
withFieldWithMaxLength
|
|
||||||
)
|
|
||||||
const { recordApi } = await setupApphierarchy(hierarchyCreator)
|
|
||||||
|
|
||||||
const record = recordApi.getNew("/customers", "customer")
|
|
||||||
record.age = 3.123
|
|
||||||
|
|
||||||
const validationResult = await recordApi.validate(record)
|
|
||||||
expect(validationResult.isValid).toBe(false)
|
|
||||||
expect(validationResult.errors.length).toBe(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should return error when datetime field is > maxValue", async () => {
|
|
||||||
const withFieldWithMaxLength = hierarchy => {
|
|
||||||
const createddate = find(
|
|
||||||
hierarchy.customerRecord.fields,
|
|
||||||
f => f.name === "createddate"
|
|
||||||
)
|
|
||||||
createddate.typeOptions.maxValue = new Date()
|
|
||||||
}
|
|
||||||
|
|
||||||
const hierarchyCreator = hierarchyFactory(
|
|
||||||
withFields,
|
|
||||||
withFieldWithMaxLength
|
|
||||||
)
|
|
||||||
const { recordApi } = await setupApphierarchy(hierarchyCreator)
|
|
||||||
|
|
||||||
const record = recordApi.getNew("/customers", "customer")
|
|
||||||
record.createddate = addHours(new Date(), 1)
|
|
||||||
|
|
||||||
const result = await recordApi.validate(record)
|
|
||||||
expect(result.isValid).toBe(false)
|
expect(result.isValid).toBe(false)
|
||||||
expect(result.errors.length).toBe(1)
|
expect(result.errors.length).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should return error when number field is < minValue", async () => {
|
it("should return error when number has too many decimal places", () => {
|
||||||
const withFieldWithMaxLength = hierarchy => {
|
const schema = testSchema()
|
||||||
const createddate = find(
|
schema.findField("Deal", "Estimated Value").typeOptions.decimalPlaces = 2
|
||||||
hierarchy.customerRecord.fields,
|
const record = getNewRecord(schema, "Deal")
|
||||||
f => f.name === "createddate"
|
record["Estimated Value"] = 1.123
|
||||||
)
|
|
||||||
createddate.typeOptions.minValue = addHours(new Date(), 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const hierarchyCreator = hierarchyFactory(
|
const result = validateRecord(schema, record)
|
||||||
withFields,
|
|
||||||
withFieldWithMaxLength
|
|
||||||
)
|
|
||||||
const { recordApi } = await setupApphierarchy(hierarchyCreator)
|
|
||||||
|
|
||||||
const record = recordApi.getNew("/customers", "customer")
|
|
||||||
record.createddate = new Date()
|
|
||||||
|
|
||||||
const result = await recordApi.validate(record)
|
|
||||||
expect(result.isValid).toBe(false)
|
expect(result.isValid).toBe(false)
|
||||||
expect(result.errors.length).toBe(1)
|
expect(result.errors.length).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should return error when string IS NOT one of declared values, and only declared values are allowed", async () => {
|
it("should return error when datetime field is > maxValue", () => {
|
||||||
const withFieldWithMaxLength = hierarchy => {
|
const schema = testSchema()
|
||||||
const surname = find(
|
schema.findField("Contact", "Created").typeOptions.maxValue = new Date(2020, 1, 1)
|
||||||
hierarchy.customerRecord.fields,
|
const record = getNewRecord(schema, "Contact")
|
||||||
f => f.name === "surname"
|
record.Name = "Bob"
|
||||||
)
|
record.Created = new Date(2020, 1, 2)
|
||||||
surname.typeOptions.allowDeclaredValuesOnly = true
|
|
||||||
surname.typeOptions.values = ["thedog"]
|
|
||||||
}
|
|
||||||
|
|
||||||
const hierarchyCreator = hierarchyFactory(
|
const result = validateRecord(schema, record)
|
||||||
withFields,
|
|
||||||
withFieldWithMaxLength
|
|
||||||
)
|
|
||||||
const { recordApi } = await setupApphierarchy(hierarchyCreator)
|
|
||||||
|
|
||||||
const record = recordApi.getNew("/customers", "customer")
|
|
||||||
record.surname = "zeecat"
|
|
||||||
|
|
||||||
const result = await recordApi.validate(record)
|
|
||||||
expect(result.isValid).toBe(false)
|
expect(result.isValid).toBe(false)
|
||||||
expect(result.errors.length).toBe(1)
|
expect(result.errors.length).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should not return error when string IS one of declared values, and only declared values are allowed", async () => {
|
it("should return error when number field is < minValue", () => {
|
||||||
const withFieldWithMaxLength = hierarchy => {
|
const schema = testSchema()
|
||||||
const surname = find(
|
schema.findField("Contact", "Created").typeOptions.minValue = new Date(2020, 1, 2)
|
||||||
hierarchy.customerRecord.fields,
|
const record = getNewRecord(schema, "Contact")
|
||||||
f => f.name === "surname"
|
record.Name = "Bob"
|
||||||
)
|
record.Created = new Date(2020, 1, 1)
|
||||||
surname.typeOptions.allowDeclaredValuesOnly = true
|
|
||||||
surname.typeOptions.values = ["thedog"]
|
|
||||||
}
|
|
||||||
|
|
||||||
const hierarchyCreator = hierarchyFactory(
|
const result = validateRecord(schema, record)
|
||||||
withFields,
|
expect(result.isValid).toBe(false)
|
||||||
withFieldWithMaxLength
|
expect(result.errors.length).toBe(1)
|
||||||
)
|
})
|
||||||
const { recordApi, appHierarchy } = await setupApphierarchy(
|
|
||||||
hierarchyCreator
|
|
||||||
)
|
|
||||||
|
|
||||||
const record = recordApi.getNew("/customers", "customer")
|
it("should return error when string IS NOT one of declared values, and only declared values are allowed", () => {
|
||||||
record.surname = "thedog"
|
const schema = testSchema()
|
||||||
|
schema.findField("Contact", "Status").typeOptions.allowDeclaredValuesOnly = true
|
||||||
|
schema.findField("Contact", "Status").typeOptions.values = ["thedog"]
|
||||||
|
const record = getNewRecord(schema, "Contact")
|
||||||
|
record.Status = "not allowed"
|
||||||
|
record.Name = "Bob"
|
||||||
|
|
||||||
const result = await recordApi.validate(record)
|
const result = validateRecord(schema, record)
|
||||||
|
expect(result.isValid).toBe(false)
|
||||||
|
expect(result.errors.length).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not return error when string IS one of declared values, and only declared values are allowed", () => {
|
||||||
|
const schema = testSchema()
|
||||||
|
schema.findField("Contact", "Status").typeOptions.allowDeclaredValuesOnly = true
|
||||||
|
schema.findField("Contact", "Status").typeOptions.values = ["thedog"]
|
||||||
|
const record = getNewRecord(schema, "Contact")
|
||||||
|
record.Status = "thedog"
|
||||||
|
record.Name = "Bob"
|
||||||
|
|
||||||
|
const result = validateRecord(schema, record)
|
||||||
expect(result.isValid).toBe(true)
|
expect(result.isValid).toBe(true)
|
||||||
expect(result.errors.length).toBe(0)
|
expect(result.errors.length).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should not return error when string IS NOT one of declared values, but any values are allowed", async () => {
|
it("should not return error when string IS NOT one of declared values, but any values are allowed", () => {
|
||||||
const withFieldWithMaxLength = (hierarchy, templateApi) => {
|
const schema = testSchema()
|
||||||
const surname = find(
|
schema.findField("Contact", "Status").typeOptions.allowDeclaredValuesOnly = false
|
||||||
hierarchy.customerRecord.fields,
|
schema.findField("Contact", "Status").typeOptions.values = ["thedog"]
|
||||||
f => f.name === "surname"
|
const record = getNewRecord(schema, "Contact")
|
||||||
)
|
record.Status = "not one of the values"
|
||||||
surname.typeOptions.allowDeclaredValuesOnly = false
|
record.Name = "Bob"
|
||||||
surname.typeOptions.values = ["thedog"]
|
|
||||||
}
|
|
||||||
|
|
||||||
const hierarchyCreator = hierarchyFactory(
|
const result = validateRecord(schema, record)
|
||||||
withFields,
|
|
||||||
withFieldWithMaxLength
|
|
||||||
)
|
|
||||||
const { recordApi, appHierarchy } = await setupApphierarchy(
|
|
||||||
hierarchyCreator
|
|
||||||
)
|
|
||||||
|
|
||||||
const record = recordApi.getNew("/customers", "customer")
|
|
||||||
record.surname = "zeecat"
|
|
||||||
|
|
||||||
const result = await recordApi.validate(record)
|
|
||||||
expect(result.isValid).toBe(true)
|
expect(result.isValid).toBe(true)
|
||||||
expect(result.errors.length).toBe(0)
|
expect(result.errors.length).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should return error when reference field does not exist in options index", async () => {
|
|
||||||
const { recordApi, appHierarchy } = await setupApphierarchy(
|
|
||||||
basicAppHierarchyCreator_WithFields_AndIndexes
|
|
||||||
)
|
|
||||||
|
|
||||||
const partner = recordApi.getNew("/partners", "partner")
|
|
||||||
partner.businessName = "ACME Inc"
|
|
||||||
await recordApi.save(partner)
|
|
||||||
|
|
||||||
const customer = recordApi.getNew("/customers", "customer")
|
|
||||||
customer.partner = { key: "incorrect key", name: partner.businessName }
|
|
||||||
const result = await await recordApi.validate(customer)
|
|
||||||
expect(result.isValid).toBe(false)
|
|
||||||
expect(result.errors.length).toBe(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should publish invalid events", async () => {
|
|
||||||
const withValidationRule = (hierarchy, templateApi) => {
|
|
||||||
templateApi.addRecordValidationRule(hierarchy.customerRecord)(
|
|
||||||
templateApi.commonRecordValidationRules.fieldNotEmpty("surname")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const hierarchyCreator = hierarchyFactory(withFields, withValidationRule)
|
|
||||||
|
|
||||||
const { recordApi, subscribe } = await setupApphierarchy(hierarchyCreator)
|
|
||||||
const handler = stubEventHandler()
|
|
||||||
subscribe(events.recordApi.save.onInvalid, handler.handle)
|
|
||||||
|
|
||||||
const record = recordApi.getNew("/customers", "customer")
|
|
||||||
record.surname = ""
|
|
||||||
|
|
||||||
try {
|
|
||||||
await recordApi.save(record)
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
const onInvalid = handler.getEvents(events.recordApi.save.onInvalid)
|
|
||||||
expect(onInvalid.length).toBe(1)
|
|
||||||
expect(onInvalid[0].context.record).toBeDefined()
|
|
||||||
expect(onInvalid[0].context.record.key).toBe(record.key)
|
|
||||||
expect(onInvalid[0].context.validationResult).toBeDefined()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -15,17 +15,6 @@ const _events = {
|
||||||
uploadFile: common(),
|
uploadFile: common(),
|
||||||
downloadFile: common(),
|
downloadFile: common(),
|
||||||
},
|
},
|
||||||
indexApi: {
|
|
||||||
buildIndex: common(),
|
|
||||||
listItems: common(),
|
|
||||||
delete: common(),
|
|
||||||
aggregates: common(),
|
|
||||||
},
|
|
||||||
collectionApi: {
|
|
||||||
getAllowedRecordTypes: common(),
|
|
||||||
initialise: common(),
|
|
||||||
delete: common(),
|
|
||||||
},
|
|
||||||
authApi: {
|
authApi: {
|
||||||
authenticate: common(),
|
authenticate: common(),
|
||||||
authenticateTemporaryAccess: common(),
|
authenticateTemporaryAccess: common(),
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
export function allModelsViewName(modelId) {
|
||||||
|
return `all_${modelId}`
|
||||||
|
}
|
||||||
|
export function allModelsDesignDocName(modelId) {
|
||||||
|
return `all_${modelId}`
|
||||||
|
}
|
||||||
|
export function instanceDatabaseId (clientId, instanceId) {
|
||||||
|
return `instance:${clientId}:${instanceId}`
|
||||||
|
}
|
||||||
|
export function clientDatabaseId(clientId) {
|
||||||
|
return `client:${clientId}`
|
||||||
|
}
|
|
@ -1,11 +1,15 @@
|
||||||
const couchdb = require("../../db")
|
const couchdb = require("../../db")
|
||||||
const { cloneDeep, mapValues, keyBy, filter, includes } = require("lodash/fp")
|
const { cloneDeep, mapValues, keyBy } = require("lodash/fp")
|
||||||
const {
|
const {
|
||||||
validateRecord,
|
validateRecord,
|
||||||
} = require("../../../common/src/records/validateRecord.mjs")
|
} = require("../../../common/src/records/validateRecord.mjs")
|
||||||
const { events } = require("../../../common/src/common/events.mjs")
|
const { events } = require("../../../common/src/common/events.mjs")
|
||||||
const { $, isNonEmptyString } = require("../../../common/src/common")
|
const { $ } = require("../../../common/src/common")
|
||||||
import { safeParseField } from "../../../common/src/schema/types"
|
import { safeParseField } from "../../../common/src/schema/types"
|
||||||
|
import {
|
||||||
|
allModelsViewName,
|
||||||
|
allModelsDesignDocName,
|
||||||
|
} from "./couchdbNamingConventions"
|
||||||
|
|
||||||
async function save(ctx) {
|
async function save(ctx) {
|
||||||
const db = couchdb.use(ctx.databaseId)
|
const db = couchdb.use(ctx.databaseId)
|
||||||
|
@ -19,7 +23,7 @@ async function save(ctx) {
|
||||||
|
|
||||||
const validationResult = await validateRecord(ctx.schema, record)
|
const validationResult = await validateRecord(ctx.schema, record)
|
||||||
if (!validationResult.isValid) {
|
if (!validationResult.isValid) {
|
||||||
await app.publish(events.recordApi.save.onInvalid, {
|
await ctx.publish(events.recordApi.save.onInvalid, {
|
||||||
record,
|
record,
|
||||||
validationResult,
|
validationResult,
|
||||||
})
|
})
|
||||||
|
@ -30,13 +34,13 @@ async function save(ctx) {
|
||||||
|
|
||||||
if (!record._rev) {
|
if (!record._rev) {
|
||||||
await db.insert(record)
|
await db.insert(record)
|
||||||
await app.publish(events.recordApi.save.onRecordCreated, {
|
await ctx.publish(events.recordApi.save.onRecordCreated, {
|
||||||
record: record,
|
record: record,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const oldRecord = await _findRecord(db, ctx.schema, record._id)
|
const oldRecord = await _findRecord(db, ctx.schema, record._id)
|
||||||
await db.insert(record)
|
await db.insert(record)
|
||||||
await app.publish(events.recordApi.save.onRecordUpdated, {
|
await ctx.publish(events.recordApi.save.onRecordUpdated, {
|
||||||
old: oldRecord,
|
old: oldRecord,
|
||||||
new: record,
|
new: record,
|
||||||
})
|
})
|
||||||
|
@ -49,16 +53,23 @@ async function save(ctx) {
|
||||||
|
|
||||||
async function fetch(ctx) {
|
async function fetch(ctx) {
|
||||||
const db = couchdb.db.use(ctx.params.databaseId)
|
const db = couchdb.db.use(ctx.params.databaseId)
|
||||||
|
const model = ctx.schema.findModel(ctx.modelName)
|
||||||
ctx.body = await db.view("database", "all_somemodel", {
|
ctx.body = db.viewAsStream(
|
||||||
include_docs: true,
|
allModelsDesignDocName(model.id),
|
||||||
key: ["app"]
|
allModelsViewName(model.id),
|
||||||
})
|
{
|
||||||
|
include_docs: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function find(ctx) {
|
async function find(ctx) {
|
||||||
const db = couchdb.db.use(ctx.params.databaseId)
|
const db = couchdb.db.use(ctx.params.databaseId)
|
||||||
const { body, status } = await _findRecord(db, ctx.schema, ctx.params.id)
|
const { body, status } = await _findRecord(
|
||||||
|
db,
|
||||||
|
ctx.schema,
|
||||||
|
ctx.params.recordId
|
||||||
|
)
|
||||||
ctx.status = status
|
ctx.status = status
|
||||||
ctx.body = body
|
ctx.body = body
|
||||||
}
|
}
|
||||||
|
@ -78,28 +89,6 @@ async function _findRecord(db, schema, id) {
|
||||||
mapValues(f => safeParseField(f, storedData)),
|
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._rev = storedData._rev
|
||||||
loadedRecord._id = storedData._id
|
loadedRecord._id = storedData._id
|
||||||
loadedRecord._modelId = storedData._modelId
|
loadedRecord._modelId = storedData._modelId
|
||||||
|
@ -112,4 +101,4 @@ async function destroy(ctx) {
|
||||||
ctx.body = await database.destroy(ctx.params.recordId);
|
ctx.body = await database.destroy(ctx.params.recordId);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {dave, fetch, destroy, find};
|
module.exports = { save, fetch, destroy, find }
|
||||||
|
|
|
@ -4,8 +4,9 @@ const controller = require("../../controllers/record");
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router
|
router
|
||||||
.get("/api/:databaseId/records", controller.fetch)
|
.get("/api/:databaseId/records/:modelname", controller.fetch)
|
||||||
.post("/api/:databaseId/records", controller.save)
|
.post("/api/:databaseId/record", controller.save)
|
||||||
.delete("/api/:databaseId/records/:recordId", controller.destroy)
|
.get("/api/:databaseId/record/:recordId", controller.find)
|
||||||
|
.delete("/api/:databaseId/record/:recordId", controller.destroy)
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
Loading…
Reference in New Issue