123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795 |
- 'use strict';
- const SchemaType = require('./schematype');
- const Types = require('./types');
- const Promise = require('bluebird');
- const { getProp, setProp, delProp } = require('./util');
- const PopulationError = require('./error/population');
- const { isPlainObject } = require('is-plain-object');
- /**
- * @callback queryFilterCallback
- * @param {*} data
- * @return {boolean}
- */
- /**
- * @callback queryCallback
- * @param {*} data
- * @return {void}
- */
- /**
- * @callback queryParseCallback
- * @param {*} a
- * @param {*} b
- * @returns {*}
- */
- /**
- * @typedef PopulateResult
- * @property {string} path
- * @property {*} model
- */
- 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 Promise.promisify(fn);
- }
- return Promise.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(key);
- const descending = sort === 'desc' || sort === -1;
- return (a, b) => {
- const result = path.compare(getProp(a, key), getProp(b, key));
- return descending && result ? result * -1 : result;
- };
- };
- class UpdateParser {
- static updateStackNormal(key, update) {
- return data => { setProp(data, key, update); };
- }
- static updateStackOperator(path_, ukey, key, update) {
- const path = path_ || new SchemaType(key);
- return data => {
- const result = path[ukey](getProp(data, key), update, data);
- setProp(data, key, result);
- };
- }
- 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 (isPlainObject(update)) {
- this.parseUpdate(update, `${name}.`, stack);
- } else {
- stack.push(UpdateParser.updateStackNormal(name, update));
- }
- }
- return stack;
- }
- }
- /**
- * @private
- */
- class QueryParser {
- constructor(paths) {
- this.paths = paths;
- }
- /**
- *
- * @param {string} name
- * @param {*} query
- * @return {queryFilterCallback}
- */
- queryStackNormal(name, query) {
- const path = this.paths[name] || new SchemaType(name);
- return data => path.match(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(name);
- return data => path[qkey](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 (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 (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);
- }
- }
- /**
- * Gets/Sets a path.
- *
- * @param {String} name
- * @param {*} obj
- * @return {SchemaType | undefined}
- */
- path(name, obj) {
- if (obj == null) {
- return this.paths[name];
- }
- let type;
- let nested = false;
- if (obj instanceof SchemaType) {
- type = obj;
- } else {
- switch (typeof obj) {
- case 'function':
- type = getSchemaType(name, {type: obj});
- break;
- case 'object':
- if (obj.type) {
- type = getSchemaType(name, obj);
- } else if (Array.isArray(obj)) {
- type = new Types.Array(name, {
- child: obj.length ? getSchemaType(name, obj[0]) : new SchemaType(name)
- });
- } 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 = getProp(data, name);
- const result = type.cast(value, data);
- if (result !== undefined) {
- setProp(data, name, result);
- }
- });
- stacks.setter.push(data => {
- const value = getProp(data, name);
- const result = type.validate(value, data);
- if (result !== undefined) {
- setProp(data, name, result);
- } else {
- delProp(data, name);
- }
- });
- stacks.import.push(data => {
- const value = getProp(data, name);
- const result = type.parse(value, data);
- if (result !== undefined) {
- setProp(data, name, result);
- }
- });
- stacks.export.push(data => {
- const value = getProp(data, name);
- const result = type.value(value, data);
- if (result !== undefined) {
- setProp(data, name, result);
- } else {
- 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
- */
- _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 PopulationError('path is required');
- }
- if (!item.model) {
- const path = paths[key];
- const ref = path.child ? path.child.options.ref : path.options.ref;
- if (!ref) {
- throw new PopulationError('model is required');
- }
- item.model = ref;
- }
- }
- return arr;
- }
- }
- Schema.prototype.Types = Types;
- Schema.Types = Schema.prototype.Types;
- module.exports = Schema;
|