1 | 'use strict';
|
2 |
|
3 | const _ = require('lodash');
|
4 | const deprecate = require('depd')('camo');
|
5 | const DB = require('./clients').getClient;
|
6 | const isSupportedType = require('./validate').isSupportedType;
|
7 | const isValidType = require('./validate').isValidType;
|
8 | const isEmptyValue = require('./validate').isEmptyValue;
|
9 | const isInChoices = require('./validate').isInChoices;
|
10 | const isArray = require('./validate').isArray;
|
11 | const isDocument = require('./validate').isDocument;
|
12 | const isEmbeddedDocument = require('./validate').isEmbeddedDocument;
|
13 | const isString = require('./validate').isString;
|
14 | const isNumber = require('./validate').isNumber;
|
15 | const isDate = require('./validate').isDate;
|
16 | const ValidationError = require('./errors').ValidationError;
|
17 |
|
18 | const normalizeType = function(property) {
|
19 |
|
20 |
|
21 | let typeDeclaration = {};
|
22 | if (property.type) {
|
23 | typeDeclaration = property;
|
24 | } else if (isSupportedType(property)) {
|
25 | typeDeclaration.type = property;
|
26 | } else {
|
27 | throw new Error('Unsupported type or bad variable. ' +
|
28 | 'Remember, non-persisted objects must start with an underscore (_). Got:', property);
|
29 | }
|
30 |
|
31 | return typeDeclaration;
|
32 | };
|
33 |
|
34 | class BaseDocument {
|
35 | constructor() {
|
36 | this._schema = {
|
37 | _id: { type: DB().nativeIdType() },
|
38 | };
|
39 |
|
40 | this._id = null;
|
41 | }
|
42 |
|
43 |
|
44 |
|
45 |
|
46 | static documentClass() {
|
47 | throw new TypeError('You must override documentClass (static).');
|
48 | }
|
49 |
|
50 | documentClass() {
|
51 | throw new TypeError('You must override documentClass.');
|
52 | }
|
53 |
|
54 | collectionName() {
|
55 |
|
56 |
|
57 | if (this._meta) {
|
58 | return this._meta.collection;
|
59 | }
|
60 |
|
61 | return this.constructor.collectionName();
|
62 | }
|
63 |
|
64 | |
65 |
|
66 |
|
67 |
|
68 |
|
69 | static collectionName() {
|
70 |
|
71 |
|
72 | let instance = new this();
|
73 | if (instance._meta) {
|
74 | return instance._meta.collection;
|
75 | }
|
76 |
|
77 | return this.name.toLowerCase() + 's';
|
78 | }
|
79 |
|
80 | get id() {
|
81 | deprecate('Document.id - use Document._id instead');
|
82 | return this._id;
|
83 | }
|
84 |
|
85 | set id(id) {
|
86 | deprecate('Document.id - use Document._id instead');
|
87 | this._id = id;
|
88 | }
|
89 |
|
90 | |
91 |
|
92 |
|
93 |
|
94 | schema(extension) {
|
95 | const that = this;
|
96 |
|
97 | if (!extension) return;
|
98 | _.keys(extension).forEach(function(k) {
|
99 | that[k] = extension[k];
|
100 | });
|
101 | }
|
102 |
|
103 | |
104 |
|
105 |
|
106 |
|
107 |
|
108 |
|
109 |
|
110 | preValidate() { }
|
111 |
|
112 | postValidate() { }
|
113 |
|
114 | preSave() { }
|
115 |
|
116 | postSave() { }
|
117 |
|
118 | preDelete() { }
|
119 |
|
120 | postDelete() { }
|
121 |
|
122 | |
123 |
|
124 |
|
125 |
|
126 |
|
127 |
|
128 | generateSchema() {
|
129 | const that = this;
|
130 |
|
131 | _.keys(this).forEach(function(k) {
|
132 |
|
133 | if (_.startsWith(k, '_')) {
|
134 | return;
|
135 | }
|
136 |
|
137 |
|
138 | that._schema[k] = normalizeType(that[k]);
|
139 |
|
140 |
|
141 | if (isArray(that._schema[k].type)) {
|
142 | that[k] = that.getDefault(k) || [];
|
143 | } else {
|
144 | that[k] = that.getDefault(k);
|
145 | }
|
146 | });
|
147 | }
|
148 |
|
149 | |
150 |
|
151 |
|
152 |
|
153 |
|
154 |
|
155 |
|
156 |
|
157 | validate() {
|
158 | const that = this;
|
159 |
|
160 | _.keys(that._schema).forEach(function(key) {
|
161 | let value = that[key];
|
162 |
|
163 |
|
164 | if (value !== null && value !== undefined) {
|
165 | if (isEmbeddedDocument(value)) {
|
166 | value.validate();
|
167 | return;
|
168 | } else if (isArray(value) && value.length > 0 && isEmbeddedDocument(value[0])) {
|
169 | value.forEach(function(v) {
|
170 | if (v.validate) {
|
171 | v.validate();
|
172 | }
|
173 | });
|
174 | return;
|
175 | }
|
176 | }
|
177 |
|
178 | if (!isValidType(value, that._schema[key].type)) {
|
179 |
|
180 | let typeName = null;
|
181 | let valueName = null;
|
182 | if (Array.isArray(that._schema[key].type) && that._schema[key].type.length > 0) {
|
183 | typeName = '[' + that._schema[key].type[0].name + ']';
|
184 | } else if (Array.isArray(that._schema[key].type) && that._schema[key].type.length === 0) {
|
185 | typeName = '[]';
|
186 | } else {
|
187 | typeName = that._schema[key].type.name;
|
188 | }
|
189 |
|
190 | if (Array.isArray(value)) {
|
191 |
|
192 | valueName = '[' + value.toString() + ']';
|
193 | } else {
|
194 | valueName = typeof(value);
|
195 | }
|
196 | throw new ValidationError('Value assigned to ' + that.collectionName() + '.' + key +
|
197 | ' should be ' + typeName + ', got ' + valueName);
|
198 | }
|
199 |
|
200 | if (that._schema[key].required && isEmptyValue(value)) {
|
201 | throw new ValidationError('Key ' + that.collectionName() + '.' + key +
|
202 | ' is required' + ', but got ' + value);
|
203 | }
|
204 |
|
205 | if (that._schema[key].match && isString(value) && !that._schema[key].match.test(value)) {
|
206 | throw new ValidationError('Value assigned to ' + that.collectionName() + '.' + key +
|
207 | ' does not match the regex/string ' + that._schema[key].match.toString() + '. Value was ' + value);
|
208 | }
|
209 |
|
210 | if (!isInChoices(that._schema[key].choices, value)) {
|
211 | throw new ValidationError('Value assigned to ' + that.collectionName() + '.' + key +
|
212 | ' should be in choices [' + that._schema[key].choices.join(', ') + '], got ' + value);
|
213 | }
|
214 |
|
215 | if (isNumber(that._schema[key].min) && value < that._schema[key].min) {
|
216 | throw new ValidationError('Value assigned to ' + that.collectionName() + '.' + key +
|
217 | ' is less than min, ' + that._schema[key].min + ', got ' + value);
|
218 | }
|
219 |
|
220 | if (isNumber(that._schema[key].max) && value > that._schema[key].max) {
|
221 | throw new ValidationError('Value assigned to ' + that.collectionName() + '.' + key +
|
222 | ' is less than max, ' + that._schema[key].max + ', got ' + value);
|
223 | }
|
224 |
|
225 | if (typeof(that._schema[key].validate) === 'function' && !that._schema[key].validate(value)) {
|
226 | throw new ValidationError('Value assigned to ' + that.collectionName() + '.' + key +
|
227 | ' failed custom validator. Value was ' + value);
|
228 | }
|
229 | });
|
230 | }
|
231 |
|
232 | |
233 |
|
234 |
|
235 |
|
236 |
|
237 | canonicalize() {
|
238 | const that = this;
|
239 |
|
240 | _.keys(that._schema).forEach(function(key) {
|
241 | let value = that[key];
|
242 |
|
243 | if (that._schema[key].type === Date && isDate(value)) {
|
244 | that[key] = new Date(value);
|
245 | } else if (value !== null && value !== undefined &&
|
246 | value.documentClass && value.documentClass() === 'embedded') {
|
247 |
|
248 | value.canonicalize();
|
249 | return;
|
250 | }
|
251 | });
|
252 | }
|
253 |
|
254 | |
255 |
|
256 |
|
257 |
|
258 |
|
259 |
|
260 | static create(data) {
|
261 | this.createIndexes();
|
262 |
|
263 | if (typeof(data) !== 'undefined') {
|
264 | return this._fromData(data);
|
265 | }
|
266 |
|
267 | return this._instantiate();
|
268 | }
|
269 |
|
270 | static createIndexes() { }
|
271 |
|
272 | |
273 |
|
274 |
|
275 |
|
276 |
|
277 |
|
278 | static _instantiate() {
|
279 | let instance = new this();
|
280 | instance.generateSchema();
|
281 | return instance;
|
282 | }
|
283 |
|
284 |
|
285 |
|
286 |
|
287 | static _fromData(datas) {
|
288 | const that = this;
|
289 |
|
290 | if (!isArray(datas)) {
|
291 | datas = [datas];
|
292 | }
|
293 |
|
294 | let documents = [];
|
295 | let embeddedPromises = [];
|
296 | datas.forEach(function(d) {
|
297 | let instance = that._instantiate();
|
298 | _.keys(d).forEach(function(key) {
|
299 | let value = null;
|
300 | if (d[key] === null) {
|
301 | value = instance.getDefault(key);
|
302 | } else {
|
303 | value = d[key];
|
304 | }
|
305 |
|
306 |
|
307 | if (key in instance._schema) {
|
308 |
|
309 | let type = instance._schema[key].type;
|
310 |
|
311 | if (type.documentClass && type.documentClass() === 'embedded') {
|
312 |
|
313 | instance[key] = type._fromData(value);
|
314 | } else if (isArray(type) && type.length > 0 &&
|
315 | type[0].documentClass && type[0].documentClass() === 'embedded') {
|
316 |
|
317 | instance[key] = [];
|
318 | value.forEach(function(v, i) {
|
319 | instance[key][i] = type[0]._fromData(v);
|
320 | });
|
321 | } else {
|
322 |
|
323 | instance[key] = value;
|
324 | }
|
325 | } else if (key in instance) {
|
326 |
|
327 | instance[key] = value;
|
328 | }
|
329 | });
|
330 |
|
331 | documents.push(instance);
|
332 | });
|
333 |
|
334 | if (documents.length === 1) {
|
335 | return documents[0];
|
336 | }
|
337 | return documents;
|
338 | }
|
339 |
|
340 | populate() {
|
341 | return BaseDocument.populate(this);
|
342 | }
|
343 |
|
344 | |
345 |
|
346 |
|
347 |
|
348 |
|
349 |
|
350 |
|
351 |
|
352 | static populate(docs, fields) {
|
353 | if (!docs) return Promise.all([]);
|
354 |
|
355 | let documents = null;
|
356 |
|
357 | if (!isArray(docs)) {
|
358 | documents = [docs];
|
359 | } else if (docs.length < 1) {
|
360 | return Promise.all(docs);
|
361 | } else {
|
362 | documents = docs;
|
363 | }
|
364 |
|
365 |
|
366 |
|
367 | let keys = [];
|
368 |
|
369 |
|
370 |
|
371 | let anInstance = documents[0];
|
372 |
|
373 | _.keys(anInstance._schema).forEach(function(key) {
|
374 |
|
375 | if (isArray(fields) && fields.indexOf(key) < 0) {
|
376 | return;
|
377 | }
|
378 |
|
379 |
|
380 | if (isArray(anInstance._schema[key].type) &&
|
381 | anInstance._schema[key].type.length > 0 &&
|
382 | isDocument(anInstance._schema[key].type[0])) {
|
383 | keys.push(key);
|
384 | }
|
385 |
|
386 | else if ((isString(anInstance[key]) || DB().isNativeId(anInstance[key])) &&
|
387 | isDocument(anInstance._schema[key].type)) {
|
388 | keys.push(key);
|
389 | }
|
390 | });
|
391 |
|
392 |
|
393 |
|
394 |
|
395 |
|
396 |
|
397 |
|
398 |
|
399 |
|
400 |
|
401 |
|
402 | let ids = {};
|
403 | keys.forEach(function(k) {
|
404 | ids[k] = {};
|
405 | documents.forEach(function(d) {
|
406 | ids[k][DB().toCanonicalId(d._id)] = [].concat(d[k]);
|
407 |
|
408 |
|
409 |
|
410 | if (isArray(d[k])) {
|
411 | d[k] = [];
|
412 | }
|
413 | });
|
414 | });
|
415 |
|
416 |
|
417 |
|
418 |
|
419 |
|
420 |
|
421 |
|
422 |
|
423 |
|
424 | let loadPromises = [];
|
425 | _.keys(ids).forEach(function(key) {
|
426 | let keyIds = [];
|
427 | _.keys(ids[key]).forEach(function(k) {
|
428 |
|
429 |
|
430 | keyIds = keyIds.concat(ids[key][k]);
|
431 | });
|
432 |
|
433 |
|
434 | keyIds = _.uniq(keyIds);
|
435 |
|
436 |
|
437 | let type = null;
|
438 | if (isArray(anInstance._schema[key].type)) {
|
439 | type = anInstance._schema[key].type[0];
|
440 | } else {
|
441 | type = anInstance._schema[key].type;
|
442 | }
|
443 |
|
444 |
|
445 | let p = type.find({ '_id': { $in: keyIds } }, { populate: false })
|
446 | .then(function(dereferences) {
|
447 |
|
448 |
|
449 | _.keys(ids[key]).forEach(function(k) {
|
450 |
|
451 |
|
452 | let doc;
|
453 | documents.forEach(function(d) {
|
454 | if (DB().toCanonicalId(d._id) === k) doc = d;
|
455 | });
|
456 |
|
457 |
|
458 |
|
459 | ids[key][k].forEach(function(id) {
|
460 |
|
461 |
|
462 | let deref;
|
463 | dereferences.forEach(function(d) {
|
464 | if (DB().toCanonicalId(d._id) === DB().toCanonicalId(id)) deref = d;
|
465 | });
|
466 |
|
467 | if (isArray(anInstance._schema[key].type)) {
|
468 | doc[key].push(deref);
|
469 | } else {
|
470 | doc[key] = deref;
|
471 | }
|
472 | });
|
473 | });
|
474 | });
|
475 |
|
476 | loadPromises.push(p);
|
477 | });
|
478 |
|
479 |
|
480 |
|
481 | return Promise.all(loadPromises).then(function() {
|
482 | return docs;
|
483 | });
|
484 | }
|
485 |
|
486 | |
487 |
|
488 |
|
489 |
|
490 |
|
491 |
|
492 | getDefault(schemaProp) {
|
493 | if (schemaProp in this._schema && 'default' in this._schema[schemaProp]) {
|
494 | let def = this._schema[schemaProp].default;
|
495 | let defVal = typeof(def) === 'function' ? def() : def;
|
496 | this[schemaProp] = defVal;
|
497 | return defVal;
|
498 | } else if (schemaProp === '_id') {
|
499 | return null;
|
500 | }
|
501 |
|
502 | return undefined;
|
503 | }
|
504 |
|
505 | |
506 |
|
507 |
|
508 |
|
509 |
|
510 | toJSON() {
|
511 | let values = this._toData({_id: true});
|
512 | let schema = this._schema;
|
513 | for (let key in schema) {
|
514 | if (schema.hasOwnProperty(key)) {
|
515 | if (schema[key].private){
|
516 | delete values[key];
|
517 | } else if (values[key] && values[key].toJSON) {
|
518 | values[key] = values[key].toJSON();
|
519 | } else if (isArray(values[key])) {
|
520 | let newArray = [];
|
521 | values[key].forEach(function(i) {
|
522 | if (i && i.toJSON) {
|
523 | newArray.push(i.toJSON());
|
524 | } else {
|
525 | newArray.push(i);
|
526 | }
|
527 | });
|
528 | values[key] = newArray;
|
529 | }
|
530 | }
|
531 | }
|
532 |
|
533 | return values;
|
534 | }
|
535 |
|
536 | |
537 |
|
538 |
|
539 |
|
540 |
|
541 |
|
542 | _toData(keep) {
|
543 | const that = this;
|
544 |
|
545 | if (keep === undefined || keep === null) {
|
546 | keep = {};
|
547 | } else if (keep._id === undefined) {
|
548 | keep._id = true;
|
549 | }
|
550 |
|
551 | let values = {};
|
552 | _.keys(this).forEach(function(k) {
|
553 | if (_.startsWith(k, '_')) {
|
554 | if (k !== '_id' || !keep._id) {
|
555 | return;
|
556 | } else {
|
557 | values[k] = that[k];
|
558 | }
|
559 | } else if (isEmbeddedDocument(that[k])) {
|
560 | values[k] = that[k]._toData();
|
561 | } else if (isArray(that[k]) && that[k].length > 0 && isEmbeddedDocument(that[k][0])) {
|
562 | values[k] = [];
|
563 | that[k].forEach(function(v) {
|
564 | values[k].push(v._toData());
|
565 | });
|
566 | } else {
|
567 | values[k] = that[k];
|
568 | }
|
569 | });
|
570 |
|
571 | return values;
|
572 | }
|
573 |
|
574 | _getEmbeddeds() {
|
575 | const that = this;
|
576 |
|
577 | let embeddeds = [];
|
578 | _.keys(this._schema).forEach(function(v) {
|
579 | if (isEmbeddedDocument(that._schema[v].type) ||
|
580 | (isArray(that._schema[v].type) && isEmbeddedDocument(that._schema[v].type[0]))) {
|
581 | embeddeds = embeddeds.concat(that[v]);
|
582 | }
|
583 | });
|
584 | return embeddeds;
|
585 | }
|
586 |
|
587 | _getHookPromises(hookName) {
|
588 | let embeddeds = this._getEmbeddeds();
|
589 |
|
590 | let hookPromises = [];
|
591 | hookPromises = hookPromises.concat(_.invoke(embeddeds, hookName));
|
592 | hookPromises.push(this[hookName]());
|
593 | return hookPromises;
|
594 | }
|
595 | }
|
596 |
|
597 | module.exports = BaseDocument;
|