378 lines
10 KiB
JavaScript
378 lines
10 KiB
JavaScript
|
"use strict";
|
||
|
|
||
|
Object.defineProperty(exports, "__esModule", {
|
||
|
value: true
|
||
|
});
|
||
|
exports.create = create;
|
||
|
exports.default = void 0;
|
||
|
|
||
|
var _has = _interopRequireDefault(require("lodash/has"));
|
||
|
|
||
|
var _snakeCase = _interopRequireDefault(require("lodash/snakeCase"));
|
||
|
|
||
|
var _camelCase = _interopRequireDefault(require("lodash/camelCase"));
|
||
|
|
||
|
var _mapKeys = _interopRequireDefault(require("lodash/mapKeys"));
|
||
|
|
||
|
var _mapValues = _interopRequireDefault(require("lodash/mapValues"));
|
||
|
|
||
|
var _propertyExpr = require("property-expr");
|
||
|
|
||
|
var _locale = require("./locale");
|
||
|
|
||
|
var _sortFields = _interopRequireDefault(require("./util/sortFields"));
|
||
|
|
||
|
var _sortByKeyOrder = _interopRequireDefault(require("./util/sortByKeyOrder"));
|
||
|
|
||
|
var _runTests = _interopRequireDefault(require("./util/runTests"));
|
||
|
|
||
|
var _ValidationError = _interopRequireDefault(require("./ValidationError"));
|
||
|
|
||
|
var _schema = _interopRequireDefault(require("./schema"));
|
||
|
|
||
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||
|
|
||
|
function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
||
|
|
||
|
let isObject = obj => Object.prototype.toString.call(obj) === '[object Object]';
|
||
|
|
||
|
function unknown(ctx, value) {
|
||
|
let known = Object.keys(ctx.fields);
|
||
|
return Object.keys(value).filter(key => known.indexOf(key) === -1);
|
||
|
}
|
||
|
|
||
|
const defaultSort = (0, _sortByKeyOrder.default)([]);
|
||
|
|
||
|
class ObjectSchema extends _schema.default {
|
||
|
constructor(spec) {
|
||
|
super({
|
||
|
type: 'object'
|
||
|
});
|
||
|
this.fields = Object.create(null);
|
||
|
this._sortErrors = defaultSort;
|
||
|
this._nodes = [];
|
||
|
this._excludedEdges = [];
|
||
|
this.withMutation(() => {
|
||
|
this.transform(function coerce(value) {
|
||
|
if (typeof value === 'string') {
|
||
|
try {
|
||
|
value = JSON.parse(value);
|
||
|
} catch (err) {
|
||
|
value = null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (this.isType(value)) return value;
|
||
|
return null;
|
||
|
});
|
||
|
|
||
|
if (spec) {
|
||
|
this.shape(spec);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
_typeCheck(value) {
|
||
|
return isObject(value) || typeof value === 'function';
|
||
|
}
|
||
|
|
||
|
_cast(_value, options = {}) {
|
||
|
var _options$stripUnknown;
|
||
|
|
||
|
let value = super._cast(_value, options); //should ignore nulls here
|
||
|
|
||
|
|
||
|
if (value === undefined) return this.getDefault();
|
||
|
if (!this._typeCheck(value)) return value;
|
||
|
let fields = this.fields;
|
||
|
let strip = (_options$stripUnknown = options.stripUnknown) != null ? _options$stripUnknown : this.spec.noUnknown;
|
||
|
|
||
|
let props = this._nodes.concat(Object.keys(value).filter(v => this._nodes.indexOf(v) === -1));
|
||
|
|
||
|
let intermediateValue = {}; // is filled during the transform below
|
||
|
|
||
|
let innerOptions = _extends({}, options, {
|
||
|
parent: intermediateValue,
|
||
|
__validating: options.__validating || false
|
||
|
});
|
||
|
|
||
|
let isChanged = false;
|
||
|
|
||
|
for (const prop of props) {
|
||
|
let field = fields[prop];
|
||
|
let exists = (0, _has.default)(value, prop);
|
||
|
|
||
|
if (field) {
|
||
|
let fieldValue;
|
||
|
let inputValue = value[prop]; // safe to mutate since this is fired in sequence
|
||
|
|
||
|
innerOptions.path = (options.path ? `${options.path}.` : '') + prop; // innerOptions.value = value[prop];
|
||
|
|
||
|
field = field.resolve({
|
||
|
value: inputValue,
|
||
|
context: options.context,
|
||
|
parent: intermediateValue
|
||
|
});
|
||
|
let fieldSpec = 'spec' in field ? field.spec : undefined;
|
||
|
let strict = fieldSpec == null ? void 0 : fieldSpec.strict;
|
||
|
|
||
|
if (fieldSpec == null ? void 0 : fieldSpec.strip) {
|
||
|
isChanged = isChanged || prop in value;
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
fieldValue = !options.__validating || !strict ? // TODO: use _cast, this is double resolving
|
||
|
field.cast(value[prop], innerOptions) : value[prop];
|
||
|
|
||
|
if (fieldValue !== undefined) {
|
||
|
intermediateValue[prop] = fieldValue;
|
||
|
}
|
||
|
} else if (exists && !strip) {
|
||
|
intermediateValue[prop] = value[prop];
|
||
|
}
|
||
|
|
||
|
if (intermediateValue[prop] !== value[prop]) {
|
||
|
isChanged = true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return isChanged ? intermediateValue : value;
|
||
|
}
|
||
|
|
||
|
_validate(_value, opts = {}, callback) {
|
||
|
let errors = [];
|
||
|
let {
|
||
|
sync,
|
||
|
from = [],
|
||
|
originalValue = _value,
|
||
|
abortEarly = this.spec.abortEarly,
|
||
|
recursive = this.spec.recursive
|
||
|
} = opts;
|
||
|
from = [{
|
||
|
schema: this,
|
||
|
value: originalValue
|
||
|
}, ...from]; // this flag is needed for handling `strict` correctly in the context of
|
||
|
// validation vs just casting. e.g strict() on a field is only used when validating
|
||
|
|
||
|
opts.__validating = true;
|
||
|
opts.originalValue = originalValue;
|
||
|
opts.from = from;
|
||
|
|
||
|
super._validate(_value, opts, (err, value) => {
|
||
|
if (err) {
|
||
|
if (!_ValidationError.default.isError(err) || abortEarly) {
|
||
|
return void callback(err, value);
|
||
|
}
|
||
|
|
||
|
errors.push(err);
|
||
|
}
|
||
|
|
||
|
if (!recursive || !isObject(value)) {
|
||
|
callback(errors[0] || null, value);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
originalValue = originalValue || value;
|
||
|
|
||
|
let tests = this._nodes.map(key => (_, cb) => {
|
||
|
let path = key.indexOf('.') === -1 ? (opts.path ? `${opts.path}.` : '') + key : `${opts.path || ''}["${key}"]`;
|
||
|
let field = this.fields[key];
|
||
|
|
||
|
if (field && 'validate' in field) {
|
||
|
field.validate(value[key], _extends({}, opts, {
|
||
|
// @ts-ignore
|
||
|
path,
|
||
|
from,
|
||
|
// inner fields are always strict:
|
||
|
// 1. this isn't strict so the casting will also have cast inner values
|
||
|
// 2. this is strict in which case the nested values weren't cast either
|
||
|
strict: true,
|
||
|
parent: value,
|
||
|
originalValue: originalValue[key]
|
||
|
}), cb);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
cb(null);
|
||
|
});
|
||
|
|
||
|
(0, _runTests.default)({
|
||
|
sync,
|
||
|
tests,
|
||
|
value,
|
||
|
errors,
|
||
|
endEarly: abortEarly,
|
||
|
sort: this._sortErrors,
|
||
|
path: opts.path
|
||
|
}, callback);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
clone(spec) {
|
||
|
const next = super.clone(spec);
|
||
|
next.fields = _extends({}, this.fields);
|
||
|
next._nodes = this._nodes;
|
||
|
next._excludedEdges = this._excludedEdges;
|
||
|
next._sortErrors = this._sortErrors;
|
||
|
return next;
|
||
|
}
|
||
|
|
||
|
concat(schema) {
|
||
|
let next = super.concat(schema);
|
||
|
let nextFields = next.fields;
|
||
|
|
||
|
for (let [field, schemaOrRef] of Object.entries(this.fields)) {
|
||
|
const target = nextFields[field];
|
||
|
|
||
|
if (target === undefined) {
|
||
|
nextFields[field] = schemaOrRef;
|
||
|
} else if (target instanceof _schema.default && schemaOrRef instanceof _schema.default) {
|
||
|
nextFields[field] = schemaOrRef.concat(target);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return next.withMutation(() => next.shape(nextFields));
|
||
|
}
|
||
|
|
||
|
getDefaultFromShape() {
|
||
|
let dft = {};
|
||
|
|
||
|
this._nodes.forEach(key => {
|
||
|
const field = this.fields[key];
|
||
|
dft[key] = 'default' in field ? field.getDefault() : undefined;
|
||
|
});
|
||
|
|
||
|
return dft;
|
||
|
}
|
||
|
|
||
|
_getDefault() {
|
||
|
if ('default' in this.spec) {
|
||
|
return super._getDefault();
|
||
|
} // if there is no default set invent one
|
||
|
|
||
|
|
||
|
if (!this._nodes.length) {
|
||
|
return undefined;
|
||
|
}
|
||
|
|
||
|
return this.getDefaultFromShape();
|
||
|
}
|
||
|
|
||
|
shape(additions, excludes = []) {
|
||
|
let next = this.clone();
|
||
|
let fields = Object.assign(next.fields, additions);
|
||
|
next.fields = fields;
|
||
|
next._sortErrors = (0, _sortByKeyOrder.default)(Object.keys(fields));
|
||
|
|
||
|
if (excludes.length) {
|
||
|
if (!Array.isArray(excludes[0])) excludes = [excludes];
|
||
|
let keys = excludes.map(([first, second]) => `${first}-${second}`);
|
||
|
next._excludedEdges = next._excludedEdges.concat(keys);
|
||
|
}
|
||
|
|
||
|
next._nodes = (0, _sortFields.default)(fields, next._excludedEdges);
|
||
|
return next;
|
||
|
}
|
||
|
|
||
|
pick(keys) {
|
||
|
const picked = {};
|
||
|
|
||
|
for (const key of keys) {
|
||
|
if (this.fields[key]) picked[key] = this.fields[key];
|
||
|
}
|
||
|
|
||
|
return this.clone().withMutation(next => {
|
||
|
next.fields = {};
|
||
|
return next.shape(picked);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
omit(keys) {
|
||
|
const next = this.clone();
|
||
|
const fields = next.fields;
|
||
|
next.fields = {};
|
||
|
|
||
|
for (const key of keys) {
|
||
|
delete fields[key];
|
||
|
}
|
||
|
|
||
|
return next.withMutation(() => next.shape(fields));
|
||
|
}
|
||
|
|
||
|
from(from, to, alias) {
|
||
|
let fromGetter = (0, _propertyExpr.getter)(from, true);
|
||
|
return this.transform(obj => {
|
||
|
if (obj == null) return obj;
|
||
|
let newObj = obj;
|
||
|
|
||
|
if ((0, _has.default)(obj, from)) {
|
||
|
newObj = _extends({}, obj);
|
||
|
if (!alias) delete newObj[from];
|
||
|
newObj[to] = fromGetter(obj);
|
||
|
}
|
||
|
|
||
|
return newObj;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
noUnknown(noAllow = true, message = _locale.object.noUnknown) {
|
||
|
if (typeof noAllow === 'string') {
|
||
|
message = noAllow;
|
||
|
noAllow = true;
|
||
|
}
|
||
|
|
||
|
let next = this.test({
|
||
|
name: 'noUnknown',
|
||
|
exclusive: true,
|
||
|
message: message,
|
||
|
|
||
|
test(value) {
|
||
|
if (value == null) return true;
|
||
|
const unknownKeys = unknown(this.schema, value);
|
||
|
return !noAllow || unknownKeys.length === 0 || this.createError({
|
||
|
params: {
|
||
|
unknown: unknownKeys.join(', ')
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
});
|
||
|
next.spec.noUnknown = noAllow;
|
||
|
return next;
|
||
|
}
|
||
|
|
||
|
unknown(allow = true, message = _locale.object.noUnknown) {
|
||
|
return this.noUnknown(!allow, message);
|
||
|
}
|
||
|
|
||
|
transformKeys(fn) {
|
||
|
return this.transform(obj => obj && (0, _mapKeys.default)(obj, (_, key) => fn(key)));
|
||
|
}
|
||
|
|
||
|
camelCase() {
|
||
|
return this.transformKeys(_camelCase.default);
|
||
|
}
|
||
|
|
||
|
snakeCase() {
|
||
|
return this.transformKeys(_snakeCase.default);
|
||
|
}
|
||
|
|
||
|
constantCase() {
|
||
|
return this.transformKeys(key => (0, _snakeCase.default)(key).toUpperCase());
|
||
|
}
|
||
|
|
||
|
describe() {
|
||
|
let base = super.describe();
|
||
|
base.fields = (0, _mapValues.default)(this.fields, value => value.describe());
|
||
|
return base;
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
exports.default = ObjectSchema;
|
||
|
|
||
|
function create(spec) {
|
||
|
return new ObjectSchema(spec);
|
||
|
}
|
||
|
|
||
|
create.prototype = ObjectSchema.prototype;
|