adding save & load controllers
This commit is contained in:
parent
195275fc9e
commit
ebc1c44343
|
@ -27,7 +27,7 @@ import {
|
||||||
includes,
|
includes,
|
||||||
filter,
|
filter,
|
||||||
} from "lodash/fp"
|
} from "lodash/fp"
|
||||||
import { events, eventsList } from "./events"
|
import { events, eventsList } from "./events.mjs"
|
||||||
|
|
||||||
// this is the combinator function
|
// this is the combinator function
|
||||||
export const $$ = (...funcs) => arg => flow(funcs)(arg)
|
export const $$ = (...funcs) => arg => flow(funcs)(arg)
|
||||||
|
@ -241,7 +241,7 @@ export const retry = async (fn, retries, delay, ...args) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { events } from "./events"
|
export { events } from "./events.mjs"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
ifExists,
|
ifExists,
|
|
@ -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,18 +1,19 @@
|
||||||
import { map, reduce, filter, isEmpty, flatten, each } from "lodash/fp"
|
import { map, reduce, filter, isEmpty, flatten, each } from "lodash/fp"
|
||||||
import { compileCode } from "../common/compileCode"
|
import { compileCode } from "../common/compileCode.mjs"
|
||||||
import _ from "lodash"
|
import _ from "lodash"
|
||||||
import { getExactNodeForKey } from "../templateApi/hierarchy"
|
import {
|
||||||
import { validateFieldParse, validateTypeConstraints } from "../types"
|
validateFieldParse,
|
||||||
import { $, isNothing, isNonEmptyString } from "../common"
|
validateTypeConstraints,
|
||||||
import { _getContext } from "./getContext"
|
} from "../schema/types/index.mjs"
|
||||||
|
import { $, isNonEmptyString } from "../common/index.mjs"
|
||||||
|
|
||||||
const fieldParseError = (fieldName, value) => ({
|
const fieldParseError = (fieldName, value) => ({
|
||||||
fields: [fieldName],
|
fields: [fieldName],
|
||||||
message: `Could not parse field ${fieldName}:${value}`,
|
message: `Could not parse field ${fieldName}:${value}`,
|
||||||
})
|
})
|
||||||
|
|
||||||
const validateAllFieldParse = (record, recordNode) =>
|
const validateAllFieldParse = (record, model) =>
|
||||||
$(recordNode.fields, [
|
$(model.fields, [
|
||||||
map(f => ({ name: f.name, parseResult: validateFieldParse(f, record) })),
|
map(f => ({ name: f.name, parseResult: validateFieldParse(f, record) })),
|
||||||
reduce((errors, f) => {
|
reduce((errors, f) => {
|
||||||
if (f.parseResult.success) return errors
|
if (f.parseResult.success) return errors
|
||||||
|
@ -21,10 +22,10 @@ const validateAllFieldParse = (record, recordNode) =>
|
||||||
}, []),
|
}, []),
|
||||||
])
|
])
|
||||||
|
|
||||||
const validateAllTypeConstraints = async (record, recordNode, context) => {
|
const validateAllTypeConstraints = async (record, model) => {
|
||||||
const errors = []
|
const errors = []
|
||||||
for (const field of recordNode.fields) {
|
for (const field of model.fields) {
|
||||||
$(await validateTypeConstraints(field, record, context), [
|
$(await 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)),
|
||||||
|
@ -33,10 +34,10 @@ const validateAllTypeConstraints = async (record, recordNode, context) => {
|
||||||
return errors
|
return errors
|
||||||
}
|
}
|
||||||
|
|
||||||
const runRecordValidationRules = (record, recordNode) => {
|
const runRecordValidationRules = (record, model) => {
|
||||||
const runValidationRule = rule => {
|
const runValidationRule = rule => {
|
||||||
const isValid = compileCode(rule.expressionWhenValid)
|
const isValid = compileCode(rule.expressionWhenValid)
|
||||||
const expressionContext = { record, _ }
|
const expressionContext = { record }
|
||||||
return isValid(expressionContext)
|
return isValid(expressionContext)
|
||||||
? { valid: true }
|
? { valid: true }
|
||||||
: {
|
: {
|
||||||
|
@ -46,7 +47,7 @@ const runRecordValidationRules = (record, recordNode) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $(recordNode.validationRules, [
|
return $(model.validationRules, [
|
||||||
map(runValidationRule),
|
map(runValidationRule),
|
||||||
flatten,
|
flatten,
|
||||||
filter(r => r.valid === false),
|
filter(r => r.valid === false),
|
||||||
|
@ -54,23 +55,17 @@ const runRecordValidationRules = (record, recordNode) => {
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
export const validate = app => async (record, context) => {
|
export const validateRecord = async (schema, record) => {
|
||||||
context = isNothing(context) ? _getContext(app, record.key) : context
|
const model = schema.findModel(record.modelId)
|
||||||
|
const fieldParseFails = validateAllFieldParse(record, model)
|
||||||
const recordNode = getExactNodeForKey(app.hierarchy)(record.key)
|
|
||||||
const fieldParseFails = validateAllFieldParse(record, recordNode)
|
|
||||||
|
|
||||||
// non parsing would cause further issues - exit here
|
// non parsing would cause further issues - exit here
|
||||||
if (!isEmpty(fieldParseFails)) {
|
if (!isEmpty(fieldParseFails)) {
|
||||||
return { isValid: false, errors: fieldParseFails }
|
return { isValid: false, errors: fieldParseFails }
|
||||||
}
|
}
|
||||||
|
|
||||||
const recordValidationRuleFails = runRecordValidationRules(record, recordNode)
|
const recordValidationRuleFails = runRecordValidationRules(record, model)
|
||||||
const typeContraintFails = await validateAllTypeConstraints(
|
const typeContraintFails = await validateAllTypeConstraints(record, model)
|
||||||
record,
|
|
||||||
recordNode,
|
|
||||||
context
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isEmpty(fieldParseFails) &&
|
isEmpty(fieldParseFails) &&
|
|
@ -1,15 +1,23 @@
|
||||||
export const fullSchema = (models, views) => {
|
export const fullSchema = (models, views) => {
|
||||||
const findModel = idOrName =>
|
const findModel = idOrName =>
|
||||||
models.find(m => m.id === idOrName || m.name === idOrName)
|
models.find(
|
||||||
|
m => m.id === idOrName || m.name.toLowerCase() === idOrName.toLowerCase()
|
||||||
|
)
|
||||||
|
|
||||||
const findView = idOrName =>
|
const findView = idOrName =>
|
||||||
views.find(m => m.id === idOrName || m.name === idOrName)
|
views.find(
|
||||||
|
m => m.id === idOrName || m.name.toLowerCase() === idOrName.toLowerCase()
|
||||||
|
)
|
||||||
|
|
||||||
const findField = (modelIdOrName, fieldName) => {
|
const findField = (modelIdOrName, fieldName) => {
|
||||||
const model = models.find(
|
const model = models.find(
|
||||||
m => m.id === modelIdOrName || m.name === modelIdOrName
|
m =>
|
||||||
|
m.id === modelIdOrName ||
|
||||||
|
m.name.toLowerCase() === modelIdOrName.toLowerCase()
|
||||||
|
)
|
||||||
|
return model.fields.find(
|
||||||
|
f => f.name.toLowerCase() === fieldName.toLowerCase()
|
||||||
)
|
)
|
||||||
return model.fields.find(f => f.name === fieldName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewsForModel = modelId => views.filter(v => v.modelId === modelId)
|
const viewsForModel = modelId => views.filter(v => v.modelId === modelId)
|
||||||
|
|
|
@ -12,7 +12,7 @@ import {
|
||||||
toNumberOrNull,
|
toNumberOrNull,
|
||||||
$$,
|
$$,
|
||||||
isSafeInteger,
|
isSafeInteger,
|
||||||
} from "../../common"
|
} from "../../common/index.mjs"
|
||||||
|
|
||||||
const arrayFunctions = () =>
|
const arrayFunctions = () =>
|
||||||
typeFunctions({
|
typeFunctions({
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
parsedSuccess,
|
parsedSuccess,
|
||||||
getDefaultExport,
|
getDefaultExport,
|
||||||
} from "./typeHelpers"
|
} from "./typeHelpers"
|
||||||
import { switchCase, defaultCase, isOneOf, toBoolOrNull } from "../../common"
|
import { switchCase, defaultCase, isOneOf, toBoolOrNull } from "../../common/index.mjs"
|
||||||
|
|
||||||
const boolFunctions = typeFunctions({
|
const boolFunctions = typeFunctions({
|
||||||
default: constant(null),
|
default: constant(null),
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
parsedSuccess,
|
parsedSuccess,
|
||||||
getDefaultExport,
|
getDefaultExport,
|
||||||
} from "./typeHelpers"
|
} from "./typeHelpers"
|
||||||
import { switchCase, defaultCase, toDateOrNull, isNonEmptyArray } from "../../common"
|
import { switchCase, defaultCase, toDateOrNull } from "../../common"
|
||||||
|
|
||||||
const dateFunctions = typeFunctions({
|
const dateFunctions = typeFunctions({
|
||||||
default: constant(null),
|
default: constant(null),
|
||||||
|
@ -21,13 +21,9 @@ const parseStringToDate = s =>
|
||||||
[defaultCase, parsedFailed]
|
[defaultCase, parsedFailed]
|
||||||
)(new Date(s))
|
)(new Date(s))
|
||||||
|
|
||||||
const isNullOrEmpty = d =>
|
const isNullOrEmpty = d => isNull(d) || (d || "").toString() === ""
|
||||||
isNull(d)
|
|
||||||
|| (d || "").toString() === ""
|
|
||||||
|
|
||||||
const isDateOrEmpty = d =>
|
const isDateOrEmpty = d => isDate(d) || isNullOrEmpty(d)
|
||||||
isDate(d)
|
|
||||||
|| isNullOrEmpty(d)
|
|
||||||
|
|
||||||
const dateTryParse = switchCase(
|
const dateTryParse = switchCase(
|
||||||
[isDateOrEmpty, parsedSuccess],
|
[isDateOrEmpty, parsedSuccess],
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {
|
||||||
parsedSuccess,
|
parsedSuccess,
|
||||||
getDefaultExport,
|
getDefaultExport,
|
||||||
} from "./typeHelpers"
|
} from "./typeHelpers"
|
||||||
import { switchCase, defaultCase, none, $, splitKey } from "../../common"
|
import { switchCase, defaultCase, none, $, splitKey } from "../../common/index.mjs"
|
||||||
|
|
||||||
const illegalCharacters = "*?\\/:<>|\0\b\f\v"
|
const illegalCharacters = "*?\\/:<>|\0\b\f\v"
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
isArray,
|
isArray,
|
||||||
has,
|
has,
|
||||||
} from "lodash/fp"
|
} from "lodash/fp"
|
||||||
import { $ } from "../../common"
|
import { $ } from "../../common/index.mjs"
|
||||||
import { parsedSuccess } from "./typeHelpers"
|
import { parsedSuccess } from "./typeHelpers"
|
||||||
import string from "./string"
|
import string from "./string"
|
||||||
import bool from "./bool"
|
import bool from "./bool"
|
||||||
|
@ -19,7 +19,7 @@ import datetime from "./datetime"
|
||||||
import array from "./array"
|
import array from "./array"
|
||||||
import link from "./link"
|
import link from "./link"
|
||||||
import file from "./file"
|
import file from "./file"
|
||||||
import { BadRequestError } from "../../common/errors"
|
import { BadRequestError } from "../../common/errors.mjs"
|
||||||
|
|
||||||
const allTypes = () => {
|
const allTypes = () => {
|
||||||
const basicTypes = {
|
const basicTypes = {
|
||||||
|
@ -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, context) =>
|
export const validateTypeConstraints = async (field, record) =>
|
||||||
await getType(field.type).validateTypeConstraints(field, record, context)
|
await getType(field.type).validateTypeConstraints(field, record)
|
||||||
|
|
||||||
export const detectType = value => {
|
export const detectType = value => {
|
||||||
if (isString(value)) return string
|
if (isString(value)) return string
|
||||||
|
@ -76,8 +76,7 @@ export const detectType = value => {
|
||||||
if (isNumber(value)) return number
|
if (isNumber(value)) return number
|
||||||
if (isDate(value)) return datetime
|
if (isDate(value)) return datetime
|
||||||
if (isArray(value)) return array(detectType(value[0]))
|
if (isArray(value)) return array(detectType(value[0]))
|
||||||
if (isObject(value) && has("key")(value) && has("value")(value))
|
if (isObject(value) && has("key")(value) && has("value")(value)) return link
|
||||||
return link
|
|
||||||
if (isObject(value) && has("relativePath")(value) && has("size")(value))
|
if (isObject(value) && has("relativePath")(value) && has("size")(value))
|
||||||
return file
|
return file
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { isString, isObjectLike, isNull, has, isEmpty } from "lodash/fp"
|
import { isString, isObjectLike, isNull, has } from "lodash/fp"
|
||||||
import {
|
import {
|
||||||
typeFunctions,
|
typeFunctions,
|
||||||
makerule,
|
|
||||||
parsedSuccess,
|
parsedSuccess,
|
||||||
getDefaultExport,
|
getDefaultExport,
|
||||||
parsedFailed,
|
parsedFailed,
|
||||||
|
@ -11,7 +10,7 @@ import {
|
||||||
defaultCase,
|
defaultCase,
|
||||||
isNonEmptyString,
|
isNonEmptyString,
|
||||||
isArrayOfString,
|
isArrayOfString,
|
||||||
} from "../../common"
|
} from "../../common/index.mjs"
|
||||||
|
|
||||||
const linkNothing = () => ({ key: "" })
|
const linkNothing = () => ({ key: "" })
|
||||||
|
|
||||||
|
@ -65,20 +64,7 @@ const options = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const isEmptyString = s => isString(s) && isEmpty(s)
|
const typeConstraints = []
|
||||||
|
|
||||||
const ensurelinkExists = async (val, opts, context) =>
|
|
||||||
isEmptyString(val.key) || (await context.linkExists(opts, val.key))
|
|
||||||
|
|
||||||
const typeConstraints = [
|
|
||||||
makerule(
|
|
||||||
ensurelinkExists,
|
|
||||||
(val, opts) =>
|
|
||||||
`"${val[opts.displayValue]}" does not exist in options list (key: ${
|
|
||||||
val.key
|
|
||||||
})`
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
export default getDefaultExport(
|
export default getDefaultExport(
|
||||||
"link",
|
"link",
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {
|
||||||
defaultCase,
|
defaultCase,
|
||||||
toNumberOrNull,
|
toNumberOrNull,
|
||||||
isSafeInteger,
|
isSafeInteger,
|
||||||
} from "../../common"
|
} from "../../common/index.mjs"
|
||||||
|
|
||||||
const numberFunctions = typeFunctions({
|
const numberFunctions = typeFunctions({
|
||||||
default: constant(null),
|
default: constant(null),
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {
|
||||||
parsedSuccess,
|
parsedSuccess,
|
||||||
getDefaultExport,
|
getDefaultExport,
|
||||||
} from "./typeHelpers"
|
} from "./typeHelpers"
|
||||||
import { switchCase, defaultCase, $ } from "../../common"
|
import { switchCase, defaultCase, $ } from "../../common/index.mjs"
|
||||||
|
|
||||||
const objectFunctions = (definition, allTypes) =>
|
const objectFunctions = (definition, allTypes) =>
|
||||||
typeFunctions({
|
typeFunctions({
|
||||||
|
|
|
@ -12,7 +12,7 @@ import {
|
||||||
toNumberOrNull,
|
toNumberOrNull,
|
||||||
isSafeInteger,
|
isSafeInteger,
|
||||||
isArrayOfString,
|
isArrayOfString,
|
||||||
} from "../../common"
|
} from "../../common/index.mjs"
|
||||||
|
|
||||||
const stringFunctions = typeFunctions({
|
const stringFunctions = typeFunctions({
|
||||||
default: constant(null),
|
default: constant(null),
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { merge } from "lodash"
|
import { merge } from "lodash"
|
||||||
import { constant, isUndefined, has, mapValues, cloneDeep } from "lodash/fp"
|
import { constant, isUndefined, has, mapValues, cloneDeep } from "lodash/fp"
|
||||||
import { isNotEmpty } from "../../common"
|
import { isNotEmpty } from "../../common/index.mjs"
|
||||||
|
|
||||||
export const getSafeFieldParser = (tryParse, defaultValueFunctions) => (
|
export const getSafeFieldParser = (tryParse, defaultValueFunctions) => (
|
||||||
field,
|
field,
|
||||||
|
@ -48,12 +48,11 @@ export const typeFunctions = specificFunctions =>
|
||||||
|
|
||||||
export const validateTypeConstraints = validationRules => async (
|
export const validateTypeConstraints = validationRules => async (
|
||||||
field,
|
field,
|
||||||
record,
|
record
|
||||||
context
|
|
||||||
) => {
|
) => {
|
||||||
const fieldValue = record[field.name]
|
const fieldValue = record[field.name]
|
||||||
const validateRule = async r =>
|
const validateRule = async r =>
|
||||||
!(await r.isValid(fieldValue, field.typeOptions, context))
|
!(await r.isValid(fieldValue, field.typeOptions))
|
||||||
? r.getMessage(fieldValue, field.typeOptions)
|
? r.getMessage(fieldValue, field.typeOptions)
|
||||||
: ""
|
: ""
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,10 @@ 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 {
|
||||||
|
recordValidationRules,
|
||||||
|
commonRecordValidationRules,
|
||||||
|
} from "../src/schema/recordValidationRules.mjs"
|
||||||
|
|
||||||
export function testSchema() {
|
export function testSchema() {
|
||||||
const addFieldToModel = (model, { type, name }) => {
|
const addFieldToModel = (model, { type, name }) => {
|
||||||
|
@ -18,6 +22,10 @@ export function testSchema() {
|
||||||
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" })
|
||||||
|
|
||||||
|
contactModel.validationRules.push(
|
||||||
|
recordValidationRules(commonRecordValidationRules.fieldNotEmpty)
|
||||||
|
)
|
||||||
|
|
||||||
const activeContactsView = newView(contactModel.id)
|
const activeContactsView = newView(contactModel.id)
|
||||||
activeContactsView.name = "Active Contacts"
|
activeContactsView.name = "Active Contacts"
|
||||||
activeContactsView.map = "if (doc['Is Active']) emit(doc.Name, doc)"
|
activeContactsView.map = "if (doc['Is Active']) emit(doc.Name, doc)"
|
||||||
|
|
|
@ -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 validationResult = validateRecord(schema, record)
|
||||||
const { recordApi } = await setupApphierarchy(hierarchyCreator)
|
|
||||||
|
|
||||||
const record = recordApi.getNew("/customers", "customer")
|
|
||||||
|
|
||||||
record.surname = ""
|
|
||||||
|
|
||||||
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 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(
|
const validationResult = validateRecord(schema, record)
|
||||||
withFields,
|
|
||||||
withFieldWithMaxLength
|
|
||||||
)
|
|
||||||
const { recordApi } = await setupApphierarchy(hierarchyCreator)
|
|
||||||
|
|
||||||
const record = recordApi.getNew("/customers", "customer")
|
|
||||||
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(
|
const validationResult = recordApi.validate(schema, record)
|
||||||
withFields,
|
expect(validationResult.isValid).toBe(false)
|
||||||
withFieldWithMaxLength
|
expect(validationResult.errors.length).toBe(1)
|
||||||
)
|
|
||||||
const { recordApi } = await setupApphierarchy(hierarchyCreator)
|
|
||||||
|
|
||||||
const tooOldRecord = recordApi.getNew("/customers", "customer")
|
|
||||||
tooOldRecord.age = 11
|
|
||||||
|
|
||||||
const tooOldResult = await recordApi.validate(tooOldRecord)
|
|
||||||
expect(tooOldResult.isValid).toBe(false)
|
|
||||||
expect(tooOldResult.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
|
||||||
destroy: async ctx => {
|
ctx.message = "record failed validation rules"
|
||||||
const databaseId = ctx.params.databaseId;
|
ctx.body = validationResult
|
||||||
const database = couchdb.db.use(databaseId)
|
}
|
||||||
ctx.body = await database.destroy(ctx.params.recordId);
|
|
||||||
},
|
if (!record._rev) {
|
||||||
|
await db.insert(record)
|
||||||
|
await app.publish(events.recordApi.save.onRecordCreated, {
|
||||||
|
record: record,
|
||||||
|
})
|
||||||
|
} 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
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = controller;
|
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 = {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 ")
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
|
@ -2983,7 +2983,7 @@ lodash.sortby@^4.7.0:
|
||||||
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
||||||
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
|
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
|
||||||
|
|
||||||
lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.4, lodash@^4.2.1:
|
lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.4, lodash@^4.2.1:
|
||||||
version "4.17.15"
|
version "4.17.15"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
|
||||||
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
|
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
|
||||||
|
|
Loading…
Reference in New Issue