record controllers...

This commit is contained in:
Michael Shanks 2020-04-09 16:42:55 +01:00 committed by Martin McKeaveney
parent ebc1c44343
commit 84c3e287d5
16 changed files with 146 additions and 306 deletions

View File

@ -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) &&

View File

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

View File

@ -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"
), ),
] ]

View File

@ -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 ${

View File

@ -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"

View File

@ -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

View File

@ -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
) )

View File

@ -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 ${

View File

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

View File

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

View File

@ -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)

View File

@ -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()
})
}) })

View File

@ -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(),

View File

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

View File

@ -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 }

View File

@ -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;