1 | 'use strict';
|
2 |
|
3 | const { EventEmitter } = require('events');
|
4 | const cloneDeep = require('rfdc')();
|
5 | const Promise = require('bluebird');
|
6 | const { parseArgs, getProp, setGetter, shuffle } = require('./util');
|
7 | const Document = require('./document');
|
8 | const Query = require('./query');
|
9 | const Schema = require('./schema');
|
10 | const Types = require('./types');
|
11 | const WarehouseError = require('./error');
|
12 | const PopulationError = require('./error/population');
|
13 | const Mutex = require('./mutex');
|
14 |
|
15 | class Model extends EventEmitter {
|
16 |
|
17 | |
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 | constructor(name, schema_) {
|
24 | super();
|
25 |
|
26 | let schema;
|
27 |
|
28 |
|
29 | if (schema_ instanceof Schema) {
|
30 | schema = schema_;
|
31 | } else if (typeof schema_ === 'object') {
|
32 | schema = new Schema(schema_);
|
33 | } else {
|
34 | schema = new Schema();
|
35 | }
|
36 |
|
37 |
|
38 | if (!schema.path('_id')) {
|
39 | schema.path('_id', {type: Types.CUID, required: true});
|
40 | }
|
41 |
|
42 | this.name = name;
|
43 | this.data = {};
|
44 | this._mutex = new Mutex();
|
45 | this.schema = schema;
|
46 | this.length = 0;
|
47 |
|
48 | class _Document extends Document {
|
49 | constructor(data) {
|
50 | super(data);
|
51 |
|
52 |
|
53 | schema._applyGetters(this);
|
54 | }
|
55 | }
|
56 |
|
57 | this.Document = _Document;
|
58 |
|
59 | _Document.prototype._model = this;
|
60 | _Document.prototype._schema = schema;
|
61 |
|
62 | class _Query extends Query {}
|
63 |
|
64 | this.Query = _Query;
|
65 |
|
66 | _Query.prototype._model = this;
|
67 | _Query.prototype._schema = schema;
|
68 |
|
69 |
|
70 | Object.assign(this, schema.statics);
|
71 |
|
72 |
|
73 | Object.assign(_Document.prototype, schema.methods);
|
74 | }
|
75 |
|
76 | |
77 |
|
78 |
|
79 |
|
80 |
|
81 |
|
82 | new(data) {
|
83 | return new this.Document(data);
|
84 | }
|
85 |
|
86 | |
87 |
|
88 |
|
89 |
|
90 |
|
91 |
|
92 |
|
93 |
|
94 | findById(id, options_) {
|
95 | const raw = this.data[id];
|
96 | if (!raw) return;
|
97 |
|
98 | const options = Object.assign({
|
99 | lean: false
|
100 | }, options_);
|
101 |
|
102 | const data = cloneDeep(raw);
|
103 | return options.lean ? data : this.new(data);
|
104 | }
|
105 |
|
106 | |
107 |
|
108 |
|
109 |
|
110 |
|
111 |
|
112 | has(id) {
|
113 | return Boolean(this.data[id]);
|
114 | }
|
115 |
|
116 | |
117 |
|
118 |
|
119 |
|
120 |
|
121 |
|
122 |
|
123 | _acquireWriteLock(id) {
|
124 | const mutex = this._mutex;
|
125 |
|
126 | return new Promise((resolve, reject) => {
|
127 | mutex.lock(resolve);
|
128 | }).disposer(() => {
|
129 | mutex.unlock();
|
130 | });
|
131 | }
|
132 |
|
133 | |
134 |
|
135 |
|
136 |
|
137 |
|
138 |
|
139 |
|
140 | _insertOne(data_) {
|
141 | const schema = this.schema;
|
142 |
|
143 |
|
144 | const data = data_ instanceof this.Document ? data_ : this.new(data_);
|
145 | const id = data._id;
|
146 |
|
147 |
|
148 | if (!id) {
|
149 | return Promise.reject(new WarehouseError('ID is not defined', WarehouseError.ID_UNDEFINED));
|
150 | }
|
151 |
|
152 | if (this.has(id)) {
|
153 | return Promise.reject(new WarehouseError('ID `' + id + '` has been used', WarehouseError.ID_EXIST));
|
154 | }
|
155 |
|
156 |
|
157 | const result = data.toObject();
|
158 | schema._applySetters(result);
|
159 |
|
160 |
|
161 | return execHooks(schema, 'pre', 'save', data).then(data => {
|
162 |
|
163 | this.data[id] = result;
|
164 | this.length++;
|
165 |
|
166 | this.emit('insert', data);
|
167 | return execHooks(schema, 'post', 'save', data);
|
168 | });
|
169 | }
|
170 |
|
171 | |
172 |
|
173 |
|
174 |
|
175 |
|
176 |
|
177 |
|
178 | insertOne(data, callback) {
|
179 | return Promise.using(this._acquireWriteLock(), () => this._insertOne(data)).asCallback(callback);
|
180 | }
|
181 |
|
182 | |
183 |
|
184 |
|
185 |
|
186 |
|
187 |
|
188 |
|
189 | insert(data, callback) {
|
190 | if (Array.isArray(data)) {
|
191 | return Promise.mapSeries(data, item => this.insertOne(item)).asCallback(callback);
|
192 | }
|
193 |
|
194 | return this.insertOne(data, callback);
|
195 | }
|
196 |
|
197 | |
198 |
|
199 |
|
200 |
|
201 |
|
202 |
|
203 |
|
204 | save(data, callback) {
|
205 | const id = data._id;
|
206 |
|
207 | if (!id) return this.insertOne(data, callback);
|
208 |
|
209 | return Promise.using(this._acquireWriteLock(), () => {
|
210 | if (this.has(id)) {
|
211 | return this._replaceById(id, data);
|
212 | }
|
213 |
|
214 | return this._insertOne(data);
|
215 | }).asCallback(callback);
|
216 | }
|
217 |
|
218 | |
219 |
|
220 |
|
221 |
|
222 |
|
223 |
|
224 |
|
225 |
|
226 | _updateWithStack(id, stack) {
|
227 | const schema = this.schema;
|
228 |
|
229 | const data = this.data[id];
|
230 |
|
231 | if (!data) {
|
232 | return Promise.reject(new WarehouseError('ID `' + id + '` does not exist', WarehouseError.ID_NOT_EXIST));
|
233 | }
|
234 |
|
235 |
|
236 | let result = cloneDeep(data);
|
237 |
|
238 |
|
239 | for (let i = 0, len = stack.length; i < len; i++) {
|
240 | stack[i](result);
|
241 | }
|
242 |
|
243 |
|
244 | const doc = this.new(result);
|
245 |
|
246 |
|
247 | result = doc.toObject();
|
248 | schema._applySetters(result);
|
249 |
|
250 |
|
251 | return execHooks(schema, 'pre', 'save', doc).then(data => {
|
252 |
|
253 | this.data[id] = result;
|
254 |
|
255 | this.emit('update', data);
|
256 | return execHooks(schema, 'post', 'save', data);
|
257 | });
|
258 | }
|
259 |
|
260 | |
261 |
|
262 |
|
263 |
|
264 |
|
265 |
|
266 |
|
267 |
|
268 | updateById(id, update, callback) {
|
269 | return Promise.using(this._acquireWriteLock(), () => {
|
270 | const stack = this.schema._parseUpdate(update);
|
271 | return this._updateWithStack(id, stack);
|
272 | }).asCallback(callback);
|
273 | }
|
274 |
|
275 | |
276 |
|
277 |
|
278 |
|
279 |
|
280 |
|
281 |
|
282 |
|
283 | update(query, data, callback) {
|
284 | return this.find(query).update(data, callback);
|
285 | }
|
286 |
|
287 | |
288 |
|
289 |
|
290 |
|
291 |
|
292 |
|
293 |
|
294 |
|
295 | _replaceById(id, data_) {
|
296 | const schema = this.schema;
|
297 |
|
298 | if (!this.has(id)) {
|
299 | return Promise.reject(new WarehouseError('ID `' + id + '` does not exist', WarehouseError.ID_NOT_EXIST));
|
300 | }
|
301 |
|
302 | data_._id = id;
|
303 |
|
304 |
|
305 | const data = data_ instanceof this.Document ? data_ : this.new(data_);
|
306 |
|
307 |
|
308 | const result = data.toObject();
|
309 | schema._applySetters(result);
|
310 |
|
311 |
|
312 | return execHooks(schema, 'pre', 'save', data).then(data => {
|
313 |
|
314 | this.data[id] = result;
|
315 |
|
316 | this.emit('update', data);
|
317 | return execHooks(schema, 'post', 'save', data);
|
318 | });
|
319 | }
|
320 |
|
321 | |
322 |
|
323 |
|
324 |
|
325 |
|
326 |
|
327 |
|
328 |
|
329 | replaceById(id, data, callback) {
|
330 | return Promise.using(this._acquireWriteLock(), () => this._replaceById(id, data)).asCallback(callback);
|
331 | }
|
332 |
|
333 | |
334 |
|
335 |
|
336 |
|
337 |
|
338 |
|
339 |
|
340 |
|
341 | replace(query, data, callback) {
|
342 | return this.find(query).replace(data, callback);
|
343 | }
|
344 |
|
345 | |
346 |
|
347 |
|
348 |
|
349 |
|
350 |
|
351 |
|
352 |
|
353 | _removeById(id) {
|
354 | const schema = this.schema;
|
355 |
|
356 | const data = this.data[id];
|
357 |
|
358 | if (!data) {
|
359 | return Promise.reject(new WarehouseError('ID `' + id + '` does not exist', WarehouseError.ID_NOT_EXIST));
|
360 | }
|
361 |
|
362 |
|
363 | return execHooks(schema, 'pre', 'remove', data).then(data => {
|
364 |
|
365 | this.data[id] = null;
|
366 | this.length--;
|
367 |
|
368 | this.emit('remove', data);
|
369 | return execHooks(schema, 'post', 'remove', data);
|
370 | });
|
371 | }
|
372 |
|
373 | |
374 |
|
375 |
|
376 |
|
377 |
|
378 |
|
379 |
|
380 | removeById(id, callback) {
|
381 | return Promise.using(this._acquireWriteLock(), () => this._removeById(id)).asCallback(callback);
|
382 | }
|
383 |
|
384 | |
385 |
|
386 |
|
387 |
|
388 |
|
389 |
|
390 |
|
391 | remove(query, callback) {
|
392 | return this.find(query).remove(callback);
|
393 | }
|
394 |
|
395 | |
396 |
|
397 |
|
398 | destroy() {
|
399 | this._database._models[this.name] = null;
|
400 | }
|
401 |
|
402 | |
403 |
|
404 |
|
405 |
|
406 |
|
407 | count() {
|
408 | return this.length;
|
409 | }
|
410 |
|
411 | |
412 |
|
413 |
|
414 |
|
415 |
|
416 |
|
417 | forEach(iterator, options) {
|
418 | const keys = Object.keys(this.data);
|
419 | let num = 0;
|
420 |
|
421 | for (let i = 0, len = keys.length; i < len; i++) {
|
422 | const data = this.findById(keys[i], options);
|
423 | if (data) iterator(data, num++);
|
424 | }
|
425 | }
|
426 |
|
427 | |
428 |
|
429 |
|
430 |
|
431 |
|
432 |
|
433 | toArray(options) {
|
434 | const result = new Array(this.length);
|
435 |
|
436 | this.forEach((item, i) => {
|
437 | result[i] = item;
|
438 | }, options);
|
439 |
|
440 | return result;
|
441 | }
|
442 |
|
443 | |
444 |
|
445 |
|
446 |
|
447 |
|
448 |
|
449 |
|
450 |
|
451 |
|
452 |
|
453 | find(query, options_) {
|
454 | const options = options_ || {};
|
455 | const filter = this.schema._execQuery(query);
|
456 | const keys = Object.keys(this.data);
|
457 | const len = keys.length;
|
458 | let limit = options.limit || this.length;
|
459 | let skip = options.skip;
|
460 | const data = this.data;
|
461 | const arr = [];
|
462 |
|
463 | for (let i = 0; limit && i < len; i++) {
|
464 | const key = keys[i];
|
465 | const item = data[key];
|
466 |
|
467 | if (item && filter(item)) {
|
468 | if (skip) {
|
469 | skip--;
|
470 | } else {
|
471 | arr.push(this.findById(key, options));
|
472 | limit--;
|
473 | }
|
474 | }
|
475 | }
|
476 |
|
477 | return options.lean ? arr : new this.Query(arr);
|
478 | }
|
479 |
|
480 | |
481 |
|
482 |
|
483 |
|
484 |
|
485 |
|
486 |
|
487 |
|
488 |
|
489 | findOne(query, options_) {
|
490 | const options = options_ || {};
|
491 | options.limit = 1;
|
492 |
|
493 | const result = this.find(query, options);
|
494 | return options.lean ? result[0] : result.data[0];
|
495 | }
|
496 |
|
497 | |
498 |
|
499 |
|
500 |
|
501 |
|
502 |
|
503 |
|
504 | sort(orderby, order) {
|
505 | const sort = parseArgs(orderby, order);
|
506 | const fn = this.schema._execSort(sort);
|
507 |
|
508 | return new this.Query(this.toArray().sort(fn));
|
509 | }
|
510 |
|
511 | |
512 |
|
513 |
|
514 |
|
515 |
|
516 |
|
517 |
|
518 |
|
519 | eq(i_, options) {
|
520 | let index = i_ < 0 ? this.length + i_ : i_;
|
521 | const data = this.data;
|
522 | const keys = Object.keys(data);
|
523 |
|
524 | for (let i = 0, len = keys.length; i < len; i++) {
|
525 | const key = keys[i];
|
526 | const item = data[key];
|
527 |
|
528 | if (!item) continue;
|
529 |
|
530 | if (index) {
|
531 | index--;
|
532 | } else {
|
533 | return this.findById(key, options);
|
534 | }
|
535 | }
|
536 | }
|
537 |
|
538 | |
539 |
|
540 |
|
541 |
|
542 |
|
543 |
|
544 | first(options) {
|
545 | return this.eq(0, options);
|
546 | }
|
547 |
|
548 | |
549 |
|
550 |
|
551 |
|
552 |
|
553 |
|
554 | last(options) {
|
555 | return this.eq(-1, options);
|
556 | }
|
557 |
|
558 | |
559 |
|
560 |
|
561 |
|
562 |
|
563 |
|
564 |
|
565 | slice(start_, end_) {
|
566 | const total = this.length;
|
567 |
|
568 | let start = start_ | 0;
|
569 | if (start < 0) start += total;
|
570 | if (start > total - 1) return new this.Query([]);
|
571 |
|
572 | let end = end_ | 0 || total;
|
573 | if (end < 0) end += total;
|
574 |
|
575 | let len = start > end ? 0 : end - start;
|
576 | if (len > total) len = total - start;
|
577 | if (!len) return new this.Query([]);
|
578 |
|
579 | const arr = new Array(len);
|
580 | const keys = Object.keys(this.data);
|
581 | const keysLen = keys.length;
|
582 | let num = 0;
|
583 |
|
584 | for (let i = 0; num < len && i < keysLen; i++) {
|
585 | const data = this.findById(keys[i]);
|
586 | if (!data) continue;
|
587 |
|
588 | if (start) {
|
589 | start--;
|
590 | } else {
|
591 | arr[num++] = data;
|
592 | }
|
593 | }
|
594 |
|
595 | return new this.Query(arr);
|
596 | }
|
597 |
|
598 | |
599 |
|
600 |
|
601 |
|
602 |
|
603 |
|
604 | limit(i) {
|
605 | return this.slice(0, i);
|
606 | }
|
607 |
|
608 | |
609 |
|
610 |
|
611 |
|
612 |
|
613 |
|
614 | skip(i) {
|
615 | return this.slice(i);
|
616 | }
|
617 |
|
618 | |
619 |
|
620 |
|
621 |
|
622 |
|
623 | reverse() {
|
624 | return new this.Query(this.toArray().reverse());
|
625 | }
|
626 |
|
627 | |
628 |
|
629 |
|
630 |
|
631 |
|
632 | shuffle() {
|
633 | return new this.Query(shuffle(this.toArray()));
|
634 | }
|
635 |
|
636 | |
637 |
|
638 |
|
639 |
|
640 |
|
641 |
|
642 |
|
643 | map(iterator, options) {
|
644 | const result = new Array(this.length);
|
645 | const keys = Object.keys(this.data);
|
646 | const len = keys.length;
|
647 |
|
648 | for (let i = 0, num = 0; i < len; i++) {
|
649 | const data = this.findById(keys[i], options);
|
650 | if (data) {
|
651 | result[num] = iterator(data, num);
|
652 | num++;
|
653 | }
|
654 | }
|
655 |
|
656 | return result;
|
657 | }
|
658 |
|
659 | |
660 |
|
661 |
|
662 |
|
663 |
|
664 |
|
665 |
|
666 |
|
667 | reduce(iterator, initial) {
|
668 | const arr = this.toArray();
|
669 | const len = this.length;
|
670 | let i, result;
|
671 |
|
672 | if (initial === undefined) {
|
673 | i = 1;
|
674 | result = arr[0];
|
675 | } else {
|
676 | i = 0;
|
677 | result = initial;
|
678 | }
|
679 |
|
680 | for (; i < len; i++) {
|
681 | result = iterator(result, arr[i], i);
|
682 | }
|
683 |
|
684 | return result;
|
685 | }
|
686 |
|
687 | |
688 |
|
689 |
|
690 |
|
691 |
|
692 |
|
693 |
|
694 |
|
695 | reduceRight(iterator, initial) {
|
696 | const arr = this.toArray();
|
697 | const len = this.length;
|
698 | let i, result;
|
699 |
|
700 | if (initial === undefined) {
|
701 | i = len - 2;
|
702 | result = arr[len - 1];
|
703 | } else {
|
704 | i = len - 1;
|
705 | result = initial;
|
706 | }
|
707 |
|
708 | for (; i >= 0; i--) {
|
709 | result = iterator(result, arr[i], i);
|
710 | }
|
711 |
|
712 | return result;
|
713 | }
|
714 |
|
715 | |
716 |
|
717 |
|
718 |
|
719 |
|
720 |
|
721 |
|
722 |
|
723 | filter(iterator, options) {
|
724 | const arr = [];
|
725 |
|
726 | this.forEach((item, i) => {
|
727 | if (iterator(item, i)) arr.push(item);
|
728 | }, options);
|
729 |
|
730 | return new this.Query(arr);
|
731 | }
|
732 |
|
733 | |
734 |
|
735 |
|
736 |
|
737 |
|
738 |
|
739 |
|
740 | every(iterator) {
|
741 | const keys = Object.keys(this.data);
|
742 | const len = keys.length;
|
743 | let num = 0;
|
744 |
|
745 | if (!len) return true;
|
746 |
|
747 | for (let i = 0; i < len; i++) {
|
748 | const data = this.findById(keys[i]);
|
749 |
|
750 | if (data) {
|
751 | if (!iterator(data, num++)) return false;
|
752 | }
|
753 | }
|
754 |
|
755 | return true;
|
756 | }
|
757 |
|
758 | |
759 |
|
760 |
|
761 |
|
762 |
|
763 |
|
764 |
|
765 | some(iterator) {
|
766 | const keys = Object.keys(this.data);
|
767 | const len = keys.length;
|
768 | let num = 0;
|
769 |
|
770 | if (!len) return false;
|
771 |
|
772 | for (let i = 0; i < len; i++) {
|
773 | const data = this.findById(keys[i]);
|
774 |
|
775 | if (data) {
|
776 | if (iterator(data, num++)) return true;
|
777 | }
|
778 | }
|
779 |
|
780 | return false;
|
781 | }
|
782 |
|
783 | |
784 |
|
785 |
|
786 |
|
787 |
|
788 |
|
789 |
|
790 |
|
791 |
|
792 | _populateGetter(data, model, options) {
|
793 | let hasCache = false;
|
794 | let cache;
|
795 |
|
796 | return () => {
|
797 | if (!hasCache) {
|
798 | cache = model.findById(data);
|
799 | hasCache = true;
|
800 | }
|
801 |
|
802 | return cache;
|
803 | };
|
804 | }
|
805 |
|
806 | |
807 |
|
808 |
|
809 |
|
810 |
|
811 |
|
812 |
|
813 |
|
814 |
|
815 | _populateGetterArray(data, model, options) {
|
816 | const Query = model.Query;
|
817 | let hasCache = false;
|
818 | let cache;
|
819 |
|
820 | return () => {
|
821 | if (!hasCache) {
|
822 | let arr = [];
|
823 |
|
824 | for (let i = 0, len = data.length; i < len; i++) {
|
825 | arr.push(model.findById(data[i]));
|
826 | }
|
827 |
|
828 | if (options.match) {
|
829 | cache = new Query(arr).find(options.match, options);
|
830 | } else if (options.skip) {
|
831 | if (options.limit) {
|
832 | arr = arr.slice(options.skip, options.skip + options.limit);
|
833 | } else {
|
834 | arr = arr.slice(options.skip);
|
835 | }
|
836 |
|
837 | cache = new Query(arr);
|
838 | } else if (options.limit) {
|
839 | cache = new Query(arr.slice(0, options.limit));
|
840 | } else {
|
841 | cache = new Query(arr);
|
842 | }
|
843 |
|
844 | if (options.sort) {
|
845 | cache = cache.sort(options.sort);
|
846 | }
|
847 |
|
848 | hasCache = true;
|
849 | }
|
850 |
|
851 | return cache;
|
852 | };
|
853 | }
|
854 |
|
855 | |
856 |
|
857 |
|
858 |
|
859 |
|
860 |
|
861 |
|
862 |
|
863 | _populate(data, stack) {
|
864 | const models = this._database._models;
|
865 |
|
866 | for (let i = 0, len = stack.length; i < len; i++) {
|
867 | const item = stack[i];
|
868 | const model = models[item.model];
|
869 |
|
870 | if (!model) {
|
871 | throw new PopulationError('Model `' + item.model + '` does not exist');
|
872 | }
|
873 |
|
874 | const path = item.path;
|
875 | const prop = getProp(data, path);
|
876 |
|
877 | if (Array.isArray(prop)) {
|
878 | setGetter(data, path, this._populateGetterArray(prop, model, item));
|
879 | } else {
|
880 | setGetter(data, path, this._populateGetter(prop, model, item));
|
881 | }
|
882 | }
|
883 |
|
884 | return data;
|
885 | }
|
886 |
|
887 | |
888 |
|
889 |
|
890 |
|
891 |
|
892 |
|
893 | populate(path) {
|
894 | if (!path) throw new TypeError('path is required');
|
895 |
|
896 | const stack = this.schema._parsePopulate(path);
|
897 | const arr = new Array(this.length);
|
898 |
|
899 | this.forEach((item, i) => {
|
900 | arr[i] = this._populate(item, stack);
|
901 | });
|
902 |
|
903 | return new Query(arr);
|
904 | }
|
905 |
|
906 | |
907 |
|
908 |
|
909 |
|
910 |
|
911 |
|
912 | _import(arr) {
|
913 | const len = arr.length;
|
914 | const data = this.data;
|
915 | const schema = this.schema;
|
916 |
|
917 | for (let i = 0; i < len; i++) {
|
918 | const item = arr[i];
|
919 | data[item._id] = schema._parseDatabase(item);
|
920 | }
|
921 |
|
922 | this.length = len;
|
923 | }
|
924 |
|
925 | |
926 |
|
927 |
|
928 |
|
929 |
|
930 |
|
931 | _export() {
|
932 | return JSON.stringify(this.toJSON());
|
933 | }
|
934 |
|
935 | toJSON() {
|
936 | const result = new Array(this.length);
|
937 | const { data, schema } = this;
|
938 | const keys = Object.keys(data);
|
939 | const { length } = keys;
|
940 |
|
941 | for (let i = 0, num = 0; i < length; i++) {
|
942 | const raw = data[keys[i]];
|
943 | if (raw) {
|
944 | result[num++] = schema._exportDatabase(cloneDeep(raw));
|
945 | }
|
946 | }
|
947 | return result;
|
948 | }
|
949 | }
|
950 |
|
951 | Model.prototype.get = Model.prototype.findById;
|
952 |
|
953 | function execHooks(schema, type, event, data) {
|
954 | const hooks = schema.hooks[type][event];
|
955 | if (!hooks.length) return Promise.resolve(data);
|
956 |
|
957 | return Promise.each(hooks, hook => hook(data)).thenReturn(data);
|
958 | }
|
959 |
|
960 | Model.prototype.size = Model.prototype.count;
|
961 |
|
962 | Model.prototype.each = Model.prototype.forEach;
|
963 |
|
964 | Model.prototype.random = Model.prototype.shuffle;
|
965 |
|
966 | module.exports = Model;
|