thoughts/node_modules/mongoose/lib/schema/documentarray.js
node_modules/mongoose/lib/schema/documentarray.jsRaw
1/* eslint no-empty: 1 */
2
3/*!
4 * Module dependencies.
5 */
6
7var ArrayType = require('./array');
8var CastError = require('../error/cast');
9var MongooseDocumentArray = require('../types/documentarray');
10var SchemaType = require('../schematype');
11var Subdocument = require('../types/embedded');
12var util = require('util');
13
14/**
15 * SubdocsArray SchemaType constructor
16 *
17 * @param {String} key
18 * @param {Schema} schema
19 * @param {Object} options
20 * @inherits SchemaArray
21 * @api public
22 */
23
24function DocumentArray(key, schema, options) {
25 // compile an embedded document for this schema
26 function EmbeddedDocument() {
27 Subdocument.apply(this, arguments);
28 }
29
30 EmbeddedDocument.prototype = Object.create(Subdocument.prototype);
31 EmbeddedDocument.prototype.$__setSchema(schema);
32 EmbeddedDocument.schema = schema;
33
34 // apply methods
35 for (var i in schema.methods) {
36 EmbeddedDocument.prototype[i] = schema.methods[i];
37 }
38
39 // apply statics
40 for (i in schema.statics) {
41 EmbeddedDocument[i] = schema.statics[i];
42 }
43
44 EmbeddedDocument.options = options;
45
46 ArrayType.call(this, key, EmbeddedDocument, options);
47
48 this.schema = schema;
49 this.$isMongooseDocumentArray = true;
50 var path = this.path;
51 var fn = this.defaultValue;
52
53 if (!('defaultValue' in this) || fn !== void 0) {
54 this.default(function() {
55 var arr = fn.call(this);
56 if (!Array.isArray(arr)) {
57 arr = [arr];
58 }
59 return new MongooseDocumentArray(arr, path, this);
60 });
61 }
62}
63
64/**
65 * This schema type's name, to defend against minifiers that mangle
66 * function names.
67 *
68 * @api public
69 */
70DocumentArray.schemaName = 'DocumentArray';
71
72/*!
73 * Inherits from ArrayType.
74 */
75DocumentArray.prototype = Object.create(ArrayType.prototype);
76DocumentArray.prototype.constructor = DocumentArray;
77
78/**
79 * Performs local validations first, then validations on each embedded doc
80 *
81 * @api private
82 */
83
84DocumentArray.prototype.doValidate = function(array, fn, scope, options) {
85 var _this = this;
86 SchemaType.prototype.doValidate.call(this, array, function(err) {
87 if (err) {
88 return fn(err);
89 }
90
91 var count = array && array.length;
92 var error;
93
94 if (!count) {
95 return fn();
96 }
97 if (options && options.updateValidator) {
98 return fn();
99 }
100
101 // handle sparse arrays, do not use array.forEach which does not
102 // iterate over sparse elements yet reports array.length including
103 // them :(
104
105 function callback(err) {
106 if (err) {
107 error = err;
108 }
109 --count || fn(error);
110 }
111
112 for (var i = 0, len = count; i < len; ++i) {
113 // sidestep sparse entries
114 var doc = array[i];
115 if (!doc) {
116 --count || fn(error);
117 continue;
118 }
119
120 // If you set the array index directly, the doc might not yet be
121 // a full fledged mongoose subdoc, so make it into one.
122 if (!(doc instanceof Subdocument)) {
123 doc = array[i] = new _this.casterConstructor(doc, array, undefined,
124 undefined, i);
125 }
126
127 // HACK: use $__original_validate to avoid promises so bluebird doesn't
128 // complain
129 if (doc.$__original_validate) {
130 doc.$__original_validate({__noPromise: true}, callback);
131 } else {
132 doc.validate({__noPromise: true}, callback);
133 }
134 }
135 }, scope);
136};
137
138/**
139 * Performs local validations first, then validations on each embedded doc.
140 *
141 * ####Note:
142 *
143 * This method ignores the asynchronous validators.
144 *
145 * @return {MongooseError|undefined}
146 * @api private
147 */
148
149DocumentArray.prototype.doValidateSync = function(array, scope) {
150 var schemaTypeError = SchemaType.prototype.doValidateSync.call(this, array, scope);
151 if (schemaTypeError) {
152 return schemaTypeError;
153 }
154
155 var count = array && array.length,
156 resultError = null;
157
158 if (!count) {
159 return;
160 }
161
162 // handle sparse arrays, do not use array.forEach which does not
163 // iterate over sparse elements yet reports array.length including
164 // them :(
165
166 for (var i = 0, len = count; i < len; ++i) {
167 // only first error
168 if (resultError) {
169 break;
170 }
171 // sidestep sparse entries
172 var doc = array[i];
173 if (!doc) {
174 continue;
175 }
176
177 var subdocValidateError = doc.validateSync();
178
179 if (subdocValidateError) {
180 resultError = subdocValidateError;
181 }
182 }
183
184 return resultError;
185};
186
187/**
188 * Casts contents
189 *
190 * @param {Object} value
191 * @param {Document} document that triggers the casting
192 * @api private
193 */
194
195DocumentArray.prototype.cast = function(value, doc, init, prev, options) {
196 var selected,
197 subdoc,
198 i;
199
200 if (!Array.isArray(value)) {
201 // gh-2442 mark whole array as modified if we're initializing a doc from
202 // the db and the path isn't an array in the document
203 if (!!doc && init) {
204 doc.markModified(this.path);
205 }
206 return this.cast([value], doc, init, prev);
207 }
208
209 if (!(value && value.isMongooseDocumentArray) &&
210 (!options || !options.skipDocumentArrayCast)) {
211 value = new MongooseDocumentArray(value, this.path, doc);
212 if (prev && prev._handlers) {
213 for (var key in prev._handlers) {
214 doc.removeListener(key, prev._handlers[key]);
215 }
216 }
217 }
218
219 i = value.length;
220
221 while (i--) {
222 if (!value[i]) {
223 continue;
224 }
225 // Check if the document has a different schema (re gh-3701)
226 if ((value[i] instanceof Subdocument) &&
227 value[i].schema !== this.casterConstructor.schema) {
228 value[i] = value[i].toObject({virtuals: false});
229 }
230 if (!(value[i] instanceof Subdocument) && value[i]) {
231 if (init) {
232 selected || (selected = scopePaths(this, doc.$__.selected, init));
233 subdoc = new this.casterConstructor(null, value, true, selected, i);
234 value[i] = subdoc.init(value[i]);
235 } else {
236 try {
237 subdoc = prev.id(value[i]._id);
238 } catch (e) {
239 }
240
241 if (prev && subdoc) {
242 // handle resetting doc with existing id but differing data
243 // doc.array = [{ doc: 'val' }]
244 subdoc.set(value[i]);
245 // if set() is hooked it will have no return value
246 // see gh-746
247 value[i] = subdoc;
248 } else {
249 try {
250 subdoc = new this.casterConstructor(value[i], value, undefined,
251 undefined, i);
252 // if set() is hooked it will have no return value
253 // see gh-746
254 value[i] = subdoc;
255 } catch (error) {
256 var valueInErrorMessage = util.inspect(value[i]);
257 throw new CastError('embedded', valueInErrorMessage,
258 value._path, error);
259 }
260 }
261 }
262 }
263 }
264
265 return value;
266};
267
268/*!
269 * Scopes paths selected in a query to this array.
270 * Necessary for proper default application of subdocument values.
271 *
272 * @param {DocumentArray} array - the array to scope `fields` paths
273 * @param {Object|undefined} fields - the root fields selected in the query
274 * @param {Boolean|undefined} init - if we are being created part of a query result
275 */
276
277function scopePaths(array, fields, init) {
278 if (!(init && fields)) {
279 return undefined;
280 }
281
282 var path = array.path + '.',
283 keys = Object.keys(fields),
284 i = keys.length,
285 selected = {},
286 hasKeys,
287 key;
288
289 while (i--) {
290 key = keys[i];
291 if (key.indexOf(path) === 0) {
292 hasKeys || (hasKeys = true);
293 selected[key.substring(path.length)] = fields[key];
294 }
295 }
296
297 return hasKeys && selected || undefined;
298}
299
300/*!
301 * Module exports.
302 */
303
304module.exports = DocumentArray;