UNPKG

14.7 kBJavaScriptView Raw
1'use strict';
2
3/*!
4 * Module dependencies.
5 */
6
7const ArrayType = require('./array');
8const CastError = require('../error/cast');
9const EventEmitter = require('events').EventEmitter;
10const SchemaDocumentArrayOptions =
11 require('../options/SchemaDocumentArrayOptions');
12const SchemaType = require('../schematype');
13const ValidationError = require('../error/validation');
14const discriminator = require('../helpers/model/discriminator');
15const get = require('../helpers/get');
16const handleIdOption = require('../helpers/schema/handleIdOption');
17const util = require('util');
18const utils = require('../utils');
19const getConstructor = require('../helpers/discriminator/getConstructor');
20
21const arrayPathSymbol = require('../helpers/symbols').arrayPathSymbol;
22const documentArrayParent = require('../helpers/symbols').documentArrayParent;
23
24let MongooseDocumentArray;
25let 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
37function 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 // Leave it up to `cast()` to convert this to a documentarray
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 * This schema type's name, to defend against minifiers that mangle
83 * function names.
84 *
85 * @api public
86 */
87DocumentArrayPath.schemaName = 'DocumentArray';
88
89/**
90 * Options for all document arrays.
91 *
92 * - `castNonArrays`: `true` by default. If `false`, Mongoose will throw a CastError when a value isn't an array. If `true`, Mongoose will wrap the provided value in an array before casting.
93 *
94 * @api public
95 */
96
97DocumentArrayPath.options = { castNonArrays: true };
98
99/*!
100 * Inherits from ArrayType.
101 */
102DocumentArrayPath.prototype = Object.create(ArrayType.prototype);
103DocumentArrayPath.prototype.constructor = DocumentArrayPath;
104DocumentArrayPath.prototype.OptionsConstructor = SchemaDocumentArrayOptions;
105
106/*!
107 * Ignore
108 */
109
110function _createConstructor(schema, options, baseClass) {
111 Subdocument || (Subdocument = require('../types/embedded'));
112
113 // compile an embedded document for this schema
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 // apply methods
129 for (const i in schema.methods) {
130 EmbeddedDocument.prototype[i] = schema.methods[i];
131 }
132
133 // apply statics
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 * Adds a discriminator to this document array.
149 *
150 * ####Example:
151 * const shapeSchema = Schema({ name: String }, { discriminatorKey: 'kind' });
152 * const schema = Schema({ shapes: [shapeSchema] });
153 *
154 * const docArrayPath = parentSchema.path('shapes');
155 * docArrayPath.discriminator('Circle', Schema({ radius: Number }));
156 *
157 * @param {String} name
158 * @param {Schema} schema fields to add to the schema for instances of this sub-class
159 * @param {String} [value] the string stored in the `discriminatorKey` property. If not specified, Mongoose uses the `name` parameter.
160 * @see discriminators /docs/discriminators.html
161 * @return {Function} the constructor Mongoose will use for creating instances of this discriminator model
162 * @api public
163 */
164
165DocumentArrayPath.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 // Ignore error, only happens on old versions of node
181 }
182
183 this.casterConstructor.discriminators[name] = EmbeddedDocument;
184
185 return this.casterConstructor.discriminators[name];
186};
187
188/**
189 * Performs local validations first, then validations on each embedded doc
190 *
191 * @api private
192 */
193
194DocumentArrayPath.prototype.doValidate = function(array, fn, scope, options) {
195 // lazy load
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 // handle sparse arrays, do not use array.forEach which does not
226 // iterate over sparse elements yet reports array.length including
227 // them :(
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 // sidestep sparse entries
241 let doc = array[i];
242 if (doc == null) {
243 --count || fn(error);
244 continue;
245 }
246
247 // If you set the array index directly, the doc might not yet be
248 // a full fledged mongoose subdoc, so make it into one.
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 * Performs local validations first, then validations on each embedded doc.
261 *
262 * ####Note:
263 *
264 * This method ignores the asynchronous validators.
265 *
266 * @return {MongooseError|undefined}
267 * @api private
268 */
269
270DocumentArrayPath.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 // handle sparse arrays, do not use array.forEach which does not
285 // iterate over sparse elements yet reports array.length including
286 // them :(
287
288 for (let i = 0, len = count; i < len; ++i) {
289 // sidestep sparse entries
290 let doc = array[i];
291 if (!doc) {
292 continue;
293 }
294
295 // If you set the array index directly, the doc might not yet be
296 // a full fledged mongoose subdoc, so make it into one.
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 * ignore
314 */
315
316DocumentArrayPath.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 // lazy load
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 // Make sure all paths in the subdoc are set to `default` instead
342 // of `init` since we used `init`.
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 * Casts contents
354 *
355 * @param {Object} value
356 * @param {Document} document that triggers the casting
357 * @api private
358 */
359
360DocumentArrayPath.prototype.cast = function(value, doc, init, prev, options) {
361 // lazy load
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 // gh-2442 mark whole array as modified if we're initializing a doc from
374 // the db and the path isn't an array in the document
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 // We need to create a new array, otherwise change tracking will
386 // update the old doc (gh-4449)
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 // Check if the document has a different schema (re gh-3701)
404 if ((value[i].$__) &&
405 (!(value[i] instanceof Constructor) || value[i][documentArrayParent] !== doc)) {
406 value[i] = value[i].toObject({
407 transform: false,
408 // Special case: if different model, but same schema, apply virtuals
409 // re: gh-7898
410 virtuals: value[i].schema === Constructor.schema
411 });
412 }
413
414 if (value[i] instanceof Subdocument) {
415 // Might not have the correct index yet, so ensure it does.
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 // handle resetting doc with existing id and same data
436 subdoc.set(value[i]);
437 // if set() is hooked it will have no return value
438 // see gh-746
439 value[i] = subdoc;
440 } else {
441 try {
442 subdoc = new Constructor(value[i], value, undefined,
443 undefined, i);
444 // if set() is hooked it will have no return value
445 // see gh-746
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 * ignore
462 */
463
464DocumentArrayPath.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 * Scopes paths selected in a query to this array.
475 * Necessary for proper default application of subdocument values.
476 *
477 * @param {DocumentArrayPath} array - the array to scope `fields` paths
478 * @param {Object|undefined} fields - the root fields selected in the query
479 * @param {Boolean|undefined} init - if we are being created part of a query result
480 */
481
482function 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 * Module exports.
515 */
516
517module.exports = DocumentArrayPath;