From ebc1c44343d2e00b34f0ed3708f1ec3667cde188 Mon Sep 17 00:00:00 2001 From: Michael Shanks Date: Thu, 9 Apr 2020 10:13:19 +0100 Subject: [PATCH] adding save & load controllers --- .../common/{apiWrapper.js => apiWrapper.mjs} | 0 .../{compileCode.js => compileCode.mjs} | 0 .../src/common/{errors.js => errors.mjs} | 0 ...eventAggregator.js => eventAggregator.mjs} | 0 .../src/common/{events.js => events.mjs} | 0 .../common/src/common/{index.js => index.mjs} | 4 +- ...lidationCommon.js => validationCommon.mjs} | 0 .../src/records/recordValidationRules.mjs | 32 +++++ .../{validate.js => validateRecord.mjs} | 43 +++--- packages/common/src/schema/fullSchema.mjs | 16 ++- packages/common/src/schema/types/array.mjs | 2 +- packages/common/src/schema/types/bool.mjs | 2 +- packages/common/src/schema/types/datetime.mjs | 10 +- packages/common/src/schema/types/file.mjs | 2 +- packages/common/src/schema/types/index.mjs | 11 +- packages/common/src/schema/types/link.mjs | 20 +-- packages/common/src/schema/types/number.mjs | 2 +- packages/common/src/schema/types/object.mjs | 2 +- packages/common/src/schema/types/string.mjs | 2 +- .../common/src/schema/types/typeHelpers.mjs | 7 +- packages/common/test/testSchema.mjs | 8 ++ ...alidate.spec.js => validateRecord.spec.js} | 100 +++++--------- .../server/middleware/controllers/record.js | 126 +++++++++++++++--- .../middleware/routes/tests/record.spec.js | 9 ++ packages/server/tests/record.spec.js | 7 - packages/server/yarn.lock | 2 +- 26 files changed, 243 insertions(+), 164 deletions(-) rename packages/common/src/common/{apiWrapper.js => apiWrapper.mjs} (100%) rename packages/common/src/common/{compileCode.js => compileCode.mjs} (100%) rename packages/common/src/common/{errors.js => errors.mjs} (100%) rename packages/common/src/common/{eventAggregator.js => eventAggregator.mjs} (100%) rename packages/common/src/common/{events.js => events.mjs} (100%) rename packages/common/src/common/{index.js => index.mjs} (98%) rename packages/common/src/common/{validationCommon.js => validationCommon.mjs} (100%) create mode 100644 packages/common/src/records/recordValidationRules.mjs rename packages/common/src/records/{validate.js => validateRecord.mjs} (59%) rename packages/common/test/{recordApi.validate.spec.js => validateRecord.spec.js} (74%) create mode 100644 packages/server/middleware/routes/tests/record.spec.js delete mode 100644 packages/server/tests/record.spec.js diff --git a/packages/common/src/common/apiWrapper.js b/packages/common/src/common/apiWrapper.mjs similarity index 100% rename from packages/common/src/common/apiWrapper.js rename to packages/common/src/common/apiWrapper.mjs diff --git a/packages/common/src/common/compileCode.js b/packages/common/src/common/compileCode.mjs similarity index 100% rename from packages/common/src/common/compileCode.js rename to packages/common/src/common/compileCode.mjs diff --git a/packages/common/src/common/errors.js b/packages/common/src/common/errors.mjs similarity index 100% rename from packages/common/src/common/errors.js rename to packages/common/src/common/errors.mjs diff --git a/packages/common/src/common/eventAggregator.js b/packages/common/src/common/eventAggregator.mjs similarity index 100% rename from packages/common/src/common/eventAggregator.js rename to packages/common/src/common/eventAggregator.mjs diff --git a/packages/common/src/common/events.js b/packages/common/src/common/events.mjs similarity index 100% rename from packages/common/src/common/events.js rename to packages/common/src/common/events.mjs diff --git a/packages/common/src/common/index.js b/packages/common/src/common/index.mjs similarity index 98% rename from packages/common/src/common/index.js rename to packages/common/src/common/index.mjs index 7cead2204a..1b7750f9ae 100644 --- a/packages/common/src/common/index.js +++ b/packages/common/src/common/index.mjs @@ -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, diff --git a/packages/common/src/common/validationCommon.js b/packages/common/src/common/validationCommon.mjs similarity index 100% rename from packages/common/src/common/validationCommon.js rename to packages/common/src/common/validationCommon.mjs diff --git a/packages/common/src/records/recordValidationRules.mjs b/packages/common/src/records/recordValidationRules.mjs new file mode 100644 index 0000000000..5cf40260cc --- /dev/null +++ b/packages/common/src/records/recordValidationRules.mjs @@ -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} ` + ), +} diff --git a/packages/common/src/records/validate.js b/packages/common/src/records/validateRecord.mjs similarity index 59% rename from packages/common/src/records/validate.js rename to packages/common/src/records/validateRecord.mjs index 052a92680e..2419f35300 100644 --- a/packages/common/src/records/validate.js +++ b/packages/common/src/records/validateRecord.mjs @@ -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) && diff --git a/packages/common/src/schema/fullSchema.mjs b/packages/common/src/schema/fullSchema.mjs index 68b89a5bd5..85a9f63106 100644 --- a/packages/common/src/schema/fullSchema.mjs +++ b/packages/common/src/schema/fullSchema.mjs @@ -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) diff --git a/packages/common/src/schema/types/array.mjs b/packages/common/src/schema/types/array.mjs index 9ec5381c2e..33098e6116 100644 --- a/packages/common/src/schema/types/array.mjs +++ b/packages/common/src/schema/types/array.mjs @@ -12,7 +12,7 @@ import { toNumberOrNull, $$, isSafeInteger, -} from "../../common" +} from "../../common/index.mjs" const arrayFunctions = () => typeFunctions({ diff --git a/packages/common/src/schema/types/bool.mjs b/packages/common/src/schema/types/bool.mjs index c49c4402eb..a9d1ea99eb 100644 --- a/packages/common/src/schema/types/bool.mjs +++ b/packages/common/src/schema/types/bool.mjs @@ -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), diff --git a/packages/common/src/schema/types/datetime.mjs b/packages/common/src/schema/types/datetime.mjs index 675d92d7a1..0521b1953f 100644 --- a/packages/common/src/schema/types/datetime.mjs +++ b/packages/common/src/schema/types/datetime.mjs @@ -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], diff --git a/packages/common/src/schema/types/file.mjs b/packages/common/src/schema/types/file.mjs index 50a9f9883c..88477d9d7b 100644 --- a/packages/common/src/schema/types/file.mjs +++ b/packages/common/src/schema/types/file.mjs @@ -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" diff --git a/packages/common/src/schema/types/index.mjs b/packages/common/src/schema/types/index.mjs index 8072b45abe..4c6dd9ff97 100644 --- a/packages/common/src/schema/types/index.mjs +++ b/packages/common/src/schema/types/index.mjs @@ -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 diff --git a/packages/common/src/schema/types/link.mjs b/packages/common/src/schema/types/link.mjs index 503f8f0af2..1d2fcbd3b0 100644 --- a/packages/common/src/schema/types/link.mjs +++ b/packages/common/src/schema/types/link.mjs @@ -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", diff --git a/packages/common/src/schema/types/number.mjs b/packages/common/src/schema/types/number.mjs index e4a4a9609c..a3859dae76 100644 --- a/packages/common/src/schema/types/number.mjs +++ b/packages/common/src/schema/types/number.mjs @@ -11,7 +11,7 @@ import { defaultCase, toNumberOrNull, isSafeInteger, -} from "../../common" +} from "../../common/index.mjs" const numberFunctions = typeFunctions({ default: constant(null), diff --git a/packages/common/src/schema/types/object.mjs b/packages/common/src/schema/types/object.mjs index 7c23d8b185..d5ea57638a 100644 --- a/packages/common/src/schema/types/object.mjs +++ b/packages/common/src/schema/types/object.mjs @@ -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({ diff --git a/packages/common/src/schema/types/string.mjs b/packages/common/src/schema/types/string.mjs index 1f9acd5a32..38847ee4d0 100644 --- a/packages/common/src/schema/types/string.mjs +++ b/packages/common/src/schema/types/string.mjs @@ -12,7 +12,7 @@ import { toNumberOrNull, isSafeInteger, isArrayOfString, -} from "../../common" +} from "../../common/index.mjs" const stringFunctions = typeFunctions({ default: constant(null), diff --git a/packages/common/src/schema/types/typeHelpers.mjs b/packages/common/src/schema/types/typeHelpers.mjs index 77a8a0507d..129864dcbf 100644 --- a/packages/common/src/schema/types/typeHelpers.mjs +++ b/packages/common/src/schema/types/typeHelpers.mjs @@ -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) : "" diff --git a/packages/common/test/testSchema.mjs b/packages/common/test/testSchema.mjs index 85048cd856..acde5037d8 100644 --- a/packages/common/test/testSchema.mjs +++ b/packages/common/test/testSchema.mjs @@ -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)" diff --git a/packages/common/test/recordApi.validate.spec.js b/packages/common/test/validateRecord.spec.js similarity index 74% rename from packages/common/test/recordApi.validate.spec.js rename to packages/common/test/validateRecord.spec.js index 93476ed2b7..6fb9f11635 100644 --- a/packages/common/test/recordApi.validate.spec.js +++ b/packages/common/test/validateRecord.spec.js @@ -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 () => { diff --git a/packages/server/middleware/controllers/record.js b/packages/server/middleware/controllers/record.js index 2b70409596..b8938bc18d 100644 --- a/packages/server/middleware/controllers/record.js +++ b/packages/server/middleware/controllers/record.js @@ -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; \ No newline at end of file +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}; \ No newline at end of file diff --git a/packages/server/middleware/routes/tests/record.spec.js b/packages/server/middleware/routes/tests/record.spec.js new file mode 100644 index 0000000000..231794a00a --- /dev/null +++ b/packages/server/middleware/routes/tests/record.spec.js @@ -0,0 +1,9 @@ +const { testSchema } = require("../../common/test/testSchema") + +describe("record persistence", () => { + it("should save a record", async () => { + + }) +}) + + diff --git a/packages/server/tests/record.spec.js b/packages/server/tests/record.spec.js deleted file mode 100644 index ec7e27255a..0000000000 --- a/packages/server/tests/record.spec.js +++ /dev/null @@ -1,7 +0,0 @@ -const { testSchema } = require("../../common/test/testSchema") - -describe("record persistence", async () => { - it("should ") -}) - - diff --git a/packages/server/yarn.lock b/packages/server/yarn.lock index d2050153b2..0c6c5e0a20 100644 --- a/packages/server/yarn.lock +++ b/packages/server/yarn.lock @@ -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==