schema.js 14 KB

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