1
0
Fork 0
mirror of synced 2024-06-28 02:50:50 +12:00

adding save & load controllers

This commit is contained in:
Michael Shanks 2020-04-09 10:13:19 +01:00 committed by Martin McKeaveney
parent 8b231c0930
commit 6954bf20cc
26 changed files with 243 additions and 164 deletions

View file

@ -27,7 +27,7 @@ import {
includes,
filter,
} from "lodash/fp"
import { events, eventsList } from "./events"
import { events, eventsList } from "./events.mjs"
// this is the combinator function
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 {
ifExists,

View file

@ -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} `
),
}

View file

@ -1,18 +1,19 @@
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 { getExactNodeForKey } from "../templateApi/hierarchy"
import { validateFieldParse, validateTypeConstraints } from "../types"
import { $, isNothing, isNonEmptyString } from "../common"
import { _getContext } from "./getContext"
import {
validateFieldParse,
validateTypeConstraints,
} from "../schema/types/index.mjs"
import { $, isNonEmptyString } from "../common/index.mjs"
const fieldParseError = (fieldName, value) => ({
fields: [fieldName],
message: `Could not parse field ${fieldName}:${value}`,
})
const validateAllFieldParse = (record, recordNode) =>
$(recordNode.fields, [
const validateAllFieldParse = (record, model) =>
$(model.fields, [
map(f => ({ name: f.name, parseResult: validateFieldParse(f, record) })),
reduce((errors, f) => {
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 = []
for (const field of recordNode.fields) {
$(await validateTypeConstraints(field, record, context), [
for (const field of model.fields) {
$(await validateTypeConstraints(field, record), [
filter(isNonEmptyString),
map(m => ({ message: m, fields: [field.name] })),
each(e => errors.push(e)),
@ -33,10 +34,10 @@ const validateAllTypeConstraints = async (record, recordNode, context) => {
return errors
}
const runRecordValidationRules = (record, recordNode) => {
const runRecordValidationRules = (record, model) => {
const runValidationRule = rule => {
const isValid = compileCode(rule.expressionWhenValid)
const expressionContext = { record, _ }
const expressionContext = { record }
return isValid(expressionContext)
? { valid: true }
: {
@ -46,7 +47,7 @@ const runRecordValidationRules = (record, recordNode) => {
}
}
return $(recordNode.validationRules, [
return $(model.validationRules, [
map(runValidationRule),
flatten,
filter(r => r.valid === false),
@ -54,23 +55,17 @@ const runRecordValidationRules = (record, recordNode) => {
])
}
export const validate = app => async (record, context) => {
context = isNothing(context) ? _getContext(app, record.key) : context
const recordNode = getExactNodeForKey(app.hierarchy)(record.key)
const fieldParseFails = validateAllFieldParse(record, recordNode)
export const validateRecord = async (schema, record) => {
const model = schema.findModel(record.modelId)
const fieldParseFails = validateAllFieldParse(record, model)
// non parsing would cause further issues - exit here
if (!isEmpty(fieldParseFails)) {
return { isValid: false, errors: fieldParseFails }
}
const recordValidationRuleFails = runRecordValidationRules(record, recordNode)
const typeContraintFails = await validateAllTypeConstraints(
record,
recordNode,
context
)
const recordValidationRuleFails = runRecordValidationRules(record, model)
const typeContraintFails = await validateAllTypeConstraints(record, model)
if (
isEmpty(fieldParseFails) &&

View file

@ -1,15 +1,23 @@
export const fullSchema = (models, views) => {
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 =>
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 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)

View file

@ -12,7 +12,7 @@ import {
toNumberOrNull,
$$,
isSafeInteger,
} from "../../common"
} from "../../common/index.mjs"
const arrayFunctions = () =>
typeFunctions({

View file

@ -6,7 +6,7 @@ import {
parsedSuccess,
getDefaultExport,
} from "./typeHelpers"
import { switchCase, defaultCase, isOneOf, toBoolOrNull } from "../../common"
import { switchCase, defaultCase, isOneOf, toBoolOrNull } from "../../common/index.mjs"
const boolFunctions = typeFunctions({
default: constant(null),

View file

@ -6,7 +6,7 @@ import {
parsedSuccess,
getDefaultExport,
} from "./typeHelpers"
import { switchCase, defaultCase, toDateOrNull, isNonEmptyArray } from "../../common"
import { switchCase, defaultCase, toDateOrNull } from "../../common"
const dateFunctions = typeFunctions({
default: constant(null),
@ -21,13 +21,9 @@ const parseStringToDate = s =>
[defaultCase, parsedFailed]
)(new Date(s))
const isNullOrEmpty = d =>
isNull(d)
|| (d || "").toString() === ""
const isNullOrEmpty = d => isNull(d) || (d || "").toString() === ""
const isDateOrEmpty = d =>
isDate(d)
|| isNullOrEmpty(d)
const isDateOrEmpty = d => isDate(d) || isNullOrEmpty(d)
const dateTryParse = switchCase(
[isDateOrEmpty, parsedSuccess],

View file

@ -5,7 +5,7 @@ import {
parsedSuccess,
getDefaultExport,
} from "./typeHelpers"
import { switchCase, defaultCase, none, $, splitKey } from "../../common"
import { switchCase, defaultCase, none, $, splitKey } from "../../common/index.mjs"
const illegalCharacters = "*?\\/:<>|\0\b\f\v"

View file

@ -10,7 +10,7 @@ import {
isArray,
has,
} from "lodash/fp"
import { $ } from "../../common"
import { $ } from "../../common/index.mjs"
import { parsedSuccess } from "./typeHelpers"
import string from "./string"
import bool from "./bool"
@ -19,7 +19,7 @@ import datetime from "./datetime"
import array from "./array"
import link from "./link"
import file from "./file"
import { BadRequestError } from "../../common/errors"
import { BadRequestError } from "../../common/errors.mjs"
const allTypes = () => {
const basicTypes = {
@ -67,8 +67,8 @@ export const validateFieldParse = (field, record) =>
export const getDefaultOptions = type => getType(type).getDefaultOptions()
export const validateTypeConstraints = async (field, record, context) =>
await getType(field.type).validateTypeConstraints(field, record, context)
export const validateTypeConstraints = async (field, record) =>
await getType(field.type).validateTypeConstraints(field, record)
export const detectType = value => {
if (isString(value)) return string
@ -76,8 +76,7 @@ export const detectType = value => {
if (isNumber(value)) return number
if (isDate(value)) return datetime
if (isArray(value)) return array(detectType(value[0]))
if (isObject(value) && has("key")(value) && has("value")(value))
return link
if (isObject(value) && has("key")(value) && has("value")(value)) return link
if (isObject(value) && has("relativePath")(value) && has("size")(value))
return file

View file

@ -1,7 +1,6 @@
import { isString, isObjectLike, isNull, has, isEmpty } from "lodash/fp"
import { isString, isObjectLike, isNull, has } from "lodash/fp"
import {
typeFunctions,
makerule,
parsedSuccess,
getDefaultExport,
parsedFailed,
@ -11,7 +10,7 @@ import {
defaultCase,
isNonEmptyString,
isArrayOfString,
} from "../../common"
} from "../../common/index.mjs"
const linkNothing = () => ({ key: "" })
@ -65,20 +64,7 @@ const options = {
},
}
const isEmptyString = s => isString(s) && isEmpty(s)
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
})`
),
]
const typeConstraints = []
export default getDefaultExport(
"link",

View file

@ -11,7 +11,7 @@ import {
defaultCase,
toNumberOrNull,
isSafeInteger,
} from "../../common"
} from "../../common/index.mjs"
const numberFunctions = typeFunctions({
default: constant(null),

View file

@ -5,7 +5,7 @@ import {
parsedSuccess,
getDefaultExport,
} from "./typeHelpers"
import { switchCase, defaultCase, $ } from "../../common"
import { switchCase, defaultCase, $ } from "../../common/index.mjs"
const objectFunctions = (definition, allTypes) =>
typeFunctions({

View file

@ -12,7 +12,7 @@ import {
toNumberOrNull,
isSafeInteger,
isArrayOfString,
} from "../../common"
} from "../../common/index.mjs"
const stringFunctions = typeFunctions({
default: constant(null),

View file

@ -1,6 +1,6 @@
import { merge } from "lodash"
import { constant, isUndefined, has, mapValues, cloneDeep } from "lodash/fp"
import { isNotEmpty } from "../../common"
import { isNotEmpty } from "../../common/index.mjs"
export const getSafeFieldParser = (tryParse, defaultValueFunctions) => (
field,
@ -48,12 +48,11 @@ export const typeFunctions = specificFunctions =>
export const validateTypeConstraints = validationRules => async (
field,
record,
context
record
) => {
const fieldValue = record[field.name]
const validateRule = async r =>
!(await r.isValid(fieldValue, field.typeOptions, context))
!(await r.isValid(fieldValue, field.typeOptions))
? r.getMessage(fieldValue, field.typeOptions)
: ""

View file

@ -2,6 +2,10 @@ import { newModel } from "../src/schema/models.mjs"
import { newView } from "../src/schema/views.mjs"
import { getNewField } from "../src/schema/fields.mjs"
import { fullSchema } from "../src/schema/fullSchema.mjs"
import {
recordValidationRules,
commonRecordValidationRules,
} from "../src/schema/recordValidationRules.mjs"
export function testSchema() {
const addFieldToModel = (model, { type, name }) => {
@ -18,6 +22,10 @@ export function testSchema() {
addFieldToModel(contactModel, { name: "Is Active", type: "bool" })
addFieldToModel(contactModel, { name: "Created", type: "datetime" })
contactModel.validationRules.push(
recordValidationRules(commonRecordValidationRules.fieldNotEmpty)
)
const activeContactsView = newView(contactModel.id)
activeContactsView.name = "Active Contacts"
activeContactsView.map = "if (doc['Is Active']) emit(doc.Name, doc)"

View file

@ -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")
)
}
it("should return errors when mandatory field is empty", () => {
const schema = testSchema()
const record = getNewRecord(schema, "Contact")
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.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
}
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 hierarchyCreator = hierarchyFactory(
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)
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
}
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 hierarchyCreator = hierarchyFactory(
withFields,
withFieldWithMaxLength
)
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)
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 () => {

View file

@ -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,
})
},
destroy: async ctx => {
const databaseId = ctx.params.databaseId;
const database = couchdb.db.use(databaseId)
ctx.body = await database.destroy(ctx.params.recordId);
},
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 {
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};

View file

@ -0,0 +1,9 @@
const { testSchema } = require("../../common/test/testSchema")
describe("record persistence", () => {
it("should save a record", async () => {
})
})

View file

@ -1,7 +0,0 @@
const { testSchema } = require("../../common/test/testSchema")
describe("record persistence", async () => {
it("should ")
})

View file

@ -2983,7 +2983,7 @@ lodash.sortby@^4.7.0:
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
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"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==