Skip to content

Commit

Permalink
Fixed validateUndefined
Browse files Browse the repository at this point in the history
  • Loading branch information
boycce committed Feb 25, 2022
1 parent bababb5 commit 58daed1
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 22 deletions.
8 changes: 4 additions & 4 deletions lib/model-crud.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ module.exports = {
* @param {object|array} <opts.data> - documents to insert
* @param {array|string|false} <opts.blacklist> - augment schema.insertBL, `false` will remove all blacklisting
* @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
* @param {array|string|false} validateUndefined - ignore all required fields during insert, or
* undefined subdocument required fields that have a defined parent/grandparent during update
* @param {array|string|false} validateUndefined - validates all 'required' undefined fields, true by
* default, but false on update
* @param {array|string|true} <opts.skipValidation> - skip validation for this field name(s)
* @param {boolean} <opts.timestamps> - whether `createdAt` and `updatedAt` are automatically inserted
* @param {any} <opts.any> - any mongodb option
Expand Down Expand Up @@ -198,8 +198,8 @@ module.exports = {
* @param {object} <opts.query> - mongodb query object
* @param {object|array} <opts.data> - mongodb document update object(s)
* @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
* @param {array|string|false} validateUndefined - ignore all required fields during insert, or
* undefined subdocument required fields that have a defined parent/grandparent during update
* @param {array|string|false} validateUndefined - validates all 'required' undefined fields, true by
* default, but false on update
* @param {array|string|true} <opts.skipValidation> - skip validation for this field name(s)
* @param {boolean} <opts.timestamps> - whether `updatedAt` is automatically updated
* @param {array|string|false} <opts.blacklist> - augment schema.updateBL, `false` will remove all blacklisting
Expand Down
17 changes: 9 additions & 8 deletions lib/model-validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ module.exports = {
* @param {boolean(false)} update - are we validating for insert or update?
* @param {array|string|false} blacklist - augment schema blacklist, `false` will remove all blacklisting
* @param {array|string} projection - only return these fields, ignores blacklist
* @param {array|string|false} validateUndefined - ignore all required fields during insert, or undefined
* subdocument required fields that have a defined parent/grandparent during update
* @param {array|string|false} validateUndefined - validates all 'required' undefined fields, true by
* default, but false on update
* @param {array|string|true} skipValidation - skip validation on these fields
* @param {boolean} timestamps - whether `createdAt` and `updatedAt` are inserted, or `updatedAt` is
* updated, depending on the `options.update` value
Expand Down Expand Up @@ -176,11 +176,11 @@ module.exports = {
} else if (util.isSubdocument(field)) {
// Object schema errors
errors.push(...(verrors = this._validateRules(dataRoot, schema, value, opts, path2)))
// Recurse if data value is a subdocument, or when inserting, or when updating deep properties (non-root)
// Recurse if inserting, value is a subdocument, or a deep property (todo: not dot-notation)
if (
util.isObject(value) ||
opts.insert ||
((path2||'').match(/\./) && (util.isDefined(opts.validateUndefined) ? opts.validateUndefined : true))
util.isObject(value) ||
(util.isDefined(opts.validateUndefined) ? opts.validateUndefined : (path2||'').match(/\./))
) {
var res = this._validateFields(dataRoot, field, value, opts, path2)
errors.push(...res[0])
Expand Down Expand Up @@ -260,13 +260,14 @@ module.exports = {
ruleArg = ruleArg === true? undefined : ruleArg
let rule = this.rules[ruleName] || rules[ruleName]
let fieldName = path.match(/[^.]+$/)[0]
let isDeepProp = path.match(/\./) // todo: not dot-notation
let ruleMessageKey = this._getMostSpecificKeyMatchingPath(this.messages, path)
let ruleMessage = ruleMessageKey && this.messages[ruleMessageKey][ruleName]
let validateUndefined = util.isDefined(opts.validateUndefined) ? opts.validateUndefined : rule.validateUndefined
let validateUndefined = util.isDefined(opts.validateUndefined) ? opts.validateUndefined : opts.insert || isDeepProp
if (!ruleMessage) ruleMessage = rule.message

// Ignore undefined (if updated root property, or ignoring)
if (typeof value === 'undefined' && (!validateUndefined || (opts.update && !path.match(/\./)))) return
// Undefined value
if (typeof value === 'undefined' && (!validateUndefined || !rule.validateUndefined)) return

// Ignore null (if nullObject is set on objects or arrays)
if (value === null && (field.isObject || field.isArray) && field.nullObject) return
Expand Down
79 changes: 69 additions & 10 deletions test/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,18 @@ module.exports = function(monastery, opendb) {
meta: { rule: 'required', model: 'user', field: 'name' }
})

// Required error (insert, and with ignoreRequired)
await expect(user.validate({}, { validateUndefined: false })).resolves.toEqual({})
await expect(user.validate({}, { validateUndefined: false, update: true })).resolves.toEqual({})

// No required error (update)
await expect(user.validate({}, { update: true })).resolves.toEqual({})

// Type error (string)
await expect(user.validate({ name: 1 })).resolves.toEqual({ name: '1' })
await expect(user.validate({ name: 1.123 })).resolves.toEqual({ name: '1.123' })
await expect(user.validate({ name: undefined }, { validateUndefined: false })).resolves.toEqual({})
await expect(user.validate({ name: undefined })).rejects.toContainEqual({
status: '400',
title: 'name',
detail: 'This field is required.',
meta: { rule: 'required', model: 'user', field: 'name' }
})
await expect(user.validate({ name: null })).rejects.toContainEqual({
status: '400',
title: 'name',
Expand Down Expand Up @@ -65,20 +66,19 @@ module.exports = function(monastery, opendb) {
await expect(usernum.validate({ amount: 0 })).resolves.toEqual({ amount: 0 })
await expect(usernum.validate({ amount: '0' })).resolves.toEqual({ amount: 0 })
await expect(usernum2.validate({ amount: '' })).resolves.toEqual({ amount: null })
await expect(usernum.validate({ amount: undefined }, { validateUndefined: false })).resolves.toEqual({})
await expect(usernum.validate({ amount: false })).rejects.toEqual([{
status: '400',
title: 'amount',
detail: 'Value was not a number.',
meta: { rule: 'isNumber', model: 'usernum', field: 'amount' }
}])
await expect(usernum.validate({ amount: null })).rejects.toEqual([{
await expect(usernum.validate({ amount: undefined })).rejects.toEqual([{
status: '400',
title: 'amount',
detail: 'This field is required.',
meta: { rule: 'required', model: 'usernum', field: 'amount' },
}])
await expect(usernum.validate({ amount: null }, { validateUndefined: false })).rejects.toEqual([{
await expect(usernum.validate({ amount: null })).rejects.toEqual([{
status: '400',
title: 'amount',
detail: 'This field is required.',
Expand Down Expand Up @@ -344,7 +344,6 @@ module.exports = function(monastery, opendb) {
})
})


test('validation getMostSpecificKeyMatchingPath', async () => {
let fn = validate._getMostSpecificKeyMatchingPath
let mock = {
Expand Down Expand Up @@ -906,7 +905,7 @@ module.exports = function(monastery, opendb) {
db.close()
})

test('validation options', async () => {
test('validation option skipValidation', async () => {
let db = (await opendb(false)).db
let user = db.model('user', { fields: {
name: { type: 'string', required: true }
Expand Down Expand Up @@ -991,6 +990,66 @@ module.exports = function(monastery, opendb) {
})
})

test('validation option validateUndefined', async () => {
// ValidateUndefined runs required rules on all fields, `true` for insert, `false` for update.

// Setup
let db = (await opendb(false)).db
let user = db.model('user', { fields: {
date: { type: 'number' },
name: { type: 'string', required: true },
}})
let usernum = db.model('usernum', { fields: {
amount: { type: 'number', required: true }
}})
let userdeep = db.model('userdeep', { fields: {
date: { type: 'number' },
name: {
first: { type: 'string', required: true },
},
names: [{
first: { type: 'string', required: true },
}]
}})
let errorRequired = {
status: '400',
title: 'name',
detail: 'This field is required.',
meta: expect.any(Object),
}

// Required error for undefined
await expect(user.validate({}))
.rejects.toEqual([errorRequired])
await expect(user.validate({}, { update: true, validateUndefined: true }))
.rejects.toEqual([errorRequired])
await expect(userdeep.validate({}))
.rejects.toEqual([{ ...errorRequired, title: 'name.first' }])
await expect(userdeep.validate({ name: {} }, { update: true }))
.rejects.toEqual([{ ...errorRequired, title: 'name.first' }])
await expect(userdeep.validate({ names: [{}] }, { update: true }))
.rejects.toEqual([{ ...errorRequired, title: 'names.0.first' }])

// Required error for null
await expect(user.validate({ name: null }, { update: true }))
.rejects.toEqual([errorRequired])
await expect(usernum.validate({ amount: null }, { update: true }))
.rejects.toEqual([{ ...errorRequired, title: 'amount' }])
await expect(user.validate({ name: null }, { update: true, validateUndefined: true }))
.rejects.toEqual([errorRequired])

// Skip required error
await expect(user.validate({ name: undefined }, { validateUndefined: false })).resolves.toEqual({})
await expect(user.validate({}, { validateUndefined: false })).resolves.toEqual({})
await expect(user.validate({}, { update: true })).resolves.toEqual({})
await expect(user.validate({}, { update: true, validateUndefined: false })).resolves.toEqual({})
await expect(userdeep.validate({}, { update: true })).resolves.toEqual({})
await expect(userdeep.validate({ name: {} }, { update: true, validateUndefined: false }))
.resolves.toEqual({ name: {} })
await expect(userdeep.validate({ names: [{}] }, { update: true, validateUndefined: false }))
.resolves.toEqual({ names: [{}] })
})

test('validation hooks', async () => {
let db = (await opendb(null)).db
let user = db.model('user', {
Expand Down

0 comments on commit 58daed1

Please sign in to comment.