Skip to content

Commit

Permalink
refactor validate only
Browse files Browse the repository at this point in the history
  • Loading branch information
boycce committed Aug 9, 2024
1 parent 9dfe7f3 commit 53c266c
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 91 deletions.
199 changes: 109 additions & 90 deletions lib/model-validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Model.prototype.validate = async function (data, opts) {
opts.update = opts.update || opts.findOneAndUpdate
opts.insert = !opts.update
opts.skipValidation = opts.skipValidation === true ? true : util.toArray(opts.skipValidation||[])
opts.timestamps = util.isDefined(opts.timestamps) ? opts.timestamps : this.manager.opts.timestamps
if (opts.skipValidation === true) return data

// Get projection
Expand All @@ -40,9 +41,9 @@ Model.prototype.validate = async function (data, opts) {
// Recurse and validate fields
// console.time('_validateFields')
let response = util.toArray(data).map(item => {
let validated = this._validateFields(item, this.fields, item, opts, '', '')
if (validated[0].length) throw validated[0]
else return validated[1]
const [errors, validated] = this._validateFields(item, this.fields, item, opts, '', '')
if (errors.length) throw errors // todo: maybe add trace to this object?
else return validated
})
// console.timeEnd('_validateFields')

Expand Down Expand Up @@ -76,7 +77,7 @@ Model.prototype._validateFields = function (dataRoot, fields, data, opts, parent
* Note: This is now super fast, it can validate 100k possible fields in 235ms
*
* @param {any} dataRoot
* @param {object|array} fields
* @param {object|array} fields (from definition)
* @param {any} data
* @param {object} opts
* @param {string} parentPath - data localised parent, e.g. pets.1.name
Expand All @@ -90,103 +91,36 @@ Model.prototype._validateFields = function (dataRoot, fields, data, opts, parent
let errors = []
let fieldsIsArray = util.isArray(fields)
let fieldsArray = fieldsIsArray ? fields : Object.keys(fields)
let timestamps = util.isDefined(opts.timestamps) ? opts.timestamps : this.manager.opts.timestamps
let dataArray = util.forceArray(data)
let data2 = fieldsIsArray ? [] : {}
let notStrict = fields.schema.strict === false

for (let i=0, l=dataArray.length; i<l; i++) {
const item = dataArray[i]
const dataItem = dataArray[i]
const dataKeys = Object.keys(dataItem || {}) // may be false when inserting, e.g. mode.insert({ data: false })

for (let m=0, n=fieldsArray.length; m<n; m++) {
// iterations++
const fieldName = fieldsIsArray ? m : fieldsArray[m] // array|object key
const field = fields[fieldName]
if (fieldName == 'schema') continue
// if (!parentPath && fieldName == 'categories') console.time(fieldName)
// if (!parentPath && fieldName == 'categories') console.time(fieldName + 1)
let schema = field.schema
let value = fieldsIsArray ? item : (item||{})[fieldName]
let indexOrFieldName = fieldsIsArray ? i : fieldName
let path = `${parentPath}.${indexOrFieldName}`
let path2 = fieldsIsArray ? parentPath2 : `${parentPath2}.${fieldName}`
if (path[0] == '.') path = path.slice(1) // remove leading dot, e.g. .pets.1.name
if (path2[0] == '.') path2 = path2.slice(1) // remove leading dot, e.g. .pets.1.name
let isTypeRule = this.rules[schema.isType] || rules[schema.isType]

// Timestamp overrides
if (schema.timestampField) {
if (timestamps && ((fieldName == 'createdAt' && opts.insert) || fieldName == 'updatedAt')) {
value = schema.default.call(dataRoot, fieldName, this)
}
// Use the default if available
} else if (util.isDefined(schema.default)) {
if ((!util.isDefined(value) && opts.insert) || schema.defaultOverride) {
value = util.isFunction(schema.default)? schema.default.call(dataRoot, fieldName, this) : schema.default
}
// Add any non-schema properties, excluding array properties
if (notStrict && !fieldsIsArray) {
for (let m=0, n=dataKeys.length; m<n; m++) {
if (!fieldsArray.includes(dataKeys[m])) data2[dataKeys[m]] = dataItem[dataKeys[m]]
}
}

// Ignore insert only
if (opts.update && schema.insertOnly) continue
// Ignore virtual fields
if (schema.virtual) continue
// Ignore blacklisted
if (!schema.defaultOverride && this._pathBlacklisted(path2, opts.projectionInclusion, opts.projectionKeys)) continue
// Type cast the value if tryParse is available, .e.g. isInteger.tryParse
if (isTypeRule && typeof isTypeRule.tryParse == 'function') {
value = isTypeRule.tryParse.call(dataRoot, value, fieldName, this) // 80ms // DISABLE
}

// Field is a subdocument
if (schema.isObject) {
// Object schema errors
let res
const verrors = this._validateRules(dataRoot, schema, value, opts, path)
if (verrors.length) errors.push(...verrors)
// Recurse if inserting, value is a subdocument, or a deep property (todo: not dot-notation)
if (
opts.insert ||
util.isObject(value) ||
(util.isDefined(opts.validateUndefined) ? opts.validateUndefined : (path||'').indexOf('.') !== -1)
) {
res = this._validateFields(dataRoot, field, value, opts, path, path2)
if (res[0].length) errors.push(...res[0])
}
if (util.isDefined(value) && !verrors.length) {
data2[indexOrFieldName] = res ? res[1] : value
}

// Field is an array
} else if (schema.isArray) {
// Array schema errors
let res2
const verrors = this._validateRules(dataRoot, schema, value, opts, path)
if (verrors.length) errors.push(...verrors)
// Data value is array too
if (util.isArray(value)) {
res2 = this._validateFields(dataRoot, field, value, opts, path, path2)
if (res2[0].length) errors.push(...res2[0])
}
if (util.isDefined(value) && !verrors.length) {
data2[indexOrFieldName] = res2? res2[1] : value
}
// Loop through each schema field
for (let m=0, n=fieldsArray.length; m<n; m++) {
const fieldName = fieldsIsArray ? m : fieldsArray[m] // array|object key
const dataFieldName = fieldsIsArray ? i : fieldName
const value = fieldsIsArray ? dataItem : (dataItem||{})[fieldName]
const field = fields[fieldName] // schema field

// Field is a field-type/field-schema
} else {
const verrors = this._validateRules(dataRoot, schema, value, opts, path)
if (verrors.length) errors.push(...verrors)
if (util.isDefined(value) && !verrors.length) data2[indexOrFieldName] = value
}
// if (!parentPath && fieldName == 'categories') console.timeEnd(fieldName)
}
// Field paths
const path = `${parentPath ? parentPath + '.' : ''}${dataFieldName}` // e.g. pets.1.name
const path2 = fieldsIsArray ? parentPath2 : (`${parentPath2 ? parentPath2 + '.' : ''}${fieldName}`) // e.g. pets.name

// Add any extra fields that are not in the schema. Item maybe false when inserting (from recursing above)
if (notStrict && !fieldsIsArray && item) {
const allDataKeys = Object.keys(item)
for (let m=0, n=allDataKeys.length; m<n; m++) {
const key = allDataKeys[m]
if (!fieldsArray.includes(key)) data2[key] = item[key]
}
const [errors2, value2] = this._validateField(dataRoot, field, fieldName, value, opts, path, path2)
if (errors2.length) errors = errors.concat(errors2)
else if (typeof value2 !== 'undefined') data2[dataFieldName] = value2
}
}

Expand All @@ -195,6 +129,91 @@ Model.prototype._validateFields = function (dataRoot, fields, data, opts, parent
return [errors, data2]
}

Model.prototype._validateField = function (dataRoot, field, fieldName, value, opts, path, path2) {
/**
* Validate a field
*
* @param {object} dataRoot - data
* @param {object} field - field (from definition)
* @param {string} fieldName
* @param {any} value
* @param {object} opts - original validate() options
* @param {string} path - full field path, e.g. pets.1.name
* @param {string} path2 - full field path without numerical keys, e.g. pets.name
* @return [errors[], valid-value]
* @this model
*/
// iterations++
const schema = field.schema
if (fieldName == 'schema') return [[]]
// if (!parentPath && fieldName == 'categories') console.time(fieldName)
// if (!parentPath && fieldName == 'categories') console.time(fieldName + 1)

const isTypeRule = this.rules[schema.isType] || rules[schema.isType]

// Timestamp overrides
if (schema.timestampField) {
if (opts.timestamps && ((fieldName == 'createdAt' && opts.insert) || fieldName == 'updatedAt')) {
value = schema.default.call(dataRoot, fieldName, this)
}
// Use the default if available
} else if (util.isDefined(schema.default)) {
if ((!util.isDefined(value) && opts.insert) || schema.defaultOverride) {
value = util.isFunction(schema.default)? schema.default.call(dataRoot, fieldName, this) : schema.default
}
}

// Ignore insert only
if (opts.update && schema.insertOnly) return [[]]
// Ignore virtual fields
if (schema.virtual) return [[]]
// Ignore blacklisted
if (!schema.defaultOverride && this._pathBlacklisted(path2, opts.projectionInclusion, opts.projectionKeys)) return [[]]
// Type cast the value if tryParse is available, .e.g. isInteger.tryParse
if (isTypeRule && typeof isTypeRule.tryParse == 'function') {
value = isTypeRule.tryParse.call(dataRoot, value, fieldName, this) // 80ms // DISABLE
}

// Field is a subdocument
if (schema.isObject) {
// Object schema errors
let verrors2, value2
let verrors = this._validateRules(dataRoot, schema, value, opts, path)
// Recurse if inserting, value is a subdocument, or we're within a subdocument (todo: not dot-notation)
const parentIsSubdocument = (path||'').indexOf('.') !== -1
if (
opts.insert ||
util.isObject(value) ||
(util.isDefined(opts.validateUndefined) ? opts.validateUndefined : parentIsSubdocument)
) {
[verrors2, value2] = this._validateFields(dataRoot, field, value, opts, path, path2)
if (verrors2.length) verrors = verrors.concat(verrors2)
}
if (verrors.length) return [verrors]
else return [[], typeof value2 !== 'undefined' && typeof value !== 'undefined' ? value2 : value]

// Field is an array
} else if (schema.isArray) {
// Array schema errors
let verrors2, value2
let verrors = this._validateRules(dataRoot, schema, value, opts, path)
// Data value is array too
if (util.isArray(value)) {
[verrors2, value2] = this._validateFields(dataRoot, field, value, opts, path, path2)
if (verrors2.length) verrors = verrors.concat(verrors2)
}
if (verrors.length) return [verrors]
else return [[], typeof value2 !== 'undefined' && typeof value !== 'undefined' ? value2 : value]

// Field is a field-type/field-schema
} else {
const verrors = this._validateRules(dataRoot, schema, value, opts, path)
if (verrors.length) return [verrors]
else return [[], value]
}
// if (!parentPath && fieldName == 'categories') console.timeEnd(fieldName)
}

Model.prototype._validateRules = function (dataRoot, fieldSchema, value, opts, path) {
/**
* Validate all the field's rules
Expand Down
2 changes: 1 addition & 1 deletion test/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ test('validation subdocument errors', async () => {
])
)

// Insert: Ignore required subdocument property with a defined parent
// Insert: Required subdocument property is ignored with a parent/grandparent specificed
await expect(user.validate({ animals: {} }, { validateUndefined: false })).resolves.toEqual({
animals: {},
})
Expand Down

0 comments on commit 53c266c

Please sign in to comment.