1 | 'use strict';
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 | const ArrayType = require('./array');
|
8 | const CastError = require('../error/cast');
|
9 | const EventEmitter = require('events').EventEmitter;
|
10 | const SchemaDocumentArrayOptions =
|
11 | require('../options/SchemaDocumentArrayOptions');
|
12 | const SchemaType = require('../schematype');
|
13 | const ValidationError = require('../error/validation');
|
14 | const discriminator = require('../helpers/model/discriminator');
|
15 | const get = require('../helpers/get');
|
16 | const handleIdOption = require('../helpers/schema/handleIdOption');
|
17 | const util = require('util');
|
18 | const utils = require('../utils');
|
19 | const getConstructor = require('../helpers/discriminator/getConstructor');
|
20 |
|
21 | const arrayPathSymbol = require('../helpers/symbols').arrayPathSymbol;
|
22 | const documentArrayParent = require('../helpers/symbols').documentArrayParent;
|
23 |
|
24 | let MongooseDocumentArray;
|
25 | let Subdocument;
|
26 |
|
27 | /**
|
28 | * SubdocsArray SchemaType constructor
|
29 | *
|
30 | * @param {String} key
|
31 | * @param {Schema} schema
|
32 | * @param {Object} options
|
33 | * @inherits SchemaArray
|
34 | * @api public
|
35 | */
|
36 |
|
37 | function DocumentArrayPath(key, schema, options, schemaOptions) {
|
38 | if (schemaOptions != null && schemaOptions._id != null) {
|
39 | schema = handleIdOption(schema, schemaOptions);
|
40 | } else if (options != null && options._id != null) {
|
41 | schema = handleIdOption(schema, options);
|
42 | }
|
43 |
|
44 | const EmbeddedDocument = _createConstructor(schema, options);
|
45 | EmbeddedDocument.prototype.$basePath = key;
|
46 |
|
47 | ArrayType.call(this, key, EmbeddedDocument, options);
|
48 |
|
49 | this.schema = schema;
|
50 | this.schemaOptions = schemaOptions || {};
|
51 | this.$isMongooseDocumentArray = true;
|
52 | this.Constructor = EmbeddedDocument;
|
53 |
|
54 | EmbeddedDocument.base = schema.base;
|
55 |
|
56 | const fn = this.defaultValue;
|
57 |
|
58 | if (!('defaultValue' in this) || fn !== void 0) {
|
59 | this.default(function() {
|
60 | let arr = fn.call(this);
|
61 | if (!Array.isArray(arr)) {
|
62 | arr = [arr];
|
63 | }
|
64 |
|
65 | return arr;
|
66 | });
|
67 | }
|
68 |
|
69 | const parentSchemaType = this;
|
70 | this.$embeddedSchemaType = new SchemaType(key + '.$', {
|
71 | required: get(this, 'schemaOptions.required', false)
|
72 | });
|
73 | this.$embeddedSchemaType.cast = function(value, doc, init) {
|
74 | return parentSchemaType.cast(value, doc, init)[0];
|
75 | };
|
76 | this.$embeddedSchemaType.$isMongooseDocumentArrayElement = true;
|
77 | this.$embeddedSchemaType.caster = this.Constructor;
|
78 | this.$embeddedSchemaType.schema = this.schema;
|
79 | }
|
80 |
|
81 |
|
82 |
|
83 |
|
84 |
|
85 |
|
86 |
|
87 | DocumentArrayPath.schemaName = 'DocumentArray';
|
88 |
|
89 |
|
90 |
|
91 |
|
92 |
|
93 |
|
94 |
|
95 |
|
96 |
|
97 | DocumentArrayPath.options = { castNonArrays: true };
|
98 |
|
99 |
|
100 |
|
101 |
|
102 | DocumentArrayPath.prototype = Object.create(ArrayType.prototype);
|
103 | DocumentArrayPath.prototype.constructor = DocumentArrayPath;
|
104 | DocumentArrayPath.prototype.OptionsConstructor = SchemaDocumentArrayOptions;
|
105 |
|
106 |
|
107 |
|
108 |
|
109 |
|
110 | function _createConstructor(schema, options, baseClass) {
|
111 | Subdocument || (Subdocument = require('../types/embedded'));
|
112 |
|
113 |
|
114 | function EmbeddedDocument() {
|
115 | Subdocument.apply(this, arguments);
|
116 |
|
117 | this.$session(this.ownerDocument().$session());
|
118 | }
|
119 |
|
120 | const proto = baseClass != null ? baseClass.prototype : Subdocument.prototype;
|
121 | EmbeddedDocument.prototype = Object.create(proto);
|
122 | EmbeddedDocument.prototype.$__setSchema(schema);
|
123 | EmbeddedDocument.schema = schema;
|
124 | EmbeddedDocument.prototype.constructor = EmbeddedDocument;
|
125 | EmbeddedDocument.$isArraySubdocument = true;
|
126 | EmbeddedDocument.events = new EventEmitter();
|
127 |
|
128 |
|
129 | for (const i in schema.methods) {
|
130 | EmbeddedDocument.prototype[i] = schema.methods[i];
|
131 | }
|
132 |
|
133 |
|
134 | for (const i in schema.statics) {
|
135 | EmbeddedDocument[i] = schema.statics[i];
|
136 | }
|
137 |
|
138 | for (const i in EventEmitter.prototype) {
|
139 | EmbeddedDocument[i] = EventEmitter.prototype[i];
|
140 | }
|
141 |
|
142 | EmbeddedDocument.options = options;
|
143 |
|
144 | return EmbeddedDocument;
|
145 | }
|
146 |
|
147 |
|
148 |
|
149 |
|
150 |
|
151 |
|
152 |
|
153 |
|
154 |
|
155 |
|
156 |
|
157 |
|
158 |
|
159 |
|
160 |
|
161 |
|
162 |
|
163 |
|
164 |
|
165 | DocumentArrayPath.prototype.discriminator = function(name, schema, tiedValue) {
|
166 | if (typeof name === 'function') {
|
167 | name = utils.getFunctionName(name);
|
168 | }
|
169 |
|
170 | schema = discriminator(this.casterConstructor, name, schema, tiedValue);
|
171 |
|
172 | const EmbeddedDocument = _createConstructor(schema, null, this.casterConstructor);
|
173 | EmbeddedDocument.baseCasterConstructor = this.casterConstructor;
|
174 |
|
175 | try {
|
176 | Object.defineProperty(EmbeddedDocument, 'name', {
|
177 | value: name
|
178 | });
|
179 | } catch (error) {
|
180 |
|
181 | }
|
182 |
|
183 | this.casterConstructor.discriminators[name] = EmbeddedDocument;
|
184 |
|
185 | return this.casterConstructor.discriminators[name];
|
186 | };
|
187 |
|
188 |
|
189 |
|
190 |
|
191 |
|
192 |
|
193 |
|
194 | DocumentArrayPath.prototype.doValidate = function(array, fn, scope, options) {
|
195 |
|
196 | MongooseDocumentArray || (MongooseDocumentArray = require('../types/documentarray'));
|
197 |
|
198 | const _this = this;
|
199 | try {
|
200 | SchemaType.prototype.doValidate.call(this, array, cb, scope);
|
201 | } catch (err) {
|
202 | err.$isArrayValidatorError = true;
|
203 | return fn(err);
|
204 | }
|
205 |
|
206 | function cb(err) {
|
207 | if (err) {
|
208 | err.$isArrayValidatorError = true;
|
209 | return fn(err);
|
210 | }
|
211 |
|
212 | let count = array && array.length;
|
213 | let error;
|
214 |
|
215 | if (!count) {
|
216 | return fn();
|
217 | }
|
218 | if (options && options.updateValidator) {
|
219 | return fn();
|
220 | }
|
221 | if (!array.isMongooseDocumentArray) {
|
222 | array = new MongooseDocumentArray(array, _this.path, scope);
|
223 | }
|
224 |
|
225 |
|
226 |
|
227 |
|
228 |
|
229 | function callback(err) {
|
230 | if (err != null) {
|
231 | error = err;
|
232 | if (!(error instanceof ValidationError)) {
|
233 | error.$isArrayValidatorError = true;
|
234 | }
|
235 | }
|
236 | --count || fn(error);
|
237 | }
|
238 |
|
239 | for (let i = 0, len = count; i < len; ++i) {
|
240 |
|
241 | let doc = array[i];
|
242 | if (doc == null) {
|
243 | --count || fn(error);
|
244 | continue;
|
245 | }
|
246 |
|
247 |
|
248 |
|
249 | if (!(doc instanceof Subdocument)) {
|
250 | const Constructor = getConstructor(_this.casterConstructor, array[i]);
|
251 | doc = array[i] = new Constructor(doc, array, undefined, undefined, i);
|
252 | }
|
253 |
|
254 | doc.$__validate(callback);
|
255 | }
|
256 | }
|
257 | };
|
258 |
|
259 |
|
260 |
|
261 |
|
262 |
|
263 |
|
264 |
|
265 |
|
266 |
|
267 |
|
268 |
|
269 |
|
270 | DocumentArrayPath.prototype.doValidateSync = function(array, scope) {
|
271 | const schemaTypeError = SchemaType.prototype.doValidateSync.call(this, array, scope);
|
272 | if (schemaTypeError != null) {
|
273 | schemaTypeError.$isArrayValidatorError = true;
|
274 | return schemaTypeError;
|
275 | }
|
276 |
|
277 | const count = array && array.length;
|
278 | let resultError = null;
|
279 |
|
280 | if (!count) {
|
281 | return;
|
282 | }
|
283 |
|
284 |
|
285 |
|
286 |
|
287 |
|
288 | for (let i = 0, len = count; i < len; ++i) {
|
289 |
|
290 | let doc = array[i];
|
291 | if (!doc) {
|
292 | continue;
|
293 | }
|
294 |
|
295 |
|
296 |
|
297 | if (!(doc instanceof Subdocument)) {
|
298 | const Constructor = getConstructor(this.casterConstructor, array[i]);
|
299 | doc = array[i] = new Constructor(doc, array, undefined, undefined, i);
|
300 | }
|
301 |
|
302 | const subdocValidateError = doc.validateSync();
|
303 |
|
304 | if (subdocValidateError && resultError == null) {
|
305 | resultError = subdocValidateError;
|
306 | }
|
307 | }
|
308 |
|
309 | return resultError;
|
310 | };
|
311 |
|
312 |
|
313 |
|
314 |
|
315 |
|
316 | DocumentArrayPath.prototype.getDefault = function(scope) {
|
317 | let ret = typeof this.defaultValue === 'function'
|
318 | ? this.defaultValue.call(scope)
|
319 | : this.defaultValue;
|
320 |
|
321 | if (ret == null) {
|
322 | return ret;
|
323 | }
|
324 |
|
325 |
|
326 | MongooseDocumentArray || (MongooseDocumentArray = require('../types/documentarray'));
|
327 |
|
328 | if (!Array.isArray(ret)) {
|
329 | ret = [ret];
|
330 | }
|
331 |
|
332 | ret = new MongooseDocumentArray(ret, this.path, scope);
|
333 |
|
334 | for (let i = 0; i < ret.length; ++i) {
|
335 | const Constructor = getConstructor(this.casterConstructor, ret[i]);
|
336 | const _subdoc = new Constructor({}, ret, undefined,
|
337 | undefined, i);
|
338 | _subdoc.init(ret[i]);
|
339 | _subdoc.isNew = true;
|
340 |
|
341 |
|
342 |
|
343 | Object.assign(_subdoc.$__.activePaths.default, _subdoc.$__.activePaths.init);
|
344 | _subdoc.$__.activePaths.init = {};
|
345 |
|
346 | ret[i] = _subdoc;
|
347 | }
|
348 |
|
349 | return ret;
|
350 | };
|
351 |
|
352 |
|
353 |
|
354 |
|
355 |
|
356 |
|
357 |
|
358 |
|
359 |
|
360 | DocumentArrayPath.prototype.cast = function(value, doc, init, prev, options) {
|
361 |
|
362 | MongooseDocumentArray || (MongooseDocumentArray = require('../types/documentarray'));
|
363 |
|
364 | let selected;
|
365 | let subdoc;
|
366 | const _opts = { transform: false, virtuals: false };
|
367 | options = options || {};
|
368 |
|
369 | if (!Array.isArray(value)) {
|
370 | if (!init && !DocumentArrayPath.options.castNonArrays) {
|
371 | throw new CastError('DocumentArray', util.inspect(value), this.path, null, this);
|
372 | }
|
373 |
|
374 |
|
375 | if (!!doc && init) {
|
376 | doc.markModified(this.path);
|
377 | }
|
378 | return this.cast([value], doc, init, prev, options);
|
379 | }
|
380 |
|
381 | if (!(value && value.isMongooseDocumentArray) &&
|
382 | !options.skipDocumentArrayCast) {
|
383 | value = new MongooseDocumentArray(value, this.path, doc);
|
384 | } else if (value && value.isMongooseDocumentArray) {
|
385 |
|
386 |
|
387 | value = new MongooseDocumentArray(value, this.path, doc);
|
388 | }
|
389 |
|
390 | if (options.arrayPath != null) {
|
391 | value[arrayPathSymbol] = options.arrayPath;
|
392 | }
|
393 |
|
394 | const len = value.length;
|
395 |
|
396 | for (let i = 0; i < len; ++i) {
|
397 | if (!value[i]) {
|
398 | continue;
|
399 | }
|
400 |
|
401 | const Constructor = getConstructor(this.casterConstructor, value[i]);
|
402 |
|
403 |
|
404 | if ((value[i].$__) &&
|
405 | (!(value[i] instanceof Constructor) || value[i][documentArrayParent] !== doc)) {
|
406 | value[i] = value[i].toObject({
|
407 | transform: false,
|
408 |
|
409 |
|
410 | virtuals: value[i].schema === Constructor.schema
|
411 | });
|
412 | }
|
413 |
|
414 | if (value[i] instanceof Subdocument) {
|
415 |
|
416 | if (value[i].__index == null) {
|
417 | value[i].$setIndex(i);
|
418 | }
|
419 | } else if (value[i] != null) {
|
420 | if (init) {
|
421 | if (doc) {
|
422 | selected || (selected = scopePaths(this, doc.$__.selected, init));
|
423 | } else {
|
424 | selected = true;
|
425 | }
|
426 |
|
427 | subdoc = new Constructor(null, value, true, selected, i);
|
428 | value[i] = subdoc.init(value[i]);
|
429 | } else {
|
430 | if (prev && typeof prev.id === 'function') {
|
431 | subdoc = prev.id(value[i]._id);
|
432 | }
|
433 |
|
434 | if (prev && subdoc && utils.deepEqual(subdoc.toObject(_opts), value[i])) {
|
435 |
|
436 | subdoc.set(value[i]);
|
437 |
|
438 |
|
439 | value[i] = subdoc;
|
440 | } else {
|
441 | try {
|
442 | subdoc = new Constructor(value[i], value, undefined,
|
443 | undefined, i);
|
444 |
|
445 |
|
446 | value[i] = subdoc;
|
447 | } catch (error) {
|
448 | const valueInErrorMessage = util.inspect(value[i]);
|
449 | throw new CastError('embedded', valueInErrorMessage,
|
450 | value[arrayPathSymbol], error, this);
|
451 | }
|
452 | }
|
453 | }
|
454 | }
|
455 | }
|
456 |
|
457 | return value;
|
458 | };
|
459 |
|
460 |
|
461 |
|
462 |
|
463 |
|
464 | DocumentArrayPath.prototype.clone = function() {
|
465 | const options = Object.assign({}, this.options);
|
466 | const schematype = new this.constructor(this.path, this.schema, options, this.schemaOptions);
|
467 | schematype.validators = this.validators.slice();
|
468 | schematype.Constructor.discriminators = Object.assign({},
|
469 | this.Constructor.discriminators);
|
470 | return schematype;
|
471 | };
|
472 |
|
473 |
|
474 |
|
475 |
|
476 |
|
477 |
|
478 |
|
479 |
|
480 |
|
481 |
|
482 | function scopePaths(array, fields, init) {
|
483 | if (!(init && fields)) {
|
484 | return undefined;
|
485 | }
|
486 |
|
487 | const path = array.path + '.';
|
488 | const keys = Object.keys(fields);
|
489 | let i = keys.length;
|
490 | const selected = {};
|
491 | let hasKeys;
|
492 | let key;
|
493 | let sub;
|
494 |
|
495 | while (i--) {
|
496 | key = keys[i];
|
497 | if (key.startsWith(path)) {
|
498 | sub = key.substring(path.length);
|
499 | if (sub === '$') {
|
500 | continue;
|
501 | }
|
502 | if (sub.startsWith('$.')) {
|
503 | sub = sub.substr(2);
|
504 | }
|
505 | hasKeys || (hasKeys = true);
|
506 | selected[sub] = fields[key];
|
507 | }
|
508 | }
|
509 |
|
510 | return hasKeys && selected || undefined;
|
511 | }
|
512 |
|
513 |
|
514 |
|
515 |
|
516 |
|
517 | module.exports = DocumentArrayPath;
|