692 lines
20 KiB
JavaScript
692 lines
20 KiB
JavaScript
"use strict";
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
}) : function(o, v) {
|
|
o["default"] = v;
|
|
});
|
|
var __importStar = (this && this.__importStar) || function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
const schematype_1 = __importDefault(require("./schematype"));
|
|
const Types = __importStar(require("./types/index"));
|
|
const bluebird_1 = __importDefault(require("bluebird"));
|
|
const util_1 = require("./util");
|
|
const population_1 = __importDefault(require("./error/population"));
|
|
const is_plain_object_1 = require("is-plain-object");
|
|
const builtinTypes = new Set(['String', 'Number', 'Boolean', 'Array', 'Object', 'Date', 'Buffer']);
|
|
const getSchemaType = (name, options) => {
|
|
const Type = options.type || options;
|
|
const typeName = Type.name;
|
|
if (builtinTypes.has(typeName)) {
|
|
return new Types[typeName](name, options);
|
|
}
|
|
return new Type(name, options);
|
|
};
|
|
const checkHookType = (type) => {
|
|
if (type !== 'save' && type !== 'remove') {
|
|
throw new TypeError('Hook type must be `save` or `remove`!');
|
|
}
|
|
};
|
|
const hookWrapper = (fn) => {
|
|
if (fn.length > 1) {
|
|
return bluebird_1.default.promisify(fn);
|
|
}
|
|
return bluebird_1.default.method(fn);
|
|
};
|
|
/**
|
|
* @param {Function[]} stack
|
|
*/
|
|
const execSortStack = (stack) => {
|
|
const len = stack.length;
|
|
return (a, b) => {
|
|
let result;
|
|
for (let i = 0; i < len; i++) {
|
|
result = stack[i](a, b);
|
|
if (result)
|
|
break;
|
|
}
|
|
return result;
|
|
};
|
|
};
|
|
const sortStack = (path_, key, sort) => {
|
|
const path = path_ || new schematype_1.default(key);
|
|
const descending = sort === 'desc' || sort === -1;
|
|
return (a, b) => {
|
|
const result = path.compare((0, util_1.getProp)(a, key), (0, util_1.getProp)(b, key));
|
|
return descending && result ? result * -1 : result;
|
|
};
|
|
};
|
|
class UpdateParser {
|
|
static updateStackNormal(key, update) {
|
|
return (data) => { (0, util_1.setProp)(data, key, update); };
|
|
}
|
|
static updateStackOperator(path_, ukey, key, update) {
|
|
const path = path_ || new schematype_1.default(key);
|
|
return (data) => {
|
|
const result = path[ukey]((0, util_1.getProp)(data, key), update, data);
|
|
(0, util_1.setProp)(data, key, result);
|
|
};
|
|
}
|
|
// eslint-disable-next-line no-useless-constructor
|
|
constructor(paths) {
|
|
this.paths = paths;
|
|
}
|
|
/**
|
|
* Parses updating expressions and returns a stack.
|
|
*
|
|
* @param {Object} updates
|
|
* @param {queryCallback[]} [stack]
|
|
* @private
|
|
*/
|
|
parseUpdate(updates, prefix = '', stack = []) {
|
|
const { paths } = this;
|
|
const { updateStackOperator } = UpdateParser;
|
|
const keys = Object.keys(updates);
|
|
let path, prefixNoDot;
|
|
if (prefix) {
|
|
prefixNoDot = prefix.substring(0, prefix.length - 1);
|
|
path = paths[prefixNoDot];
|
|
}
|
|
for (let i = 0, len = keys.length; i < len; i++) {
|
|
const key = keys[i];
|
|
const update = updates[key];
|
|
const name = prefix + key;
|
|
// Update operators
|
|
if (key[0] === '$') {
|
|
const ukey = `u${key}`;
|
|
// First-class update operators
|
|
if (prefix) {
|
|
stack.push(updateStackOperator(path, ukey, prefixNoDot, update));
|
|
}
|
|
else { // Inline update operators
|
|
const fields = Object.keys(update);
|
|
const fieldLen = fields.length;
|
|
for (let j = 0; j < fieldLen; j++) {
|
|
const field = fields[i];
|
|
stack.push(updateStackOperator(paths[field], ukey, field, update[field]));
|
|
}
|
|
}
|
|
}
|
|
else if ((0, is_plain_object_1.isPlainObject)(update)) {
|
|
this.parseUpdate(update, `${name}.`, stack);
|
|
}
|
|
else {
|
|
stack.push(UpdateParser.updateStackNormal(name, update));
|
|
}
|
|
}
|
|
return stack;
|
|
}
|
|
}
|
|
/**
|
|
* @private
|
|
*/
|
|
class QueryParser {
|
|
// eslint-disable-next-line no-useless-constructor
|
|
constructor(paths) {
|
|
this.paths = paths;
|
|
}
|
|
/**
|
|
*
|
|
* @param {string} name
|
|
* @param {*} query
|
|
* @return {queryFilterCallback}
|
|
*/
|
|
queryStackNormal(name, query) {
|
|
const path = this.paths[name] || new schematype_1.default(name);
|
|
return (data) => path.match((0, util_1.getProp)(data, name), query, data);
|
|
}
|
|
/**
|
|
*
|
|
* @param {string} qkey
|
|
* @param {string} name
|
|
* @param {*} query
|
|
* @return {queryFilterCallback}
|
|
*/
|
|
queryStackOperator(qkey, name, query) {
|
|
const path = this.paths[name] || new schematype_1.default(name);
|
|
return data => path[qkey]((0, util_1.getProp)(data, name), query, data);
|
|
}
|
|
/**
|
|
* @param {Array} arr
|
|
* @param {queryFilterCallback[]} stack The function generated by query is added to the stack.
|
|
* @return {void}
|
|
* @private
|
|
*/
|
|
$and(arr, stack) {
|
|
for (let i = 0, len = arr.length; i < len; i++) {
|
|
stack.push(this.execQuery(arr[i]));
|
|
}
|
|
}
|
|
/**
|
|
* @param {Array} query
|
|
* @return {queryFilterCallback}
|
|
* @private
|
|
*/
|
|
$or(query) {
|
|
const stack = this.parseQueryArray(query);
|
|
const len = stack.length;
|
|
return data => {
|
|
for (let i = 0; i < len; i++) {
|
|
if (stack[i](data))
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
}
|
|
/**
|
|
* @param {Array} query
|
|
* @return {queryFilterCallback}
|
|
* @private
|
|
*/
|
|
$nor(query) {
|
|
const stack = this.parseQueryArray(query);
|
|
const len = stack.length;
|
|
return data => {
|
|
for (let i = 0; i < len; i++) {
|
|
if (stack[i](data))
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
}
|
|
/**
|
|
* @param {*} query
|
|
* @return {queryFilterCallback}
|
|
* @private
|
|
*/
|
|
$not(query) {
|
|
const stack = this.parseQuery(query);
|
|
const len = stack.length;
|
|
return data => {
|
|
for (let i = 0; i < len; i++) {
|
|
if (!stack[i](data))
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
}
|
|
/**
|
|
* @callback queryWherecallback
|
|
* @return {boolean}
|
|
* @this {QueryPerser}
|
|
*/
|
|
/**
|
|
* @param {queryWherecallback} fn
|
|
* @return {queryFilterCallback}
|
|
* @private
|
|
*/
|
|
$where(fn) {
|
|
return data => Reflect.apply(fn, data, []);
|
|
}
|
|
/**
|
|
* Parses array of query expressions and returns a stack.
|
|
*
|
|
* @param {Array} arr
|
|
* @return {queryFilterCallback[]}
|
|
* @private
|
|
*/
|
|
parseQueryArray(arr) {
|
|
const stack = [];
|
|
this.$and(arr, stack);
|
|
return stack;
|
|
}
|
|
/**
|
|
* Parses normal query expressions and returns a stack.
|
|
*
|
|
* @param {Object} queries
|
|
* @param {String} prefix
|
|
* @param {queryFilterCallback[]} [stack] The function generated by query is added to the stack passed in this argument. If not passed, a new stack will be created.
|
|
* @return {void}
|
|
* @private
|
|
*/
|
|
parseNormalQuery(queries, prefix, stack = []) {
|
|
const keys = Object.keys(queries);
|
|
for (let i = 0, len = keys.length; i < len; i++) {
|
|
const key = keys[i];
|
|
const query = queries[key];
|
|
if (key[0] === '$') {
|
|
stack.push(this.queryStackOperator(`q${key}`, prefix, query));
|
|
continue;
|
|
}
|
|
const name = `${prefix}.${key}`;
|
|
if ((0, is_plain_object_1.isPlainObject)(query)) {
|
|
this.parseNormalQuery(query, name, stack);
|
|
}
|
|
else {
|
|
stack.push(this.queryStackNormal(name, query));
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Parses query expressions and returns a stack.
|
|
*
|
|
* @param {Object} queries
|
|
* @return {queryFilterCallback[]}
|
|
* @private
|
|
*/
|
|
parseQuery(queries) {
|
|
/** @type {queryFilterCallback[]} */
|
|
const stack = [];
|
|
const keys = Object.keys(queries);
|
|
for (let i = 0, len = keys.length; i < len; i++) {
|
|
const key = keys[i];
|
|
const query = queries[key];
|
|
switch (key) {
|
|
case '$and':
|
|
this.$and(query, stack);
|
|
break;
|
|
case '$or':
|
|
stack.push(this.$or(query));
|
|
break;
|
|
case '$nor':
|
|
stack.push(this.$nor(query));
|
|
break;
|
|
case '$not':
|
|
stack.push(this.$not(query));
|
|
break;
|
|
case '$where':
|
|
stack.push(this.$where(query));
|
|
break;
|
|
default:
|
|
if ((0, is_plain_object_1.isPlainObject)(query)) {
|
|
this.parseNormalQuery(query, key, stack);
|
|
}
|
|
else {
|
|
stack.push(this.queryStackNormal(key, query));
|
|
}
|
|
}
|
|
}
|
|
return stack;
|
|
}
|
|
/**
|
|
* Returns a function for querying.
|
|
*
|
|
* @param {Object} query
|
|
* @return {queryFilterCallback}
|
|
* @private
|
|
*/
|
|
execQuery(query) {
|
|
const stack = this.parseQuery(query);
|
|
const len = stack.length;
|
|
return data => {
|
|
for (let i = 0; i < len; i++) {
|
|
if (!stack[i](data))
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
}
|
|
}
|
|
class Schema {
|
|
/**
|
|
* Schema constructor.
|
|
*
|
|
* @param {Object} [schema]
|
|
*/
|
|
constructor(schema) {
|
|
this.paths = {};
|
|
this.statics = {};
|
|
this.methods = {};
|
|
this.hooks = {
|
|
pre: {
|
|
save: [],
|
|
remove: []
|
|
},
|
|
post: {
|
|
save: [],
|
|
remove: []
|
|
}
|
|
};
|
|
this.stacks = {
|
|
getter: [],
|
|
setter: [],
|
|
import: [],
|
|
export: []
|
|
};
|
|
if (schema) {
|
|
this.add(schema);
|
|
}
|
|
}
|
|
/**
|
|
* Adds paths.
|
|
*
|
|
* @param {Object} schema
|
|
* @param {String} prefix
|
|
*/
|
|
add(schema, prefix = '') {
|
|
const keys = Object.keys(schema);
|
|
const len = keys.length;
|
|
if (!len)
|
|
return;
|
|
for (let i = 0; i < len; i++) {
|
|
const key = keys[i];
|
|
const value = schema[key];
|
|
this.path(prefix + key, value);
|
|
}
|
|
}
|
|
path(name, obj) {
|
|
if (obj == null) {
|
|
return this.paths[name];
|
|
}
|
|
let type;
|
|
let nested = false;
|
|
if (obj instanceof schematype_1.default) {
|
|
type = obj;
|
|
}
|
|
else {
|
|
switch (typeof obj) {
|
|
case 'function':
|
|
type = getSchemaType(name, { type: obj });
|
|
break;
|
|
case 'object':
|
|
if (Array.isArray(obj)) {
|
|
type = new Types.Array(name, {
|
|
child: obj.length ? getSchemaType(name, obj[0]) : new schematype_1.default(name)
|
|
});
|
|
}
|
|
else if (obj.type) {
|
|
type = getSchemaType(name, obj);
|
|
}
|
|
else {
|
|
type = new Types.Object();
|
|
nested = Object.keys(obj).length > 0;
|
|
}
|
|
break;
|
|
default:
|
|
throw new TypeError(`Invalid value for schema path \`${name}\``);
|
|
}
|
|
}
|
|
this.paths[name] = type;
|
|
this._updateStack(name, type);
|
|
if (nested)
|
|
this.add(obj, `${name}.`);
|
|
}
|
|
/**
|
|
* Updates cache stacks.
|
|
*
|
|
* @param {String} name
|
|
* @param {SchemaType} type
|
|
* @private
|
|
*/
|
|
_updateStack(name, type) {
|
|
const { stacks } = this;
|
|
stacks.getter.push(data => {
|
|
const value = (0, util_1.getProp)(data, name);
|
|
const result = type.cast(value, data);
|
|
if (result !== undefined) {
|
|
(0, util_1.setProp)(data, name, result);
|
|
}
|
|
});
|
|
stacks.setter.push(data => {
|
|
const value = (0, util_1.getProp)(data, name);
|
|
const result = type.validate(value, data);
|
|
if (result !== undefined) {
|
|
(0, util_1.setProp)(data, name, result);
|
|
}
|
|
else {
|
|
(0, util_1.delProp)(data, name);
|
|
}
|
|
});
|
|
stacks.import.push(data => {
|
|
const value = (0, util_1.getProp)(data, name);
|
|
const result = type.parse(value);
|
|
if (result !== undefined) {
|
|
(0, util_1.setProp)(data, name, result);
|
|
}
|
|
});
|
|
stacks.export.push(data => {
|
|
const value = (0, util_1.getProp)(data, name);
|
|
const result = type.value(value, data);
|
|
if (result !== undefined) {
|
|
(0, util_1.setProp)(data, name, result);
|
|
}
|
|
else {
|
|
(0, util_1.delProp)(data, name);
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Adds a virtual path.
|
|
*
|
|
* @param {String} name
|
|
* @param {Function} [getter]
|
|
* @return {SchemaType.Virtual}
|
|
*/
|
|
virtual(name, getter) {
|
|
const virtual = new Types.Virtual(name, {});
|
|
if (getter)
|
|
virtual.get(getter);
|
|
this.path(name, virtual);
|
|
return virtual;
|
|
}
|
|
/**
|
|
* Adds a pre-hook.
|
|
*
|
|
* @param {String} type Hook type. One of `save` or `remove`.
|
|
* @param {Function} fn
|
|
*/
|
|
pre(type, fn) {
|
|
checkHookType(type);
|
|
if (typeof fn !== 'function')
|
|
throw new TypeError('Hook must be a function!');
|
|
this.hooks.pre[type].push(hookWrapper(fn));
|
|
}
|
|
/**
|
|
* Adds a post-hook.
|
|
*
|
|
* @param {String} type Hook type. One of `save` or `remove`.
|
|
* @param {Function} fn
|
|
*/
|
|
post(type, fn) {
|
|
checkHookType(type);
|
|
if (typeof fn !== 'function')
|
|
throw new TypeError('Hook must be a function!');
|
|
this.hooks.post[type].push(hookWrapper(fn));
|
|
}
|
|
/**
|
|
* Adds a instance method.
|
|
*
|
|
* @param {String} name
|
|
* @param {Function} fn
|
|
*/
|
|
method(name, fn) {
|
|
if (!name)
|
|
throw new TypeError('Method name is required!');
|
|
if (typeof fn !== 'function') {
|
|
throw new TypeError('Instance method must be a function!');
|
|
}
|
|
this.methods[name] = fn;
|
|
}
|
|
/**
|
|
* Adds a static method.
|
|
*
|
|
* @param {String} name
|
|
* @param {Function} fn
|
|
*/
|
|
static(name, fn) {
|
|
if (!name)
|
|
throw new TypeError('Method name is required!');
|
|
if (typeof fn !== 'function') {
|
|
throw new TypeError('Static method must be a function!');
|
|
}
|
|
this.statics[name] = fn;
|
|
}
|
|
/**
|
|
* Apply getters.
|
|
*
|
|
* @param {Object} data
|
|
* @return {void}
|
|
* @private
|
|
*/
|
|
_applyGetters(data) {
|
|
const stack = this.stacks.getter;
|
|
for (let i = 0, len = stack.length; i < len; i++) {
|
|
stack[i](data);
|
|
}
|
|
}
|
|
/**
|
|
* Apply setters.
|
|
*
|
|
* @param {Object} data
|
|
* @return {void}
|
|
* @private
|
|
*/
|
|
_applySetters(data) {
|
|
const stack = this.stacks.setter;
|
|
for (let i = 0, len = stack.length; i < len; i++) {
|
|
stack[i](data);
|
|
}
|
|
}
|
|
/**
|
|
* Parses database.
|
|
*
|
|
* @param {Object} data
|
|
* @return {Object}
|
|
* @private
|
|
*/
|
|
_parseDatabase(data) {
|
|
const stack = this.stacks.import;
|
|
for (let i = 0, len = stack.length; i < len; i++) {
|
|
stack[i](data);
|
|
}
|
|
return data;
|
|
}
|
|
/**
|
|
* Exports database.
|
|
*
|
|
* @param {Object} data
|
|
* @return {Object}
|
|
* @private
|
|
*/
|
|
_exportDatabase(data) {
|
|
const stack = this.stacks.export;
|
|
for (let i = 0, len = stack.length; i < len; i++) {
|
|
stack[i](data);
|
|
}
|
|
return data;
|
|
}
|
|
/**
|
|
* Parses updating expressions and returns a stack.
|
|
*
|
|
* @param {Object} updates
|
|
* @return {queryCallback[]}
|
|
* @private
|
|
*/
|
|
_parseUpdate(updates) {
|
|
return new UpdateParser(this.paths).parseUpdate(updates);
|
|
}
|
|
/**
|
|
* Returns a function for querying.
|
|
*
|
|
* @param {Object} query
|
|
* @return {queryFilterCallback}
|
|
* @private
|
|
*/
|
|
_execQuery(query) {
|
|
return new QueryParser(this.paths).execQuery(query);
|
|
}
|
|
/**
|
|
* Parses sorting expressions and returns a stack.
|
|
*
|
|
* @param {Object} sorts
|
|
* @param {string} [prefix]
|
|
* @param {queryParseCallback[]} [stack]
|
|
* @return {queryParseCallback[]}
|
|
* @private
|
|
*/
|
|
_parseSort(sorts, prefix = '', stack = []) {
|
|
const { paths } = this;
|
|
const keys = Object.keys(sorts);
|
|
for (let i = 0, len = keys.length; i < len; i++) {
|
|
const key = keys[i];
|
|
const sort = sorts[key];
|
|
const name = prefix + key;
|
|
if (typeof sort === 'object') {
|
|
this._parseSort(sort, `${name}.`, stack);
|
|
}
|
|
else {
|
|
stack.push(sortStack(paths[name], name, sort));
|
|
}
|
|
}
|
|
return stack;
|
|
}
|
|
/**
|
|
* Returns a function for sorting.
|
|
*
|
|
* @param {Object} sorts
|
|
* @return {queryParseCallback}
|
|
* @private
|
|
*/
|
|
_execSort(sorts) {
|
|
const stack = this._parseSort(sorts);
|
|
return execSortStack(stack);
|
|
}
|
|
/**
|
|
* Parses population expression and returns a stack.
|
|
*
|
|
* @param {String|Object} expr
|
|
* @return {PopulateResult[]}
|
|
* @private
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
_parsePopulate(expr) {
|
|
const { paths } = this;
|
|
const arr = [];
|
|
if (typeof expr === 'string') {
|
|
const split = expr.split(' ');
|
|
for (let i = 0, len = split.length; i < len; i++) {
|
|
arr[i] = { path: split[i] };
|
|
}
|
|
}
|
|
else if (Array.isArray(expr)) {
|
|
for (let i = 0, len = expr.length; i < len; i++) {
|
|
const item = expr[i];
|
|
arr[i] = typeof item === 'string' ? { path: item } : item;
|
|
}
|
|
}
|
|
else {
|
|
arr[0] = expr;
|
|
}
|
|
for (let i = 0, len = arr.length; i < len; i++) {
|
|
const item = arr[i];
|
|
const key = item.path;
|
|
if (!key) {
|
|
throw new population_1.default('path is required');
|
|
}
|
|
if (!item.model) {
|
|
const path = paths[key];
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
// @ts-ignore
|
|
const ref = path.child ? path.child.options.ref : path.options.ref;
|
|
if (!ref) {
|
|
throw new population_1.default('model is required');
|
|
}
|
|
item.model = ref;
|
|
}
|
|
}
|
|
return arr;
|
|
}
|
|
}
|
|
Schema.Types = Types;
|
|
Schema.prototype.Types = Types;
|
|
exports.default = Schema;
|
|
//# sourceMappingURL=schema.js.map
|