1 | 'use strict';
|
2 |
|
3 | const SchemaType = require('./schematype');
|
4 | const Types = require('./types');
|
5 | const Promise = require('bluebird');
|
6 | const { getProp, setProp, delProp } = require('./util');
|
7 | const PopulationError = require('./error/population');
|
8 | const isPlainObject = require('is-plain-object');
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 | const builtinTypes = new Set(['String', 'Number', 'Boolean', 'Array', 'Object', 'Date', 'Buffer']);
|
36 |
|
37 | const 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 |
|
48 | const checkHookType = type => {
|
49 | if (type !== 'save' && type !== 'remove') {
|
50 | throw new TypeError('Hook type must be `save` or `remove`!');
|
51 | }
|
52 | };
|
53 |
|
54 | const hookWrapper = fn => {
|
55 | if (fn.length > 1) {
|
56 | return Promise.promisify(fn);
|
57 | }
|
58 |
|
59 | return Promise.method(fn);
|
60 | };
|
61 |
|
62 |
|
63 |
|
64 |
|
65 | const 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 |
|
80 | const 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 |
|
90 | class 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 |
|
110 |
|
111 |
|
112 |
|
113 |
|
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 |
|
132 | if (key[0] === '$') {
|
133 | const ukey = `u${key}`;
|
134 |
|
135 |
|
136 | if (prefix) {
|
137 | stack.push(updateStackOperator(path, ukey, prefixNoDot, update));
|
138 | } else {
|
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 |
|
160 |
|
161 | class QueryParser {
|
162 | constructor(paths) {
|
163 | this.paths = paths;
|
164 | }
|
165 |
|
166 | |
167 |
|
168 |
|
169 |
|
170 |
|
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 |
|
181 |
|
182 |
|
183 |
|
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 |
|
193 |
|
194 |
|
195 |
|
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 |
|
205 |
|
206 |
|
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 |
|
223 |
|
224 |
|
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 |
|
241 |
|
242 |
|
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 |
|
259 |
|
260 |
|
261 |
|
262 |
|
263 | |
264 |
|
265 |
|
266 |
|
267 |
|
268 | $where(fn) {
|
269 | return data => Reflect.apply(fn, data, []);
|
270 | }
|
271 |
|
272 | |
273 |
|
274 |
|
275 |
|
276 |
|
277 |
|
278 |
|
279 | parseQueryArray(arr) {
|
280 | const stack = [];
|
281 | this.$and(arr, stack);
|
282 | return stack;
|
283 | }
|
284 |
|
285 | |
286 |
|
287 |
|
288 |
|
289 |
|
290 |
|
291 |
|
292 |
|
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 |
|
317 |
|
318 |
|
319 |
|
320 |
|
321 |
|
322 | parseQuery(queries) {
|
323 |
|
324 |
|
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 |
|
367 |
|
368 |
|
369 |
|
370 |
|
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 |
|
386 | class Schema {
|
387 |
|
388 | |
389 |
|
390 |
|
391 |
|
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 |
|
423 |
|
424 |
|
425 |
|
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 |
|
443 |
|
444 |
|
445 |
|
446 |
|
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 |
|
491 |
|
492 |
|
493 |
|
494 |
|
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 |
|
542 |
|
543 |
|
544 |
|
545 |
|
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 |
|
558 |
|
559 |
|
560 |
|
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 |
|
571 |
|
572 |
|
573 |
|
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 |
|
584 |
|
585 |
|
586 |
|
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 |
|
600 |
|
601 |
|
602 |
|
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 |
|
616 |
|
617 |
|
618 |
|
619 |
|
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 |
|
631 |
|
632 |
|
633 |
|
634 |
|
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 |
|
646 |
|
647 |
|
648 |
|
649 |
|
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 |
|
663 |
|
664 |
|
665 |
|
666 |
|
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 |
|
680 |
|
681 |
|
682 |
|
683 |
|
684 |
|
685 | _parseUpdate(updates) {
|
686 | return new UpdateParser(this.paths).parseUpdate(updates);
|
687 | }
|
688 |
|
689 | |
690 |
|
691 |
|
692 |
|
693 |
|
694 |
|
695 |
|
696 | _execQuery(query) {
|
697 | return new QueryParser(this.paths).execQuery(query);
|
698 | }
|
699 |
|
700 |
|
701 | |
702 |
|
703 |
|
704 |
|
705 |
|
706 |
|
707 |
|
708 |
|
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 |
|
731 |
|
732 |
|
733 |
|
734 |
|
735 |
|
736 | _execSort(sorts) {
|
737 | const stack = this._parseSort(sorts);
|
738 | return execSortStack(stack);
|
739 | }
|
740 |
|
741 | |
742 |
|
743 |
|
744 |
|
745 |
|
746 |
|
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 |
|
792 | Schema.prototype.Types = Types;
|
793 | Schema.Types = Schema.prototype.Types;
|
794 |
|
795 | module.exports = Schema;
|