UNPKG

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