diff --git a/lib/model-crud.js b/lib/model-crud.js index fa48b13..d3228fb 100644 --- a/lib/model-crud.js +++ b/lib/model-crud.js @@ -9,8 +9,8 @@ module.exports = { * @param {object|array} - documents to insert * @param {array|string|false} - augment schema.insertBL, `false` will remove all blacklisting * @param {boolean} - 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} - skip validation for this field name(s) * @param {boolean} - whether `createdAt` and `updatedAt` are automatically inserted * @param {any} - any mongodb option @@ -198,8 +198,8 @@ module.exports = { * @param {object} - mongodb query object * @param {object|array} - mongodb document update object(s) * @param {boolean} - 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} - skip validation for this field name(s) * @param {boolean} - whether `updatedAt` is automatically updated * @param {array|string|false} - augment schema.updateBL, `false` will remove all blacklisting diff --git a/lib/model-validate.js b/lib/model-validate.js index 38c62d2..c54c69d 100644 --- a/lib/model-validate.js +++ b/lib/model-validate.js @@ -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 @@ -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]) @@ -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 diff --git a/test/validate.js b/test/validate.js index 0f9d09e..facd3f7 100644 --- a/test/validate.js +++ b/test/validate.js @@ -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', @@ -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.', @@ -344,7 +344,6 @@ module.exports = function(monastery, opendb) { }) }) - test('validation getMostSpecificKeyMatchingPath', async () => { let fn = validate._getMostSpecificKeyMatchingPath let mock = { @@ -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 } @@ -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', {