Coerse record fields, to be a bit more tolerant of data input

This commit is contained in:
Michael Shanks 2020-10-05 17:28:23 +01:00
parent cb5e9f69a0
commit 40e6d4c844
4 changed files with 108 additions and 16 deletions

View File

@ -6,7 +6,7 @@ export const FIELDS = {
constraints: { constraints: {
type: "string", type: "string",
length: {}, length: {},
presence: { allowEmpty: true }, presence: false,
}, },
}, },
NUMBER: { NUMBER: {
@ -15,7 +15,7 @@ export const FIELDS = {
type: "number", type: "number",
constraints: { constraints: {
type: "number", type: "number",
presence: { allowEmpty: true }, presence: false,
numericality: { greaterThanOrEqualTo: "", lessThanOrEqualTo: "" }, numericality: { greaterThanOrEqualTo: "", lessThanOrEqualTo: "" },
}, },
}, },
@ -25,7 +25,7 @@ export const FIELDS = {
type: "boolean", type: "boolean",
constraints: { constraints: {
type: "boolean", type: "boolean",
presence: { allowEmpty: true }, presence: false,
}, },
}, },
// OPTIONS: { // OPTIONS: {
@ -44,7 +44,7 @@ export const FIELDS = {
constraints: { constraints: {
type: "string", type: "string",
length: {}, length: {},
presence: { allowEmpty: true }, presence: false,
datetime: { datetime: {
latest: "", latest: "",
earliest: "", earliest: "",
@ -57,7 +57,7 @@ export const FIELDS = {
type: "attachment", type: "attachment",
constraints: { constraints: {
type: "array", type: "array",
presence: { allowEmpty: true }, presence: false,
}, },
}, },
// LINKED_FIELDS: { // LINKED_FIELDS: {

View File

@ -33,7 +33,7 @@ exports.patch = async function(ctx) {
const model = await db.get(record.modelId) const model = await db.get(record.modelId)
const patchfields = ctx.request.body const patchfields = ctx.request.body
coersceRecordValues(record, model) coerceRecordValues(record, model)
for (let key in patchfields) { for (let key in patchfields) {
if (!model.schema[key]) continue if (!model.schema[key]) continue
@ -73,7 +73,7 @@ exports.save = async function(ctx) {
const model = await db.get(record.modelId) const model = await db.get(record.modelId)
coersceRecordValues(record, model) coerceRecordValues(record, model)
const validateResult = await validate({ const validateResult = await validate({
record, record,
@ -223,12 +223,12 @@ async function validate({ instanceId, modelId, record, model }) {
return { valid: Object.keys(errors).length === 0, errors } return { valid: Object.keys(errors).length === 0, errors }
} }
function coersceRecordValues(record, model) { function coerceRecordValues(record, model) {
for (let [key, value] of Object.entries(record)) { for (let [key, value] of Object.entries(record)) {
const field = model.schema[key] const field = model.schema[key]
if (!field) continue if (!field) continue
const mapping = Object.prototype.hasOwnProperty.call( const mapping = Object.prototype.hasOwnProperty.call(
TYPE_TRANSFORM_MAP, TYPE_TRANSFORM_MAP[field.type],
value value
) )
? TYPE_TRANSFORM_MAP[field.type][value] ? TYPE_TRANSFORM_MAP[field.type][value]
@ -243,10 +243,11 @@ const TYPE_TRANSFORM_MAP = {
"": "", "": "",
[null]: "", [null]: "",
[undefined]: undefined, [undefined]: undefined,
parse: s => s.toString(), parse: s => s,
}, },
number: { number: {
"": null, "": null,
[null]: null,
[undefined]: undefined, [undefined]: undefined,
parse: n => parseFloat(n), parse: n => parseFloat(n),
}, },
@ -254,19 +255,21 @@ const TYPE_TRANSFORM_MAP = {
"": null, "": null,
[undefined]: undefined, [undefined]: undefined,
[null]: null, [null]: null,
parse: d => new Date(d).getTime(), parse: d => d,
}, },
attachments: { attachment: {
"": [],
[null]: [], [null]: [],
[undefined]: undefined, [undefined]: undefined,
parse: a => a, parse: a => a,
}, },
boolean: { boolean: {
"": null,
[null]: null, [null]: null,
[undefined]: undefined, [undefined]: undefined,
parse: b => { parse: b => {
if (b === "false") return false
if (b === "true") return true if (b === "true") return true
if (b === "false") return false
return b return b
}, },
}, },

View File

@ -46,13 +46,13 @@ exports.createModel = async (request, appId, instanceId, model) => {
key: "name", key: "name",
schema: { schema: {
name: { name: {
type: "text", type: "string",
constraints: { constraints: {
type: "string", type: "string",
}, },
}, },
description: { description: {
type: "text", type: "string",
constraints: { constraints: {
type: "string", type: "string",
}, },

View File

@ -38,7 +38,7 @@ describe("/records", () => {
const createRecord = async r => const createRecord = async r =>
await request await request
.post(`/api/${model._id}/records`) .post(`/api/${r ? r.modelId : record.modelId}/records`)
.send(r || record) .send(r || record)
.set(defaultHeaders(app._id, instance._id)) .set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
@ -152,6 +152,95 @@ describe("/records", () => {
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(404) .expect(404)
}) })
it("record values are coerced", async () => {
const str = {type:"string", constraints: { type: "string", presence: false }}
const attachment = {type:"attachment", constraints: { type: "array", presence: false }}
const bool = {type:"boolean", constraints: { type: "boolean", presence: false }}
const number = {type:"number", constraints: { type: "number", presence: false }}
const datetime = {type:"datetime", constraints: { type: "string", presence: false, datetime: {earliest:"", latest: ""} }}
model = await createModel(request, app._id, instance._id, {
name: "TestModel2",
type: "model",
key: "name",
schema: {
name: str,
stringUndefined: str,
stringNull: str,
stringString: str,
numberEmptyString: number,
numberNull: number,
numberUndefined: number,
numberString: number,
datetimeEmptyString: datetime,
datetimeNull: datetime,
datetimeUndefined: datetime,
datetimeString: datetime,
datetimeDate: datetime,
boolNull: bool,
boolEmpty: bool,
boolUndefined: bool,
boolString: bool,
boolBool: bool,
attachmentNull : attachment,
attachmentUndefined : attachment,
attachmentEmpty : attachment,
},
})
record = {
name: "Test Record",
stringUndefined: undefined,
stringNull: null,
stringString: "i am a string",
numberEmptyString: "",
numberNull: null,
numberUndefined: undefined,
numberString: "123",
numberNumber: 123,
datetimeEmptyString: "",
datetimeNull: null,
datetimeUndefined: undefined,
datetimeString: "1984-04-20T00:00:00.000Z",
datetimeDate: new Date("1984-04-20"),
boolNull: null,
boolEmpty: "",
boolUndefined: undefined,
boolString: "true",
boolBool: true,
modelId: model._id,
attachmentNull : null,
attachmentUndefined : undefined,
attachmentEmpty : "",
}
const id = (await createRecord(record)).body._id
const saved = (await loadRecord(id)).body
expect(saved.stringUndefined).toBe(undefined)
expect(saved.stringNull).toBe("")
expect(saved.stringString).toBe("i am a string")
expect(saved.numberEmptyString).toBe(null)
expect(saved.numberNull).toBe(null)
expect(saved.numberUndefined).toBe(undefined)
expect(saved.numberString).toBe(123)
expect(saved.numberNumber).toBe(123)
expect(saved.datetimeEmptyString).toBe(null)
expect(saved.datetimeNull).toBe(null)
expect(saved.datetimeUndefined).toBe(undefined)
expect(saved.datetimeString).toBe(new Date(record.datetimeString).toISOString())
expect(saved.datetimeDate).toBe(record.datetimeDate.toISOString())
expect(saved.boolNull).toBe(null)
expect(saved.boolEmpty).toBe(null)
expect(saved.boolUndefined).toBe(undefined)
expect(saved.boolString).toBe(true)
expect(saved.boolBool).toBe(true)
expect(saved.attachmentNull).toEqual([])
expect(saved.attachmentUndefined).toBe(undefined)
expect(saved.attachmentEmpty).toEqual([])
})
}) })
describe("patch", () => { describe("patch", () => {