Coerse record fields, to be a bit more tolerant of data input
This commit is contained in:
parent
cb5e9f69a0
commit
40e6d4c844
|
@ -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: {
|
||||||
|
|
|
@ -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
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -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",
|
||||||
},
|
},
|
||||||
|
|
|
@ -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", () => {
|
||||||
|
|
Loading…
Reference in New Issue