import utils from './utils'
import Component from './Component'
const DOMAIN = 'Schema'
/**
* A function map for each of the seven primitive JSON types defined by the core specification.
* Each function will check a given value and return true or false if the value is an instance of that type.
* ```
* types.integer(1) // returns true
* types.string({}) // returns false
* ```
* http://json-schema.org/latest/json-schema-core.html#anchor8
* @name Schema.types
* @type {object}
*/
const types = {
array: utils.isArray,
boolean: utils.isBoolean,
integer: utils.isInteger,
'null': utils.isNull,
number: utils.isNumber,
object: utils.isObject,
string: utils.isString
}
/**
* @ignore
*/
const segmentToString = function (segment, prev) {
let str = ''
if (segment) {
if (utils.isNumber(segment)) {
str += `[${segment}]`
} else if (prev) {
str += `.${segment}`
} else {
str += `${segment}`
}
}
return str
}
/**
* @ignore
*/
const makePath = function (opts) {
opts || (opts = {})
let path = ''
const segments = opts.path || []
segments.forEach(function (segment) {
path += segmentToString(segment, path)
})
path += segmentToString(opts.prop, path)
return path
}
/**
* @ignore
*/
const makeError = function (actual, expected, opts) {
return {
expected,
actual: '' + actual,
path: makePath(opts)
}
}
/**
* @ignore
*/
const addError = function (actual, expected, opts, errors) {
errors.push(makeError(actual, expected, opts))
}
/**
* @ignore
*/
const maxLengthCommon = function (keyword, value, schema, opts) {
const max = schema[keyword]
if (value.length > max) {
return makeError(value.length, `length no more than ${max}`, opts)
}
}
/**
* @ignore
*/
const minLengthCommon = function (keyword, value, schema, opts) {
const min = schema[keyword]
if (value.length < min) {
return makeError(value.length, `length no less than ${min}`, opts)
}
}
/**
* A map of all object member validation functions for each keyword defined in the JSON Schema.
* @name Schema.validationKeywords
* @type {object}
*/
const validationKeywords = {
/**
* Validates the provided value against all schemas defined in the Schemas `allOf` keyword.
* The instance is valid against if and only if it is valid against all the schemas declared in the Schema's value.
*
* The value of this keyword MUST be an array. This array MUST have at least one element.
* Each element of this array MUST be a valid JSON Schema.
*
* see http://json-schema.org/latest/json-schema-validation.html#anchor82
*
* @name Schema.validationKeywords.allOf
* @method
* @param {*} value Value to be validated.
* @param {object} schema Schema containing the `allOf` keyword.
* @param {object} [opts] Configuration options.
* @returns {(array|undefined)} Array of errors or `undefined` if valid.
*/
allOf (value, schema, opts) {
let allErrors = []
schema.allOf.forEach(function (_schema) {
allErrors = allErrors.concat(validate(value, _schema, opts) || [])
})
return allErrors.length ? allErrors : undefined
},
/**
* Validates the provided value against all schemas defined in the Schemas `anyOf` keyword.
* The instance is valid against this keyword if and only if it is valid against
* at least one of the schemas in this keyword's value.
*
* The value of this keyword MUST be an array. This array MUST have at least one element.
* Each element of this array MUST be an object, and each object MUST be a valid JSON Schema.
* see http://json-schema.org/latest/json-schema-validation.html#anchor85
*
* @name Schema.validationKeywords.anyOf
* @method
* @param {*} value Value to be validated.
* @param {object} schema Schema containing the `anyOf` keyword.
* @param {object} [opts] Configuration options.
* @returns {(array|undefined)} Array of errors or `undefined` if valid.
*/
anyOf (value, schema, opts) {
let validated = false
let allErrors = []
schema.anyOf.forEach(function (_schema) {
const errors = validate(value, _schema, opts)
if (errors) {
allErrors = allErrors.concat(errors)
} else {
validated = true
}
})
return validated ? undefined : allErrors
},
/**
* http://json-schema.org/latest/json-schema-validation.html#anchor70
*
* @name Schema.validationKeywords.dependencies
* @method
* @param {*} value TODO
* @param {object} schema TODO
* @param {object} opts TODO
*/
dependencies (value, schema, opts) {
// TODO
},
/**
* Validates the provided value against an array of possible values defined by the Schema's `enum` keyword
* Validation succeeds if the value is deeply equal to one of the values in the array.
* see http://json-schema.org/latest/json-schema-validation.html#anchor76
*
* @name Schema.validationKeywords.enum
* @method
* @param {*} value Value to validate
* @param {object} schema Schema containing the `enum` keyword.
* @param {object} [opts] Configuration options.
* @returns {(array|undefined)} Array of errors or `undefined` if valid.
*/
enum (value, schema, opts) {
const possibleValues = schema['enum']
if (utils.findIndex(possibleValues, (item) => utils.deepEqual(item, value)) === -1) {
return makeError(value, `one of (${possibleValues.join(', ')})`, opts)
}
},
/**
* Validates each of the provided array values against a schema or an array of schemas defined by the Schema's `items` keyword
* see http://json-schema.org/latest/json-schema-validation.html#anchor37 for validation rules.
*
* @name Schema.validationKeywords.items
* @method
* @param {*} value Array to be validated.
* @param {object} schema Schema containing the items keyword.
* @param {object} [opts] Configuration options.
* @returns {(array|undefined)} Array of errors or `undefined` if valid.
*/
items (value, schema, opts) {
opts || (opts = {})
// TODO: additionalItems
let items = schema.items
let errors = []
const checkingTuple = utils.isArray(items)
const length = value.length
for (var prop = 0; prop < length; prop++) {
if (checkingTuple) {
// Validating a tuple, instead of just checking each item against the
// same schema
items = schema.items[prop]
}
opts.prop = prop
errors = errors.concat(validate(value[prop], items, opts) || [])
}
return errors.length ? errors : undefined
},
/**
* Validates the provided number against a maximum value defined by the Schema's `maximum` keyword
* Validation succeeds if the value is a number, and is less than, or equal to, the value of this keyword.
* http://json-schema.org/latest/json-schema-validation.html#anchor17
*
* @name Schema.validationKeywords.maximum
* @method
* @param {*} value Number to validate against the keyword.
* @param {object} schema Schema containing the `maximum` keyword.
* @param {object} [opts] Configuration options.
* @returns {(array|undefined)} Array of errors or `undefined` if valid.
*/
maximum (value, schema, opts) {
// Must be a number
const maximum = schema.maximum
// Must be a boolean
// Depends on maximum
// default: false
const exclusiveMaximum = schema.exclusiveMaximum
if (typeof value === typeof maximum && !(exclusiveMaximum ? maximum > value : maximum >= value)) {
return exclusiveMaximum
? makeError(value, `no more than nor equal to ${maximum}`, opts)
: makeError(value, `no more than ${maximum}`, opts)
}
},
/**
* Validates the length of the provided array against a maximum value defined by the Schema's `maxItems` keyword.
* Validation succeeds if the length of the array is less than, or equal to the value of this keyword.
* see http://json-schema.org/latest/json-schema-validation.html#anchor42
*
* @name Schema.validationKeywords.maxItems
* @method
* @param {*} value Array to be validated.
* @param {object} schema Schema containing the `maxItems` keyword.
* @param {object} [opts] Configuration options.
* @returns {(array|undefined)} Array of errors or `undefined` if valid.
*/
maxItems (value, schema, opts) {
if (utils.isArray(value)) {
return maxLengthCommon('maxItems', value, schema, opts)
}
},
/**
* Validates the length of the provided string against a maximum value defined in the Schema's `maxLength` keyword.
* Validation succeeds if the length of the string is less than, or equal to the value of this keyword.
* see http://json-schema.org/latest/json-schema-validation.html#anchor26
*
* @name Schema.validationKeywords.maxLength
* @method
* @param {*} value String to be validated.
* @param {object} schema Schema containing the `maxLength` keyword.
* @param {object} [opts] Configuration options.
* @returns {(array|undefined)} Array of errors or `undefined` if valid.
*/
maxLength (value, schema, opts) {
return maxLengthCommon('maxLength', value, schema, opts)
},
/**
* Validates the count of the provided object's properties against a maximum value defined in the Schema's `maxProperties` keyword.
* Validation succeeds if the object's property count is less than, or equal to the value of this keyword.
* see http://json-schema.org/latest/json-schema-validation.html#anchor54
*
* @name Schema.validationKeywords.maxProperties
* @method
* @param {*} value Object to be validated.
* @param {object} schema Schema containing the `maxProperties` keyword.
* @param {object} [opts] Configuration options.
* @returns {(array|undefined)} Array of errors or `undefined` if valid.
*/
maxProperties (value, schema, opts) {
// validate only objects
if (!utils.isObject(value)) return
const maxProperties = schema.maxProperties
const length = Object.keys(value).length
if (length > maxProperties) {
return makeError(length, `no more than ${maxProperties} properties`, opts)
}
},
/**
* Validates the provided value against a minimum value defined by the Schema's `minimum` keyword
* Validation succeeds if the value is a number and is greater than, or equal to, the value of this keyword.
* http://json-schema.org/latest/json-schema-validation.html#anchor21
*
* @name Schema.validationKeywords.minimum
* @method
* @param {*} value Number to validate against the keyword.
* @param {object} schema Schema containing the `minimum` keyword.
* @param {object} [opts] Configuration options.
* @returns {(array|undefined)} Array of errors or `undefined` if valid.
*/
minimum (value, schema, opts) {
// Must be a number
const minimum = schema.minimum
// Must be a boolean
// Depends on minimum
// default: false
const exclusiveMinimum = schema.exclusiveMinimum
if (typeof value === typeof minimum && !(exclusiveMinimum ? value > minimum : value >= minimum)) {
return exclusiveMinimum
? makeError(value, `no less than nor equal to ${minimum}`, opts)
: makeError(value, `no less than ${minimum}`, opts)
}
},
/**
* Validates the length of the provided array against a minimum value defined by the Schema's `minItems` keyword.
* Validation succeeds if the length of the array is greater than, or equal to the value of this keyword.
* see http://json-schema.org/latest/json-schema-validation.html#anchor45
*
* @name Schema.validationKeywords.minItems
* @method
* @param {*} value Array to be validated.
* @param {object} schema Schema containing the `minItems` keyword.
* @param {object} [opts] Configuration options.
* @returns {(array|undefined)} Array of errors or `undefined` if valid.
*/
minItems (value, schema, opts) {
if (utils.isArray(value)) {
return minLengthCommon('minItems', value, schema, opts)
}
},
/**
* Validates the length of the provided string against a minimum value defined in the Schema's `minLength` keyword.
* Validation succeeds if the length of the string is greater than, or equal to the value of this keyword.
* see http://json-schema.org/latest/json-schema-validation.html#anchor29
*
* @name Schema.validationKeywords.minLength
* @method
* @param {*} value String to be validated.
* @param {object} schema Schema containing the `minLength` keyword.
* @param {object} [opts] Configuration options.
* @returns {(array|undefined)} Array of errors or `undefined` if valid.
*/
minLength (value, schema, opts) {
return minLengthCommon('minLength', value, schema, opts)
},
/**
* Validates the count of the provided object's properties against a minimum value defined in the Schema's `minProperties` keyword.
* Validation succeeds if the object's property count is greater than, or equal to the value of this keyword.
* see http://json-schema.org/latest/json-schema-validation.html#anchor57
*
* @name Schema.validationKeywords.minProperties
* @method
* @param {*} value Object to be validated.
* @param {object} schema Schema containing the `minProperties` keyword.
* @param {object} [opts] Configuration options.
* @returns {(array|undefined)} Array of errors or `undefined` if valid.
*/
minProperties (value, schema, opts) {
// validate only objects
if (!utils.isObject(value)) return
const minProperties = schema.minProperties
const length = Object.keys(value).length
if (length < minProperties) {
return makeError(length, `no more than ${minProperties} properties`, opts)
}
},
/**
* Validates the provided number is a multiple of the number defined in the Schema's `multipleOf` keyword.
* Validation succeeds if the number can be divided equally into the value of this keyword.
* see http://json-schema.org/latest/json-schema-validation.html#anchor14
*
* @name Schema.validationKeywords.multipleOf
* @method
* @param {*} value Number to be validated.
* @param {object} schema Schema containing the `multipleOf` keyword.
* @param {object} [opts] Configuration options.
* @returns {(array|undefined)} Array of errors or `undefined` if valid.
*/
multipleOf (value, schema, opts) {
const multipleOf = schema.multipleOf
if (utils.isNumber(value)) {
if ((value / multipleOf) % 1 !== 0) {
return makeError(value, `multipleOf ${multipleOf}`, opts)
}
}
},
/**
* Validates the provided value is not valid with any of the schemas defined in the Schema's `not` keyword.
* An instance is valid against this keyword if and only if it is NOT valid against the schemas in this keyword's value.
*
* see http://json-schema.org/latest/json-schema-validation.html#anchor91
* @name Schema.validationKeywords.not
* @method
* @param {*} value to be checked.
* @param {object} schema Schema containing the not keyword.
* @param {object} [opts] Configuration options.
* @returns {(array|undefined)} Array of errors or `undefined` if valid.
*/
not (value, schema, opts) {
if (!validate(value, schema.not, opts)) {
// TODO: better messaging
return makeError('succeeded', 'should have failed', opts)
}
},
/**
* Validates the provided value is valid with one and only one of the schemas defined in the Schema's `oneOf` keyword.
* An instance is valid against this keyword if and only if it is valid against a single schemas in this keyword's value.
*
* see http://json-schema.org/latest/json-schema-validation.html#anchor88
* @name Schema.validationKeywords.oneOf
* @method
* @param {*} value to be checked.
* @param {object} schema Schema containing the `oneOf` keyword.
* @param {object} [opts] Configuration options.
* @returns {(array|undefined)} Array of errors or `undefined` if valid.
*/
oneOf (value, schema, opts) {
let validated = false
let allErrors = []
schema.oneOf.forEach(function (_schema) {
const errors = validate(value, _schema, opts)
if (errors) {
allErrors = allErrors.concat(errors)
} else if (validated) {
allErrors = [makeError('valid against more than one', 'valid against only one', opts)]
validated = false
return false
} else {
validated = true
}
})
return validated ? undefined : allErrors
},
/**
* Validates the provided string matches a pattern defined in the Schema's `pattern` keyword.
* Validation succeeds if the string is a match of the regex value of this keyword.
*
* see http://json-schema.org/latest/json-schema-validation.html#anchor33
* @name Schema.validationKeywords.pattern
* @method
* @param {*} value String to be validated.
* @param {object} schema Schema containing the `pattern` keyword.
* @param {object} [opts] Configuration options.
* @returns {(array|undefined)} Array of errors or `undefined` if valid.
*/
pattern (value, schema, opts) {
const pattern = schema.pattern
if (utils.isString(value) && !value.match(pattern)) {
return makeError(value, pattern, opts)
}
},
/**
* Validates the provided object's properties against a map of values defined in the Schema's `properties` keyword.
* Validation succeeds if the object's property are valid with each of the schema's in the provided map.
* Validation also depends on the additionalProperties and or patternProperties.
*
* see http://json-schema.org/latest/json-schema-validation.html#anchor64 for more info.
*
* @name Schema.validationKeywords.properties
* @method
* @param {*} value Object to be validated.
* @param {object} schema Schema containing the `properties` keyword.
* @param {object} [opts] Configuration options.
* @returns {(array|undefined)} Array of errors or `undefined` if valid.
*/
properties (value, schema, opts) {
opts || (opts = {})
if (utils.isArray(value)) {
return
}
// Can be a boolean or an object
// Technically the default is an "empty schema", but here "true" is
// functionally the same
const additionalProperties = schema.additionalProperties === undefined ? true : schema.additionalProperties
const validated = []
// "p": The property set from "properties".
// Default is an object
const properties = schema.properties || {}
// "pp": The property set from "patternProperties".
// Default is an object
const patternProperties = schema.patternProperties || {}
let errors = []
utils.forOwn(properties, function (_schema, prop) {
opts.prop = prop
errors = errors.concat(validate(value[prop], _schema, opts) || [])
validated.push(prop)
})
const toValidate = utils.omit(value, validated)
utils.forOwn(patternProperties, function (_schema, pattern) {
utils.forOwn(toValidate, function (undef, prop) {
if (prop.match(pattern)) {
opts.prop = prop
errors = errors.concat(validate(value[prop], _schema, opts) || [])
validated.push(prop)
}
})
})
const keys = Object.keys(utils.omit(value, validated))
// If "s" is not empty, validation fails
if (additionalProperties === false) {
if (keys.length) {
const origProp = opts.prop
opts.prop = ''
addError(`extra fields: ${keys.join(', ')}`, 'no extra fields', opts, errors)
opts.prop = origProp
}
} else if (utils.isObject(additionalProperties)) {
// Otherwise, validate according to provided schema
keys.forEach(function (prop) {
opts.prop = prop
errors = errors.concat(validate(value[prop], additionalProperties, opts) || [])
})
}
return errors.length ? errors : undefined
},
/**
* Validates the provided object's has all properties listed in the Schema's `properties` keyword array.
* Validation succeeds if the object contains all properties provided in the array value of this keyword.
* see http://json-schema.org/latest/json-schema-validation.html#anchor61
*
* @name Schema.validationKeywords.required
* @method
* @param {*} value Object to be validated.
* @param {object} schema Schema containing the `required` keyword.
* @param {object} [opts] Configuration options.
* @returns {(array|undefined)} Array of errors or `undefined` if valid.
*/
required (value, schema, opts) {
opts || (opts = {})
const required = schema.required
let errors = []
if (!opts.existingOnly) {
required.forEach(function (prop) {
if (utils.get(value, prop) === undefined) {
const prevProp = opts.prop
opts.prop = prop
addError(undefined, 'a value', opts, errors)
opts.prop = prevProp
}
})
}
return errors.length ? errors : undefined
},
/**
* Validates the provided value's type is equal to the type, or array of types, defined in the Schema's `type` keyword.
* see http://json-schema.org/latest/json-schema-validation.html#anchor79
*
* @name Schema.validationKeywords.type
* @method
* @param {*} value Value to be validated.
* @param {object} schema Schema containing the `type` keyword.
* @param {object} [opts] Configuration options.
* @returns {(array|undefined)} Array of errors or `undefined` if valid.
*/
type (value, schema, opts) {
let type = schema.type
let validType
// Can be one of several types
if (utils.isString(type)) {
type = [type]
}
// Try to match the value against an expected type
type.forEach(function (_type) {
// TODO: throw an error if type is not defined
if (types[_type](value, schema, opts)) {
// Matched a type
validType = _type
return false
}
})
// Value did not match any expected type
if (!validType) {
return makeError(value !== undefined && value !== null ? typeof value : '' + value, `one of (${type.join(', ')})`, opts)
}
// Run keyword validators for matched type
// http://json-schema.org/latest/json-schema-validation.html#anchor12
const validator = typeGroupValidators[validType]
if (validator) {
return validator(value, schema, opts)
}
},
/**
* Validates the provided array values are unique.
* Validation succeeds if the items in the array are unique, but only if the value of this keyword is true
* see http://json-schema.org/latest/json-schema-validation.html#anchor49
*
* @name Schema.validationKeywords.uniqueItems
* @method
* @param {*} value Array to be validated.
* @param {object} schema Schema containing the `uniqueItems` keyword.
* @param {object} [opts] Configuration options.
* @returns {(array|undefined)} Array of errors or `undefined` if valid.
*/
uniqueItems (value, schema, opts) {
if (value && value.length && schema.uniqueItems) {
const length = value.length
let item, i, j
// Check n - 1 items
for (i = length - 1; i > 0; i--) {
item = value[i]
// Only compare against unchecked items
for (j = i - 1; j >= 0; j--) {
// Found a duplicate
if (utils.deepEqual(item, value[j])) {
return makeError(item, 'no duplicates', opts)
}
}
}
}
}
}
/**
* @ignore
*/
const runOps = function (ops, value, schema, opts) {
let errors = []
ops.forEach(function (op) {
if (schema[op] !== undefined) {
errors = errors.concat(validationKeywords[op](value, schema, opts) || [])
}
})
return errors.length ? errors : undefined
}
/**
* Validation keywords validated for any type:
*
* - `enum`
* - `type`
* - `allOf`
* - `anyOf`
* - `oneOf`
* - `not`
*
* @name Schema.ANY_OPS
* @type {string[]}
*/
const ANY_OPS = ['enum', 'type', 'allOf', 'anyOf', 'oneOf', 'not']
/**
* Validation keywords validated for array types:
*
* - `items`
* - `maxItems`
* - `minItems`
* - `uniqueItems`
*
* @name Schema.ARRAY_OPS
* @type {string[]}
*/
const ARRAY_OPS = ['items', 'maxItems', 'minItems', 'uniqueItems']
/**
* Validation keywords validated for numeric (number and integer) types:
*
* - `multipleOf`
* - `maximum`
* - `minimum`
*
* @name Schema.NUMERIC_OPS
* @type {string[]}
*/
const NUMERIC_OPS = ['multipleOf', 'maximum', 'minimum']
/**
* Validation keywords validated for object types:
*
* - `maxProperties`
* - `minProperties`
* - `required`
* - `properties`
* - `dependencies`
*
* @name Schema.OBJECT_OPS
* @type {string[]}
*/
const OBJECT_OPS = ['maxProperties', 'minProperties', 'required', 'properties', 'dependencies']
/**
* Validation keywords validated for string types:
*
* - `maxLength`
* - `minLength`
* - `pattern`
*
* @name Schema.STRING_OPS
* @type {string[]}
*/
const STRING_OPS = ['maxLength', 'minLength', 'pattern']
/**
* http://json-schema.org/latest/json-schema-validation.html#anchor75
* @ignore
*/
const validateAny = function (value, schema, opts) {
return runOps(ANY_OPS, value, schema, opts)
}
/**
* Validates the provided value against a given Schema according to the http://json-schema.org/ v4 specification.
*
* @name Schema.validate
* @method
* @param {*} value Value to be validated.
* @param {object} schema Valid Schema according to the http://json-schema.org/ v4 specification.
* @param {object} [opts] Configuration options.
* @returns {(array|undefined)} Array of errors or `undefined` if valid.
*/
const validate = function (value, schema, opts) {
let errors = []
opts || (opts = {})
opts.ctx || (opts.ctx = { value, schema })
let shouldPop
let prevProp = opts.prop
if (schema === undefined) {
return
}
if (!utils.isObject(schema)) {
throw utils.err(`${DOMAIN}#validate`)(500, `Invalid schema at path: "${opts.path}"`)
}
if (opts.path === undefined) {
opts.path = []
}
// Track our location as we recurse
if (opts.prop !== undefined) {
shouldPop = true
opts.path.push(opts.prop)
opts.prop = undefined
}
// Validate against parent schema
if (schema['extends']) {
// opts.path = path
// opts.prop = prop
if (utils.isFunction(schema['extends'].validate)) {
errors = errors.concat(schema['extends'].validate(value, opts) || [])
} else {
errors = errors.concat(validate(value, schema['extends'], opts) || [])
}
}
if (value === undefined) {
// Check if property is required
if (schema.required === true && !opts.existingOnly) {
addError(value, 'a value', opts, errors)
}
if (shouldPop) {
opts.path.pop()
opts.prop = prevProp
}
return errors.length ? errors : undefined
}
errors = errors.concat(validateAny(value, schema, opts) || [])
if (shouldPop) {
opts.path.pop()
opts.prop = prevProp
}
return errors.length ? errors : undefined
}
// These strings are cached for optimal performance of the change detection
// boolean - Whether a Record is changing in the current execution frame
const changingPath = 'changing'
// string[] - Properties that have changed in the current execution frame
const changedPath = 'changed'
// Object[] - History of change records
const changeHistoryPath = 'history'
// boolean - Whether a Record is currently being instantiated
const creatingPath = 'creating'
// number - The setTimeout change event id of a Record, if any
const eventIdPath = 'eventId'
// boolean - Whether to skip validation for a Record's currently changing property
const noValidatePath = 'noValidate'
// boolean - Whether to preserve Change History for a Record
const keepChangeHistoryPath = 'keepChangeHistory'
// boolean - Whether to skip change notification for a Record's currently
// changing property
const silentPath = 'silent'
const validationFailureMsg = 'validation failed'
/**
* A map of validation functions grouped by type.
*
* @name Schema.typeGroupValidators
* @type {object}
*/
const typeGroupValidators = {
/**
* Validates the provided value against the schema using all of the validation keywords specific to instances of an array.
* The validation keywords for the type `array` are:
*```
* ['items', 'maxItems', 'minItems', 'uniqueItems']
*```
* see http://json-schema.org/latest/json-schema-validation.html#anchor25
*
* @name Schema.typeGroupValidators.array
* @method
* @param {*} value Array to be validated.
* @param {object} schema Schema containing at least one array keyword.
* @param {Object} [opts] Configuration options.
* @returns {(array|undefined)} Array of errors or `undefined` if valid.
*/
array: function (value, schema, opts) {
return runOps(ARRAY_OPS, value, schema, opts)
},
/**
* Validates the provided value against the schema using all of the validation keywords specific to instances of an integer.
* The validation keywords for the type `integer` are:
*```
* ['multipleOf', 'maximum', 'minimum']
*```
* @name Schema.typeGroupValidators.integer
* @method
* @param {*} value Number to be validated.
* @param {Object} schema Schema containing at least one `integer` keyword.
* @param {Object} [opts] Configuration options.
* @returns {(array|undefined)} Array of errors or `undefined` if valid.
*/
integer: function (value, schema, opts) {
// Additional validations for numerics are the same
return typeGroupValidators.numeric(value, schema, opts)
},
/**
* Validates the provided value against the schema using all of the validation keywords specific to instances of an number.
* The validation keywords for the type `number` are:
*```
* ['multipleOf', 'maximum', 'minimum']
*```
* @name Schema.typeGroupValidators.number
* @method
* @param {*} value Number to be validated.
* @param {Object} schema Schema containing at least one `number` keyword.
* @param {Object} [opts] Configuration options.
* @returns {(array|undefined)} Array of errors or `undefined` if valid.
*/
number: function (value, schema, opts) {
// Additional validations for numerics are the same
return typeGroupValidators.numeric(value, schema, opts)
},
/**
* Validates the provided value against the schema using all of the validation keywords specific to instances of a number or integer.
* The validation keywords for the type `numeric` are:
*```
* ['multipleOf', 'maximum', 'minimum']
*```
* See http://json-schema.org/latest/json-schema-validation.html#anchor13.
*
* @name Schema.typeGroupValidators.numeric
* @method
* @param {*} value Number to be validated.
* @param {Object} schema Schema containing at least one `numeric` keyword.
* @param {Object} [opts] Configuration options.
* @returns {(array|undefined)} Array of errors or `undefined` if valid.
*/
numeric: function (value, schema, opts) {
return runOps(NUMERIC_OPS, value, schema, opts)
},
/**
* Validates the provided value against the schema using all of the validation keywords specific to instances of an object.
* The validation keywords for the type `object` are:
*```
* ['maxProperties', 'minProperties', 'required', 'properties', 'dependencies']
*```
* See http://json-schema.org/latest/json-schema-validation.html#anchor53.
*
* @name Schema.typeGroupValidators.object
* @method
* @param {*} value Object to be validated.
* @param {Object} schema Schema containing at least one `object` keyword.
* @param {Object} [opts] Configuration options.
* @returns {(array|undefined)} Array of errors or `undefined` if valid.
*/
object: function (value, schema, opts) {
return runOps(OBJECT_OPS, value, schema, opts)
},
/**
* Validates the provided value against the schema using all of the validation keywords specific to instances of an string.
* The validation keywords for the type `string` are:
*```
* ['maxLength', 'minLength', 'pattern']
*```
* See http://json-schema.org/latest/json-schema-validation.html#anchor25.
*
* @name Schema.typeGroupValidators.string
* @method
* @param {*} value String to be validated.
* @param {Object} schema Schema containing at least one `string` keyword.
* @param {Object} [opts] Configuration options.
* @returns {(array|undefined)} Array of errors or `undefined` if valid.
*/
string: function (value, schema, opts) {
return runOps(STRING_OPS, value, schema, opts)
}
}
/**
* js-data's Schema class.
*
* @example <caption>Schema#constructor</caption>
* // Normally you would do: import {Schema} from 'js-data'
* const JSData = require('js-data@3.0.0-rc.4')
* const {Schema} = JSData
* console.log('Using JSData v' + JSData.version.full)
*
* const PostSchema = new Schema({
* type: 'object',
* properties: {
* title: { type: 'string' }
* }
* })
* PostSchema.validate({ title: 1234 })
*
* @class Schema
* @extends Component
* @param {object} definition Schema definition according to json-schema.org
*/
function Schema (definition) {
definition || (definition = {})
// TODO: schema validation
utils.fillIn(this, definition)
if (this.type === 'object') {
this.properties = this.properties || {}
utils.forOwn(this.properties, (_definition, prop) => {
if (!(_definition instanceof Schema)) {
this.properties[prop] = new Schema(_definition)
}
})
} else if (this.type === 'array' && this.items && !(this.items instanceof Schema)) {
this.items = new Schema(this.items)
}
if (this.extends && !(this.extends instanceof Schema)) {
this.extends = new Schema(this.extends)
}
['allOf', 'anyOf', 'oneOf'].forEach((validationKeyword) => {
if (this[validationKeyword]) {
this[validationKeyword].forEach((_definition, i) => {
if (!(_definition instanceof Schema)) {
this[validationKeyword][i] = new Schema(_definition)
}
})
}
})
}
export default Component.extend({
constructor: Schema,
/**
* This adds ES5 getters/setters to the target based on the "properties" in
* this Schema, which makes possible change tracking and validation on
* property assignment.
*
* @name Schema#apply
* @method
* @param {object} target The prototype to which to apply this schema.
*/
apply (target, opts) {
opts || (opts = {})
opts.getter || (opts.getter = '_get')
opts.setter || (opts.setter = '_set')
opts.unsetter || (opts.unsetter = '_unset')
opts.track || (opts.track = this.track)
const properties = this.properties || {}
utils.forOwn(properties, (schema, prop) => {
Object.defineProperty(
target,
prop,
this.makeDescriptor(prop, schema, opts)
)
})
},
/**
* Apply default values to the target object for missing values.
*
* @name Schema#applyDefaults
* @method
* @param {object} target The target to which to apply values for missing values.
*/
applyDefaults (target) {
if (!target) {
return
}
const properties = this.properties || {}
const hasSet = utils.isFunction(target.set) || utils.isFunction(target._set)
utils.forOwn(properties, function (schema, prop) {
if (schema.hasOwnProperty('default') && utils.get(target, prop) === undefined) {
if (hasSet) {
target.set(prop, utils.plainCopy(schema['default']), { silent: true })
} else {
utils.set(target, prop, utils.plainCopy(schema['default']))
}
}
if (schema.type === 'object' && schema.properties) {
if (hasSet) {
const orig = target._get('noValidate')
target._set('noValidate', true)
utils.set(target, prop, utils.get(target, prop) || {}, { silent: true })
target._set('noValidate', orig)
} else {
utils.set(target, prop, utils.get(target, prop) || {})
}
schema.applyDefaults(utils.get(target, prop))
}
})
},
/**
* Assemble a property descriptor for tracking and validating changes to
* a property according to the given schema. This method is called when
* {@link Mapper#applySchema} is set to `true`.
*
* @name Schema#makeDescriptor
* @method
* @param {string} prop The property name.
* @param {(Schema|object)} schema The schema for the property.
* @param {object} [opts] Optional configuration.
* @param {function} [opts.getter] Custom getter function.
* @param {function} [opts.setter] Custom setter function.
* @param {function} [opts.track] Whether to track changes.
* @returns {object} A property descriptor for the given schema.
*/
makeDescriptor (prop, schema, opts) {
const descriptor = {
// Better to allow configurability, but at the user's own risk
configurable: true,
// These properties are enumerable by default, but regardless of their
// enumerability, they won't be "own" properties of individual records
enumerable: schema.enumerable === undefined ? true : !!schema.enumerable
}
// Cache a few strings for optimal performance
const keyPath = `props.${prop}`
const previousPath = `previous.${prop}`
const getter = opts.getter
const setter = opts.setter
const unsetter = opts.unsetter
const track = utils.isBoolean(opts.track) ? opts.track : schema.track
descriptor.get = function () {
return this._get(keyPath)
}
if (utils.isFunction(schema.get)) {
const originalGet = descriptor.get
descriptor.get = function () {
return schema.get.call(this, originalGet)
}
}
descriptor.set = function (value) {
// These are accessed a lot
const _get = this[getter]
const _set = this[setter]
const _unset = this[unsetter]
// Optionally check that the new value passes validation
if (!_get(noValidatePath)) {
const errors = schema.validate(value, { path: [prop] })
if (errors) {
// Immediately throw an error, preventing the record from getting into
// an invalid state
const error = new Error(validationFailureMsg)
error.errors = errors
throw error
}
}
// TODO: Make it so tracking can be turned on for all properties instead of
// only per-property
if (track && !_get(creatingPath)) {
// previous is versioned on database commit
// props are versioned on set()
const previous = _get(previousPath)
const current = _get(keyPath)
let changing = _get(changingPath)
let changed = _get(changedPath)
if (!changing) {
// Track properties that are changing in the current event loop
changed = []
}
// Add changing properties to this array once at most
const index = changed.indexOf(prop)
if (current !== value && index === -1) {
changed.push(prop)
}
if (previous === value) {
if (index >= 0) {
changed.splice(index, 1)
}
}
// No changes in current event loop
if (!changed.length) {
changing = false
_unset(changingPath)
_unset(changedPath)
// Cancel pending change event
if (_get(eventIdPath)) {
clearTimeout(_get(eventIdPath))
_unset(eventIdPath)
}
}
// Changes detected in current event loop
if (!changing && changed.length) {
_set(changedPath, changed)
_set(changingPath, true)
// Saving the timeout id allows us to batch all changes in the same
// event loop into a single "change"
// TODO: Optimize
_set(eventIdPath, setTimeout(() => {
// Previous event loop where changes were gathered has ended, so
// notify any listeners of those changes and prepare for any new
// changes
_unset(changedPath)
_unset(eventIdPath)
_unset(changingPath)
// TODO: Optimize
if (!_get(silentPath)) {
let i
for (i = 0; i < changed.length; i++) {
this.emit('change:' + changed[i], this, utils.get(this, changed[i]))
}
const changes = utils.diffObjects({ [prop]: value }, { [prop]: current })
if (_get(keepChangeHistoryPath)) {
const changeRecord = utils.plainCopy(changes)
changeRecord.timestamp = new Date().getTime()
let changeHistory = _get(changeHistoryPath)
!changeHistory && _set(changeHistoryPath, (changeHistory = []))
changeHistory.push(changeRecord)
}
this.emit('change', this, changes)
}
_unset(silentPath)
}, 0))
}
}
_set(keyPath, value)
return value
}
if (utils.isFunction(schema.set)) {
const originalSet = descriptor.set
descriptor.set = function (value) {
return schema.set.call(this, value, originalSet)
}
}
return descriptor
},
/**
* Create a copy of the given value that contains only the properties defined
* in this schema.
*
* @name Schema#pick
* @method
* @param {*} value The value to copy.
* @returns {*} The copy.
*/
pick (value) {
if (value === undefined) {
return
}
if (this.type === 'object') {
let copy = {}
const properties = this.properties
if (properties) {
utils.forOwn(properties, (_definition, prop) => {
copy[prop] = _definition.pick(value[prop])
})
}
if (this.extends) {
utils.fillIn(copy, this.extends.pick(value))
}
// Conditionally copy properties not defined in "properties"
if (this.additionalProperties) {
for (var key in value) {
if (!properties[key]) {
copy[key] = utils.plainCopy(value[key])
}
}
}
return copy
} else if (this.type === 'array') {
return value.map((item) => {
const _copy = this.items ? this.items.pick(item) : {}
if (this.extends) {
utils.fillIn(_copy, this.extends.pick(item))
}
return _copy
})
}
return utils.plainCopy(value)
},
/**
* Validate the provided value against this schema.
*
* @name Schema#validate
* @method
* @param {*} value Value to validate.
* @param {object} [opts] Configuration options.
* @returns {(array|undefined)} Array of errors or `undefined` if valid.
*/
validate (value, opts) {
return validate(value, this, opts)
}
}, {
ANY_OPS,
ARRAY_OPS,
NUMERIC_OPS,
OBJECT_OPS,
STRING_OPS,
typeGroupValidators,
types,
validate,
validationKeywords
})
/**
* Create a subclass of this Schema:
* @example <caption>Schema.extend</caption>
* // Normally you would do: import {Schema} from 'js-data'
* const JSData = require('js-data@3.0.0-rc.4')
* const {Schema} = JSData
* console.log('Using JSData v' + JSData.version.full)
*
* // Extend the class using ES2015 class syntax.
* class CustomSchemaClass extends Schema {
* foo () { return 'bar' }
* static beep () { return 'boop' }
* }
* const customSchema = new CustomSchemaClass()
* console.log(customSchema.foo())
* console.log(CustomSchemaClass.beep())
*
* // Extend the class using alternate method.
* const OtherSchemaClass = Schema.extend({
* foo () { return 'bar' }
* }, {
* beep () { return 'boop' }
* })
* const otherSchema = new OtherSchemaClass()
* console.log(otherSchema.foo())
* console.log(OtherSchemaClass.beep())
*
* // Extend the class, providing a custom constructor.
* function AnotherSchemaClass () {
* Schema.call(this)
* this.created_at = new Date().getTime()
* }
* Schema.extend({
* constructor: AnotherSchemaClass,
* foo () { return 'bar' }
* }, {
* beep () { return 'boop' }
* })
* const anotherSchema = new AnotherSchemaClass()
* console.log(anotherSchema.created_at)
* console.log(anotherSchema.foo())
* console.log(AnotherSchemaClass.beep())
*
* @method Schema.extend
* @param {object} [props={}] Properties to add to the prototype of the
* subclass.
* @param {object} [props.constructor] Provide a custom constructor function
* to be used as the subclass itself.
* @param {object} [classProps={}] Static properties to add to the subclass.
* @returns {Constructor} Subclass of this Schema class.
* @since 3.0.0
*/