validate
Version:
Validate object properties in javascript.
456 lines (376 loc) • 14.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = void 0;
var _dot = _interopRequireDefault(require("@eivifj/dot"));
var _typecast = _interopRequireDefault(require("typecast"));
var _property = _interopRequireDefault(require("./property"));
var _messages = _interopRequireDefault(require("./messages"));
var _validators = _interopRequireDefault(require("./validators"));
var _error = _interopRequireDefault(require("./error"));
var _utils = require("./utils");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); }
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); }
function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); }
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
function _iterableToArrayLimit(arr, i) { var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
/**
* A Schema defines the structure that objects should be validated against.
*
* @example
* const post = new Schema({
* title: {
* type: String,
* required: true,
* length: { min: 1, max: 255 }
* },
* content: {
* type: String,
* required: true
* },
* published: {
* type: Date,
* required: true
* },
* keywords: [{ type: String }]
* })
*
* @example
* const author = new Schema({
* name: {
* type: String,
* required: true
* },
* email: {
* type: String,
* required: true
* },
* posts: [post]
* })
*
* @param {Object} [obj] - schema definition
* @param {Object} [opts] - options
* @param {Boolean} [opts.typecast=false] - typecast values before validation
* @param {Boolean} [opts.strip=true] - strip properties not defined in the schema
* @param {Boolean} [opts.strict=false] - validation fails when object contains properties not defined in the schema
*/
var Schema = /*#__PURE__*/function () {
function Schema() {
var _this = this;
var obj = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
_classCallCheck(this, Schema);
this.opts = opts;
this.hooks = [];
this.props = {};
this.messages = Object.assign({}, _messages["default"]);
this.validators = Object.assign({}, _validators["default"]);
this.typecasters = Object.assign({}, _typecast["default"]);
Object.keys(obj).forEach(function (k) {
return _this.path(k, obj[k]);
});
}
/**
* Create or update `path` with given `rules`.
*
* @example
* const schema = new Schema()
* schema.path('name.first', { type: String })
* schema.path('name.last').type(String).required()
*
* @param {String} path - full path using dot-notation
* @param {Object|Array|String|Schema|Property} [rules] - rules to apply
* @return {Property}
*/
_createClass(Schema, [{
key: "path",
value: function path(_path, rules) {
var _this2 = this;
var parts = _path.split('.');
var suffix = parts.pop();
var prefix = parts.join('.'); // Make sure full path is created
if (prefix) {
this.path(prefix);
} // Array index placeholder
if (suffix === '$') {
this.path(prefix).type(Array);
} // Nested schema
if (rules instanceof Schema) {
rules.hook(function (k, v) {
return _this2.path((0, _utils.join)(k, _path), v);
});
return this.path(_path, rules.props);
} // Return early when given a `Property`
if (rules instanceof _property["default"]) {
this.props[_path] = rules; // Notify parents if mounted
this.propagate(_path, rules);
return rules;
}
var prop = this.props[_path] || new _property["default"](_path, this);
this.props[_path] = prop; // Notify parents if mounted
this.propagate(_path, prop); // No rules?
if (!rules) return prop; // type shorthand
// `{ name: String }`
if (typeof rules == 'string' || typeof rules == 'function') {
prop.type(rules);
return prop;
} // Allow arrays to be defined implicitly:
// `{ keywords: [String] }`
// `{ keyVal: [[String, Number]] }`
if (Array.isArray(rules)) {
prop.type(Array);
if (rules.length === 1) {
prop.each(rules[0]);
} else {
prop.elements(rules);
}
return prop;
}
var keys = Object.keys(rules);
var nested = false; // Check for nested objects
for (var _i = 0, _keys = keys; _i < _keys.length; _i++) {
var key = _keys[_i];
if (typeof prop[key] == 'function') continue;
prop.type(Object);
nested = true;
break;
}
keys.forEach(function (key) {
var rule = rules[key];
if (nested) {
return _this2.path((0, _utils.join)(key, _path), rule);
}
prop[key](rule);
});
return prop;
}
/**
* Typecast given `obj`.
*
* @param {Object} obj - the object to typecast
* @return {Schema}
* @private
*/
}, {
key: "typecast",
value: function typecast(obj) {
var _loop = function _loop() {
var _Object$entries$_i = _slicedToArray(_Object$entries[_i2], 2),
path = _Object$entries$_i[0],
prop = _Object$entries$_i[1];
(0, _utils.enumerate)(path, obj, function (key, value) {
if (value == null) return;
var cast = prop.typecast(value);
if (cast === value) return;
_dot["default"].set(obj, key, cast);
});
};
for (var _i2 = 0, _Object$entries = Object.entries(this.props); _i2 < _Object$entries.length; _i2++) {
_loop();
}
return this;
}
/**
* Strip all keys not defined in the schema
*
* @param {Object} obj - the object to strip
* @param {String} [prefix]
* @return {Schema}
* @private
*/
}, {
key: "strip",
value: function strip(obj) {
var _this3 = this;
(0, _utils.walk)(obj, function (path, prop) {
if (_this3.props[prop]) return true;
_dot["default"]["delete"](obj, path);
return false;
});
return this;
}
/**
* Create errors for all properties that are not defined in the schema
*
* @param {Object} obj - the object to check
* @return {Schema}
* @private
*/
}, {
key: "enforce",
value: function enforce(obj) {
var _this4 = this;
var errors = [];
(0, _utils.walk)(obj, function (path, prop) {
if (_this4.props[prop]) return true;
var error = new _error["default"](_messages["default"].illegal(path), path);
errors.push(error);
return false;
});
return errors;
}
/**
* Validate given `obj`.
*
* @example
* const schema = new Schema({ name: { required: true }})
* const errors = schema.validate({})
* assert(errors.length == 1)
* assert(errors[0].message == 'name is required')
* assert(errors[0].path == 'name')
*
* @param {Object} obj - the object to validate
* @param {Object} [opts] - options, see [Schema](#schema-1)
* @return {Array}
*/
}, {
key: "validate",
value: function validate(obj) {
var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
opts = Object.assign(this.opts, opts);
var errors = [];
if (opts.typecast) {
this.typecast(obj);
}
if (opts.strict) {
errors.push.apply(errors, _toConsumableArray(this.enforce(obj)));
}
if (opts.strip !== false) {
this.strip(obj);
}
var _loop2 = function _loop2() {
var _Object$entries2$_i = _slicedToArray(_Object$entries2[_i3], 2),
path = _Object$entries2$_i[0],
prop = _Object$entries2$_i[1];
(0, _utils.enumerate)(path, obj, function (key, value) {
var err = prop.validate(value, obj, key);
if (err) errors.push(err);
});
};
for (var _i3 = 0, _Object$entries2 = Object.entries(this.props); _i3 < _Object$entries2.length; _i3++) {
_loop2();
}
return errors;
}
/**
* Assert that given `obj` is valid.
*
* @example
* const schema = new Schema({ name: String })
* schema.assert({ name: 1 }) // Throws an error
*
* @param {Object} obj
* @param {Object} [opts]
*/
}, {
key: "assert",
value: function assert(obj, opts) {
var _this$validate = this.validate(obj, opts),
_this$validate2 = _slicedToArray(_this$validate, 1),
err = _this$validate2[0];
if (err) throw err;
}
/**
* Override default error messages.
*
* @example
* const hex = (val) => /^0x[0-9a-f]+$/.test(val)
* schema.path('some.path').use({ hex })
* schema.message('hex', path => `${path} must be hexadecimal`)
*
* @example
* schema.message({ hex: path => `${path} must be hexadecimal` })
*
* @param {String|Object} name - name of the validator or an object with name-message pairs
* @param {String|Function} [message] - the message or message generator to use
* @return {Schema}
*/
}, {
key: "message",
value: function message(name, _message) {
(0, _utils.assign)(name, _message, this.messages);
return this;
}
/**
* Override default validators.
*
* @example
* schema.validator('required', val => val != null)
*
* @example
* schema.validator({ required: val => val != null })
*
* @param {String|Object} name - name of the validator or an object with name-function pairs
* @param {Function} [fn] - the function to use
* @return {Schema}
*/
}, {
key: "validator",
value: function validator(name, fn) {
(0, _utils.assign)(name, fn, this.validators);
return this;
}
/**
* Override default typecasters.
*
* @example
* schema.typecaster('SomeClass', val => new SomeClass(val))
*
* @example
* schema.typecaster({ SomeClass: val => new SomeClass(val) })
*
* @param {String|Object} name - name of the validator or an object with name-function pairs
* @param {Function} [fn] - the function to use
* @return {Schema}
*/
}, {
key: "typecaster",
value: function typecaster(name, fn) {
(0, _utils.assign)(name, fn, this.typecasters);
return this;
}
/**
* Accepts a function that is called whenever new props are added.
*
* @param {Function} fn - the function to call
* @return {Schema}
* @private
*/
}, {
key: "hook",
value: function hook(fn) {
this.hooks.push(fn);
return this;
}
/**
* Notify all subscribers that a property has been added.
*
* @param {String} path - the path of the property
* @param {Property} prop - the new property
* @return {Schema}
* @private
*/
}, {
key: "propagate",
value: function propagate(path, prop) {
this.hooks.forEach(function (fn) {
return fn(path, prop);
});
return this;
}
}]);
return Schema;
}(); // Export ValidationError
exports["default"] = Schema;
Schema.ValidationError = _error["default"];
module.exports = exports.default;