diff --git a/lib/schema.js b/lib/schema.js index 4589149f..cb8fb009 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -3,25 +3,355 @@ const SchemaType = require('./schematype'); const Types = require('./types'); const Promise = require('bluebird'); -const util = require('./util'); +const { getProp, setProp, delProp } = require('./util'); const PopulationError = require('./error/population'); const isPlainObject = require('is-plain-object'); -const { getProp } = util; -const { setProp } = util; -const { delProp } = util; -const { isArray } = Array; - -const builtinTypes = { - String: true, - Number: true, - Boolean: true, - Array: true, - Object: true, - Date: true, - Buffer: true +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 {((a: any, b: any) => any)[]} 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 {Array<(data: any) => void>} [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 {(data: any) => boolean} + */ + 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 {(data: any) => boolean} + */ + 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 {Array<(data: any) => boolean>} 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 {(data: any) => boolean} + * @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 {(data: any) => boolean} + * @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 {(data: any) => boolean} + * @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; + }; + } + + /** + * @param {(this: any) => boolean} fn + * @return {(data: any) => boolean} + * @private + */ + $where(fn) { + return data => Reflect.apply(fn, data, []); + } + + /** + * Parses array of query expressions and returns a stack. + * + * @param {Array} arr + * @return {Array<(data: any) => boolean>} + * @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 {Array<(data: any) => boolean>} [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 {Array<(data: any) => boolean>} + * @private + */ + parseQuery(queries) { + + /** @type {Array<(data: any) => boolean>} */ + 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 {(data: any) => boolean} + * @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 { /** @@ -63,8 +393,7 @@ class Schema { * @param {Object} schema * @param {String} prefix */ - add(schema, prefix_) { - const prefix = prefix_ || ''; + add(schema, prefix = '') { const keys = Object.keys(schema); const len = keys.length; @@ -83,7 +412,7 @@ class Schema { * * @param {String} name * @param {*} obj - * @return {SchemaType} + * @return {SchemaType | undefined} */ path(name, obj) { if (obj == null) { @@ -104,7 +433,7 @@ class Schema { case 'object': if (obj.type) { type = getSchemaType(name, obj); - } else if (isArray(obj)) { + } else if (Array.isArray(obj)) { type = new Types.Array(name, { child: obj.length ? getSchemaType(name, obj[0]) : new SchemaType(name) }); @@ -134,7 +463,7 @@ class Schema { * @private */ _updateStack(name, type) { - const stacks = this.stacks; + const { stacks } = this; stacks.getter.push(data => { const value = getProp(data, name); @@ -255,7 +584,7 @@ class Schema { * Apply getters. * * @param {Object} data - * @return {*} + * @return {void} * @private */ _applyGetters(data) { @@ -270,7 +599,7 @@ class Schema { * Apply setters. * * @param {Object} data - * @return {*} + * @return {void} * @private */ _applySetters(data) { @@ -319,181 +648,35 @@ class Schema { * Parses updating expressions and returns a stack. * * @param {Object} updates - * @param {String} [prefix] - * @return {Array} - * @private - */ - _parseUpdate(updates, prefix_) { - const prefix = prefix_ || ''; - const paths = this.paths; - let stack = []; - 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)) { - stack = stack.concat(this._parseUpdate(update, `${name}.`)); - } else { - stack.push(updateStackNormal(name, update)); - } - } - - return stack; - } - - /** - * Parses array of query expressions and returns a stack. - * - * @param {Array} arr - * @return {Array} - * @private - */ - _parseQueryArray(arr) { - const stack = []; - - for (let i = 0, len = arr.length; i < len; i++) { - stack.push(execQueryStack(this._parseQuery(arr[i]))); - } - - return stack; - } - - /** - * Parses normal query expressions and returns a stack. - * - * @param {Array} queries - * @param {String} [prefix] - * @return {Array} - * @private - */ - _parseNormalQuery(queries, prefix_) { - const prefix = prefix_ || ''; - const paths = this.paths; - let stack = []; - const keys = Object.keys(queries); - 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 query = queries[key]; - const name = prefix + key; - - if (key[0] === '$') { - stack.push(queryStackOperator(path, `q${key}`, prefixNoDot, query)); - } else if (isPlainObject(query)) { - stack = stack.concat(this._parseNormalQuery(query, `${name}.`)); - } else { - stack.push(queryStackNormal(paths[name], name, query)); - } - } - - return stack; - } - - /** - * Parses query expressions and returns a stack. - * - * @param {Array} queries - * @return {Array} + * @return {((data: any) => void)[]} * @private */ - _parseQuery(queries) { - let stack = []; - const paths = this.paths; - 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': - stack = stack.concat(this._parseQueryArray(query)); - break; - - case '$or': - stack.push($or(this._parseQueryArray(query))); - break; - - case '$nor': - stack.push($nor(this._parseQueryArray(query))); - break; - - case '$not': - stack.push($not(this._parseQuery(query))); - break; - - case '$where': - stack.push($where(query)); - break; - - default: - if (isPlainObject(query)) { - stack = stack.concat(this._parseNormalQuery(query, `${key}.`)); - } else { - stack.push(queryStackNormal(paths[key], key, query)); - } - } - } - - return stack; + _parseUpdate(updates) { + return (new UpdateParser(this.paths)).parseUpdate(updates); } /** * Returns a function for querying. * * @param {Object} query - * @return {Function} + * @return {(data: any) => boolean} * @private */ _execQuery(query) { - const stack = this._parseQuery(query); - return execQueryStack(stack); + return (new QueryParser(this.paths)).execQuery(query); } /** * Parses sorting expressions and returns a stack. * * @param {Object} sorts - * @param {String} [prefix] - * @return {Array} + * @param {string} [prefix] + * @param {((a: any, b: any) => any)[]} [stack] + * @return {((a: any, b: any) => any)[]} * @private */ - _parseSort(sorts, prefix_) { - const prefix = prefix_ || ''; - const paths = this.paths; - let stack = []; + _parseSort(sorts, prefix = '', stack = []) { + const { paths } = this; const keys = Object.keys(sorts); for (let i = 0, len = keys.length; i < len; i++) { @@ -502,7 +685,7 @@ class Schema { const name = prefix + key; if (typeof sort === 'object') { - stack = stack.concat(this._parseSort(sort, `${name}.`)); + this._parseSort(sort, `${name}.`, stack); } else { stack.push(sortStack(paths[name], name, sort)); } @@ -515,7 +698,7 @@ class Schema { * Returns a function for sorting. * * @param {Object} sorts - * @return {Function} + * @return {(a: any, b: any) => any} * @private */ _execSort(sorts) { @@ -527,37 +710,27 @@ class Schema { * Parses population expression and returns a stack. * * @param {String|Object} expr - * @return {Array} + * @return {{ path: string; model: any }[]} * @private */ _parsePopulate(expr) { - const paths = this.paths; - let arr, path, ref; + const { paths } = this; + const arr = []; if (typeof expr === 'string') { const split = expr.split(' '); - arr = []; for (let i = 0, len = split.length; i < len; i++) { - arr.push({ - path: split[i] - }); + arr[i] = { path: split[i] }; } - } else if (isArray(expr)) { - arr = []; + } else if (Array.isArray(expr)) { for (let i = 0, len = expr.length; i < len; i++) { const item = expr[i]; - if (typeof item === 'string') { - arr.push({ - path: item - }); - } else { - arr.push(item); - } + arr[i] = typeof item === 'string' ? { path: item } : item; } } else { - arr = [expr]; + arr[0] = expr; } for (let i = 0, len = arr.length; i < len; i++) { @@ -569,14 +742,14 @@ class Schema { } if (!item.model) { - path = paths[key]; - ref = path.child ? path.child.options.ref : path.options.ref; + const path = paths[key]; + const ref = path.child ? path.child.options.ref : path.options.ref; - if (ref) { - item.model = ref; - } else { + if (!ref) { throw new PopulationError('model is required'); } + + item.model = ref; } } @@ -584,129 +757,6 @@ class Schema { } } -function getSchemaType(name, options) { - const Type = options.type || options; - const typeName = Type.name; - - if (builtinTypes[typeName]) { - return new Types[typeName](name, options); - } - - return new Type(name, options); -} - -function checkHookType(type) { - if (type !== 'save' && type !== 'remove') { - throw new TypeError('Hook type must be `save` or `remove`!'); - } -} - -function hookWrapper(fn) { - if (fn.length > 1) { - return Promise.promisify(fn); - } - - return Promise.method(fn); -} - -function updateStackNormal(key, update) { - return data => { - setProp(data, key, update); - }; -} - -function 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); - }; -} - -function queryStackNormal(path_, key, query) { - const path = path_ || new SchemaType(key); - - return data => path.match(getProp(data, key), query, data); -} - -function queryStackOperator(path_, qkey, key, query) { - const path = path_ || new SchemaType(key); - - return data => path[qkey](getProp(data, key), query, data); -} - -function execQueryStack(stack) { - const len = stack.length; - - return data => { - for (let i = 0; i < len; i++) { - if (!stack[i](data)) return false; - } - - return true; - }; -} - -function $or(stack) { - const len = stack.length; - - return data => { - for (let i = 0; i < len; i++) { - if (stack[i](data)) return true; - } - - return false; - }; -} - -function $nor(stack) { - const len = stack.length; - - return data => { - for (let i = 0; i < len; i++) { - if (stack[i](data)) return false; - } - - return true; - }; -} - -function $not(stack) { - const fn = execQueryStack(stack); - - return data => !fn(data); -} - -function $where(fn) { - return data => fn.call(data); -} - -function 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; - }; -} - -function 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; - }; -} - Schema.Types = Schema.prototype.Types = Types; module.exports = Schema;