schema.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795
  1. 'use strict';
  2. const SchemaType = require('./schematype');
  3. const Types = require('./types');
  4. const Promise = require('bluebird');
  5. const { getProp, setProp, delProp } = require('./util');
  6. const PopulationError = require('./error/population');
  7. const { isPlainObject } = require('is-plain-object');
  8. /**
  9. * @callback queryFilterCallback
  10. * @param {*} data
  11. * @return {boolean}
  12. */
  13. /**
  14. * @callback queryCallback
  15. * @param {*} data
  16. * @return {void}
  17. */
  18. /**
  19. * @callback queryParseCallback
  20. * @param {*} a
  21. * @param {*} b
  22. * @returns {*}
  23. */
  24. /**
  25. * @typedef PopulateResult
  26. * @property {string} path
  27. * @property {*} model
  28. */
  29. const builtinTypes = new Set(['String', 'Number', 'Boolean', 'Array', 'Object', 'Date', 'Buffer']);
  30. const getSchemaType = (name, options) => {
  31. const Type = options.type || options;
  32. const typeName = Type.name;
  33. if (builtinTypes.has(typeName)) {
  34. return new Types[typeName](name, options);
  35. }
  36. return new Type(name, options);
  37. };
  38. const checkHookType = type => {
  39. if (type !== 'save' && type !== 'remove') {
  40. throw new TypeError('Hook type must be `save` or `remove`!');
  41. }
  42. };
  43. const hookWrapper = fn => {
  44. if (fn.length > 1) {
  45. return Promise.promisify(fn);
  46. }
  47. return Promise.method(fn);
  48. };
  49. /**
  50. * @param {Function[]} stack
  51. */
  52. const execSortStack = stack => {
  53. const len = stack.length;
  54. return (a, b) => {
  55. let result;
  56. for (let i = 0; i < len; i++) {
  57. result = stack[i](a, b);
  58. if (result) break;
  59. }
  60. return result;
  61. };
  62. };
  63. const sortStack = (path_, key, sort) => {
  64. const path = path_ || new SchemaType(key);
  65. const descending = sort === 'desc' || sort === -1;
  66. return (a, b) => {
  67. const result = path.compare(getProp(a, key), getProp(b, key));
  68. return descending && result ? result * -1 : result;
  69. };
  70. };
  71. class UpdateParser {
  72. static updateStackNormal(key, update) {
  73. return data => { setProp(data, key, update); };
  74. }
  75. static updateStackOperator(path_, ukey, key, update) {
  76. const path = path_ || new SchemaType(key);
  77. return data => {
  78. const result = path[ukey](getProp(data, key), update, data);
  79. setProp(data, key, result);
  80. };
  81. }
  82. constructor(paths) {
  83. this.paths = paths;
  84. }
  85. /**
  86. * Parses updating expressions and returns a stack.
  87. *
  88. * @param {Object} updates
  89. * @param {queryCallback[]} [stack]
  90. * @private
  91. */
  92. parseUpdate(updates, prefix = '', stack = []) {
  93. const { paths } = this;
  94. const { updateStackOperator } = UpdateParser;
  95. const keys = Object.keys(updates);
  96. let path, prefixNoDot;
  97. if (prefix) {
  98. prefixNoDot = prefix.substring(0, prefix.length - 1);
  99. path = paths[prefixNoDot];
  100. }
  101. for (let i = 0, len = keys.length; i < len; i++) {
  102. const key = keys[i];
  103. const update = updates[key];
  104. const name = prefix + key;
  105. // Update operators
  106. if (key[0] === '$') {
  107. const ukey = `u${key}`;
  108. // First-class update operators
  109. if (prefix) {
  110. stack.push(updateStackOperator(path, ukey, prefixNoDot, update));
  111. } else { // Inline update operators
  112. const fields = Object.keys(update);
  113. const fieldLen = fields.length;
  114. for (let j = 0; j < fieldLen; j++) {
  115. const field = fields[i];
  116. stack.push(updateStackOperator(paths[field], ukey, field, update[field]));
  117. }
  118. }
  119. } else if (isPlainObject(update)) {
  120. this.parseUpdate(update, `${name}.`, stack);
  121. } else {
  122. stack.push(UpdateParser.updateStackNormal(name, update));
  123. }
  124. }
  125. return stack;
  126. }
  127. }
  128. /**
  129. * @private
  130. */
  131. class QueryParser {
  132. constructor(paths) {
  133. this.paths = paths;
  134. }
  135. /**
  136. *
  137. * @param {string} name
  138. * @param {*} query
  139. * @return {queryFilterCallback}
  140. */
  141. queryStackNormal(name, query) {
  142. const path = this.paths[name] || new SchemaType(name);
  143. return data => path.match(getProp(data, name), query, data);
  144. }
  145. /**
  146. *
  147. * @param {string} qkey
  148. * @param {string} name
  149. * @param {*} query
  150. * @return {queryFilterCallback}
  151. */
  152. queryStackOperator(qkey, name, query) {
  153. const path = this.paths[name] || new SchemaType(name);
  154. return data => path[qkey](getProp(data, name), query, data);
  155. }
  156. /**
  157. * @param {Array} arr
  158. * @param {queryFilterCallback[]} stack The function generated by query is added to the stack.
  159. * @return {void}
  160. * @private
  161. */
  162. $and(arr, stack) {
  163. for (let i = 0, len = arr.length; i < len; i++) {
  164. stack.push(this.execQuery(arr[i]));
  165. }
  166. }
  167. /**
  168. * @param {Array} query
  169. * @return {queryFilterCallback}
  170. * @private
  171. */
  172. $or(query) {
  173. const stack = this.parseQueryArray(query);
  174. const len = stack.length;
  175. return data => {
  176. for (let i = 0; i < len; i++) {
  177. if (stack[i](data)) return true;
  178. }
  179. return false;
  180. };
  181. }
  182. /**
  183. * @param {Array} query
  184. * @return {queryFilterCallback}
  185. * @private
  186. */
  187. $nor(query) {
  188. const stack = this.parseQueryArray(query);
  189. const len = stack.length;
  190. return data => {
  191. for (let i = 0; i < len; i++) {
  192. if (stack[i](data)) return false;
  193. }
  194. return true;
  195. };
  196. }
  197. /**
  198. * @param {*} query
  199. * @return {queryFilterCallback}
  200. * @private
  201. */
  202. $not(query) {
  203. const stack = this.parseQuery(query);
  204. const len = stack.length;
  205. return data => {
  206. for (let i = 0; i < len; i++) {
  207. if (!stack[i](data)) return true;
  208. }
  209. return false;
  210. };
  211. }
  212. /**
  213. * @callback queryWherecallback
  214. * @return {boolean}
  215. * @this {QueryPerser}
  216. */
  217. /**
  218. * @param {queryWherecallback} fn
  219. * @return {queryFilterCallback}
  220. * @private
  221. */
  222. $where(fn) {
  223. return data => Reflect.apply(fn, data, []);
  224. }
  225. /**
  226. * Parses array of query expressions and returns a stack.
  227. *
  228. * @param {Array} arr
  229. * @return {queryFilterCallback[]}
  230. * @private
  231. */
  232. parseQueryArray(arr) {
  233. const stack = [];
  234. this.$and(arr, stack);
  235. return stack;
  236. }
  237. /**
  238. * Parses normal query expressions and returns a stack.
  239. *
  240. * @param {Object} queries
  241. * @param {String} prefix
  242. * @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.
  243. * @return {void}
  244. * @private
  245. */
  246. parseNormalQuery(queries, prefix, stack = []) {
  247. const keys = Object.keys(queries);
  248. for (let i = 0, len = keys.length; i < len; i++) {
  249. const key = keys[i];
  250. const query = queries[key];
  251. if (key[0] === '$') {
  252. stack.push(this.queryStackOperator(`q${key}`, prefix, query));
  253. continue;
  254. }
  255. const name = `${prefix}.${key}`;
  256. if (isPlainObject(query)) {
  257. this.parseNormalQuery(query, name, stack);
  258. } else {
  259. stack.push(this.queryStackNormal(name, query));
  260. }
  261. }
  262. }
  263. /**
  264. * Parses query expressions and returns a stack.
  265. *
  266. * @param {Object} queries
  267. * @return {queryFilterCallback[]}
  268. * @private
  269. */
  270. parseQuery(queries) {
  271. /** @type {queryFilterCallback[]} */
  272. const stack = [];
  273. const keys = Object.keys(queries);
  274. for (let i = 0, len = keys.length; i < len; i++) {
  275. const key = keys[i];
  276. const query = queries[key];
  277. switch (key) {
  278. case '$and':
  279. this.$and(query, stack);
  280. break;
  281. case '$or':
  282. stack.push(this.$or(query));
  283. break;
  284. case '$nor':
  285. stack.push(this.$nor(query));
  286. break;
  287. case '$not':
  288. stack.push(this.$not(query));
  289. break;
  290. case '$where':
  291. stack.push(this.$where(query));
  292. break;
  293. default:
  294. if (isPlainObject(query)) {
  295. this.parseNormalQuery(query, key, stack);
  296. } else {
  297. stack.push(this.queryStackNormal(key, query));
  298. }
  299. }
  300. }
  301. return stack;
  302. }
  303. /**
  304. * Returns a function for querying.
  305. *
  306. * @param {Object} query
  307. * @return {queryFilterCallback}
  308. * @private
  309. */
  310. execQuery(query) {
  311. const stack = this.parseQuery(query);
  312. const len = stack.length;
  313. return data => {
  314. for (let i = 0; i < len; i++) {
  315. if (!stack[i](data)) return false;
  316. }
  317. return true;
  318. };
  319. }
  320. }
  321. class Schema {
  322. /**
  323. * Schema constructor.
  324. *
  325. * @param {Object} schema
  326. */
  327. constructor(schema) {
  328. this.paths = {};
  329. this.statics = {};
  330. this.methods = {};
  331. this.hooks = {
  332. pre: {
  333. save: [],
  334. remove: []
  335. },
  336. post: {
  337. save: [],
  338. remove: []
  339. }
  340. };
  341. this.stacks = {
  342. getter: [],
  343. setter: [],
  344. import: [],
  345. export: []
  346. };
  347. if (schema) {
  348. this.add(schema);
  349. }
  350. }
  351. /**
  352. * Adds paths.
  353. *
  354. * @param {Object} schema
  355. * @param {String} prefix
  356. */
  357. add(schema, prefix = '') {
  358. const keys = Object.keys(schema);
  359. const len = keys.length;
  360. if (!len) return;
  361. for (let i = 0; i < len; i++) {
  362. const key = keys[i];
  363. const value = schema[key];
  364. this.path(prefix + key, value);
  365. }
  366. }
  367. /**
  368. * Gets/Sets a path.
  369. *
  370. * @param {String} name
  371. * @param {*} obj
  372. * @return {SchemaType | undefined}
  373. */
  374. path(name, obj) {
  375. if (obj == null) {
  376. return this.paths[name];
  377. }
  378. let type;
  379. let nested = false;
  380. if (obj instanceof SchemaType) {
  381. type = obj;
  382. } else {
  383. switch (typeof obj) {
  384. case 'function':
  385. type = getSchemaType(name, {type: obj});
  386. break;
  387. case 'object':
  388. if (obj.type) {
  389. type = getSchemaType(name, obj);
  390. } else if (Array.isArray(obj)) {
  391. type = new Types.Array(name, {
  392. child: obj.length ? getSchemaType(name, obj[0]) : new SchemaType(name)
  393. });
  394. } else {
  395. type = new Types.Object();
  396. nested = Object.keys(obj).length > 0;
  397. }
  398. break;
  399. default:
  400. throw new TypeError(`Invalid value for schema path \`${name}\``);
  401. }
  402. }
  403. this.paths[name] = type;
  404. this._updateStack(name, type);
  405. if (nested) this.add(obj, `${name}.`);
  406. }
  407. /**
  408. * Updates cache stacks.
  409. *
  410. * @param {String} name
  411. * @param {SchemaType} type
  412. * @private
  413. */
  414. _updateStack(name, type) {
  415. const { stacks } = this;
  416. stacks.getter.push(data => {
  417. const value = getProp(data, name);
  418. const result = type.cast(value, data);
  419. if (result !== undefined) {
  420. setProp(data, name, result);
  421. }
  422. });
  423. stacks.setter.push(data => {
  424. const value = getProp(data, name);
  425. const result = type.validate(value, data);
  426. if (result !== undefined) {
  427. setProp(data, name, result);
  428. } else {
  429. delProp(data, name);
  430. }
  431. });
  432. stacks.import.push(data => {
  433. const value = getProp(data, name);
  434. const result = type.parse(value, data);
  435. if (result !== undefined) {
  436. setProp(data, name, result);
  437. }
  438. });
  439. stacks.export.push(data => {
  440. const value = getProp(data, name);
  441. const result = type.value(value, data);
  442. if (result !== undefined) {
  443. setProp(data, name, result);
  444. } else {
  445. delProp(data, name);
  446. }
  447. });
  448. }
  449. /**
  450. * Adds a virtual path.
  451. *
  452. * @param {String} name
  453. * @param {Function} [getter]
  454. * @return {SchemaType.Virtual}
  455. */
  456. virtual(name, getter) {
  457. const virtual = new Types.Virtual(name, {});
  458. if (getter) virtual.get(getter);
  459. this.path(name, virtual);
  460. return virtual;
  461. }
  462. /**
  463. * Adds a pre-hook.
  464. *
  465. * @param {String} type Hook type. One of `save` or `remove`.
  466. * @param {Function} fn
  467. */
  468. pre(type, fn) {
  469. checkHookType(type);
  470. if (typeof fn !== 'function') throw new TypeError('Hook must be a function!');
  471. this.hooks.pre[type].push(hookWrapper(fn));
  472. }
  473. /**
  474. * Adds a post-hook.
  475. *
  476. * @param {String} type Hook type. One of `save` or `remove`.
  477. * @param {Function} fn
  478. */
  479. post(type, fn) {
  480. checkHookType(type);
  481. if (typeof fn !== 'function') throw new TypeError('Hook must be a function!');
  482. this.hooks.post[type].push(hookWrapper(fn));
  483. }
  484. /**
  485. * Adds a instance method.
  486. *
  487. * @param {String} name
  488. * @param {Function} fn
  489. */
  490. method(name, fn) {
  491. if (!name) throw new TypeError('Method name is required!');
  492. if (typeof fn !== 'function') {
  493. throw new TypeError('Instance method must be a function!');
  494. }
  495. this.methods[name] = fn;
  496. }
  497. /**
  498. * Adds a static method.
  499. *
  500. * @param {String} name
  501. * @param {Function} fn
  502. */
  503. static(name, fn) {
  504. if (!name) throw new TypeError('Method name is required!');
  505. if (typeof fn !== 'function') {
  506. throw new TypeError('Static method must be a function!');
  507. }
  508. this.statics[name] = fn;
  509. }
  510. /**
  511. * Apply getters.
  512. *
  513. * @param {Object} data
  514. * @return {void}
  515. * @private
  516. */
  517. _applyGetters(data) {
  518. const stack = this.stacks.getter;
  519. for (let i = 0, len = stack.length; i < len; i++) {
  520. stack[i](data);
  521. }
  522. }
  523. /**
  524. * Apply setters.
  525. *
  526. * @param {Object} data
  527. * @return {void}
  528. * @private
  529. */
  530. _applySetters(data) {
  531. const stack = this.stacks.setter;
  532. for (let i = 0, len = stack.length; i < len; i++) {
  533. stack[i](data);
  534. }
  535. }
  536. /**
  537. * Parses database.
  538. *
  539. * @param {Object} data
  540. * @return {Object}
  541. * @private
  542. */
  543. _parseDatabase(data) {
  544. const stack = this.stacks.import;
  545. for (let i = 0, len = stack.length; i < len; i++) {
  546. stack[i](data);
  547. }
  548. return data;
  549. }
  550. /**
  551. * Exports database.
  552. *
  553. * @param {Object} data
  554. * @return {Object}
  555. * @private
  556. */
  557. _exportDatabase(data) {
  558. const stack = this.stacks.export;
  559. for (let i = 0, len = stack.length; i < len; i++) {
  560. stack[i](data);
  561. }
  562. return data;
  563. }
  564. /**
  565. * Parses updating expressions and returns a stack.
  566. *
  567. * @param {Object} updates
  568. * @return {queryCallback[]}
  569. * @private
  570. */
  571. _parseUpdate(updates) {
  572. return new UpdateParser(this.paths).parseUpdate(updates);
  573. }
  574. /**
  575. * Returns a function for querying.
  576. *
  577. * @param {Object} query
  578. * @return {queryFilterCallback}
  579. * @private
  580. */
  581. _execQuery(query) {
  582. return new QueryParser(this.paths).execQuery(query);
  583. }
  584. /**
  585. * Parses sorting expressions and returns a stack.
  586. *
  587. * @param {Object} sorts
  588. * @param {string} [prefix]
  589. * @param {queryParseCallback[]} [stack]
  590. * @return {queryParseCallback[]}
  591. * @private
  592. */
  593. _parseSort(sorts, prefix = '', stack = []) {
  594. const { paths } = this;
  595. const keys = Object.keys(sorts);
  596. for (let i = 0, len = keys.length; i < len; i++) {
  597. const key = keys[i];
  598. const sort = sorts[key];
  599. const name = prefix + key;
  600. if (typeof sort === 'object') {
  601. this._parseSort(sort, `${name}.`, stack);
  602. } else {
  603. stack.push(sortStack(paths[name], name, sort));
  604. }
  605. }
  606. return stack;
  607. }
  608. /**
  609. * Returns a function for sorting.
  610. *
  611. * @param {Object} sorts
  612. * @return {queryParseCallback}
  613. * @private
  614. */
  615. _execSort(sorts) {
  616. const stack = this._parseSort(sorts);
  617. return execSortStack(stack);
  618. }
  619. /**
  620. * Parses population expression and returns a stack.
  621. *
  622. * @param {String|Object} expr
  623. * @return {PopulateResult[]}
  624. * @private
  625. */
  626. _parsePopulate(expr) {
  627. const { paths } = this;
  628. const arr = [];
  629. if (typeof expr === 'string') {
  630. const split = expr.split(' ');
  631. for (let i = 0, len = split.length; i < len; i++) {
  632. arr[i] = { path: split[i] };
  633. }
  634. } else if (Array.isArray(expr)) {
  635. for (let i = 0, len = expr.length; i < len; i++) {
  636. const item = expr[i];
  637. arr[i] = typeof item === 'string' ? { path: item } : item;
  638. }
  639. } else {
  640. arr[0] = expr;
  641. }
  642. for (let i = 0, len = arr.length; i < len; i++) {
  643. const item = arr[i];
  644. const key = item.path;
  645. if (!key) {
  646. throw new PopulationError('path is required');
  647. }
  648. if (!item.model) {
  649. const path = paths[key];
  650. const ref = path.child ? path.child.options.ref : path.options.ref;
  651. if (!ref) {
  652. throw new PopulationError('model is required');
  653. }
  654. item.model = ref;
  655. }
  656. }
  657. return arr;
  658. }
  659. }
  660. Schema.prototype.Types = Types;
  661. Schema.Types = Schema.prototype.Types;
  662. module.exports = Schema;