thoughts/node_modules/mongoose/lib/schema.js
node_modules/mongoose/lib/schema.jsRaw
1/*!
2 * Module dependencies.
3 */
4
5var readPref = require('./drivers').ReadPreference;
6var EventEmitter = require('events').EventEmitter;
7var VirtualType = require('./virtualtype');
8var utils = require('./utils');
9var MongooseTypes;
10var Kareem = require('kareem');
11var async = require('async');
12
13var IS_KAREEM_HOOK = {
14 count: true,
15 find: true,
16 findOne: true,
17 findOneAndUpdate: true,
18 findOneAndRemove: true,
19 insertMany: true,
20 update: true
21};
22
23/**
24 * Schema constructor.
25 *
26 * ####Example:
27 *
28 * var child = new Schema({ name: String });
29 * var schema = new Schema({ name: String, age: Number, children: [child] });
30 * var Tree = mongoose.model('Tree', schema);
31 *
32 * // setting schema options
33 * new Schema({ name: String }, { _id: false, autoIndex: false })
34 *
35 * ####Options:
36 *
37 * - [autoIndex](/docs/guide.html#autoIndex): bool - defaults to null (which means use the connection's autoIndex option)
38 * - [bufferCommands](/docs/guide.html#bufferCommands): bool - defaults to true
39 * - [capped](/docs/guide.html#capped): bool - defaults to false
40 * - [collection](/docs/guide.html#collection): string - no default
41 * - [emitIndexErrors](/docs/guide.html#emitIndexErrors): bool - defaults to false.
42 * - [id](/docs/guide.html#id): bool - defaults to true
43 * - [_id](/docs/guide.html#_id): bool - defaults to true
44 * - `minimize`: bool - controls [document#toObject](#document_Document-toObject) behavior when called manually - defaults to true
45 * - [read](/docs/guide.html#read): string
46 * - [safe](/docs/guide.html#safe): bool - defaults to true.
47 * - [shardKey](/docs/guide.html#shardKey): bool - defaults to `null`
48 * - [strict](/docs/guide.html#strict): bool - defaults to true
49 * - [toJSON](/docs/guide.html#toJSON) - object - no default
50 * - [toObject](/docs/guide.html#toObject) - object - no default
51 * - [typeKey](/docs/guide.html#typeKey) - string - defaults to 'type'
52 * - [useNestedStrict](/docs/guide.html#useNestedStrict) - boolean - defaults to false
53 * - [validateBeforeSave](/docs/guide.html#validateBeforeSave) - bool - defaults to `true`
54 * - [versionKey](/docs/guide.html#versionKey): string - defaults to "__v"
55 *
56 * ####Note:
57 *
58 * _When nesting schemas, (`children` in the example above), always declare the child schema first before passing it into its parent._
59 *
60 * @param {Object} definition
61 * @param {Object} [options]
62 * @inherits NodeJS EventEmitter http://nodejs.org/api/events.html#events_class_events_eventemitter
63 * @event `init`: Emitted after the schema is compiled into a `Model`.
64 * @api public
65 */
66
67function Schema(obj, options) {
68 if (!(this instanceof Schema)) {
69 return new Schema(obj, options);
70 }
71
72 this.paths = {};
73 this.subpaths = {};
74 this.virtuals = {};
75 this.singleNestedPaths = {};
76 this.nested = {};
77 this.inherits = {};
78 this.callQueue = [];
79 this._indexes = [];
80 this.methods = {};
81 this.statics = {};
82 this.tree = {};
83 this._requiredpaths = undefined;
84 this.discriminatorMapping = undefined;
85 this._indexedpaths = undefined;
86 this.query = {};
87 this.childSchemas = [];
88
89 this.s = {
90 hooks: new Kareem(),
91 kareemHooks: IS_KAREEM_HOOK
92 };
93
94 this.options = this.defaultOptions(options);
95
96 // build paths
97 if (obj) {
98 this.add(obj);
99 }
100
101 // check if _id's value is a subdocument (gh-2276)
102 var _idSubDoc = obj && obj._id && utils.isObject(obj._id);
103
104 // ensure the documents get an auto _id unless disabled
105 var auto_id = !this.paths['_id'] &&
106 (!this.options.noId && this.options._id) && !_idSubDoc;
107
108 if (auto_id) {
109 obj = {_id: {auto: true}};
110 obj._id[this.options.typeKey] = Schema.ObjectId;
111 this.add(obj);
112 }
113
114 // ensure the documents receive an id getter unless disabled
115 var autoid = !this.paths['id'] &&
116 (!this.options.noVirtualId && this.options.id);
117 if (autoid) {
118 this.virtual('id').get(idGetter);
119 }
120
121 for (var i = 0; i < this._defaultMiddleware.length; ++i) {
122 var m = this._defaultMiddleware[i];
123 this[m.kind](m.hook, !!m.isAsync, m.fn);
124 }
125
126 if (this.options.timestamps) {
127 this.setupTimestamp(this.options.timestamps);
128 }
129}
130
131/*!
132 * Returns this documents _id cast to a string.
133 */
134
135function idGetter() {
136 if (this.$__._id) {
137 return this.$__._id;
138 }
139
140 this.$__._id = this._id == null
141 ? null
142 : String(this._id);
143 return this.$__._id;
144}
145
146/*!
147 * Inherit from EventEmitter.
148 */
149Schema.prototype = Object.create(EventEmitter.prototype);
150Schema.prototype.constructor = Schema;
151Schema.prototype.instanceOfSchema = true;
152
153/**
154 * Default middleware attached to a schema. Cannot be changed.
155 *
156 * This field is used to make sure discriminators don't get multiple copies of
157 * built-in middleware. Declared as a constant because changing this at runtime
158 * may lead to instability with Model.prototype.discriminator().
159 *
160 * @api private
161 * @property _defaultMiddleware
162 */
163Object.defineProperty(Schema.prototype, '_defaultMiddleware', {
164 configurable: false,
165 enumerable: false,
166 writable: false,
167 value: [
168 {
169 kind: 'pre',
170 hook: 'save',
171 fn: function(next, options) {
172 var _this = this;
173 // Nested docs have their own presave
174 if (this.ownerDocument) {
175 return next();
176 }
177
178 var hasValidateBeforeSaveOption = options &&
179 (typeof options === 'object') &&
180 ('validateBeforeSave' in options);
181
182 var shouldValidate;
183 if (hasValidateBeforeSaveOption) {
184 shouldValidate = !!options.validateBeforeSave;
185 } else {
186 shouldValidate = this.schema.options.validateBeforeSave;
187 }
188
189 // Validate
190 if (shouldValidate) {
191 // HACK: use $__original_validate to avoid promises so bluebird doesn't
192 // complain
193 if (this.$__original_validate) {
194 this.$__original_validate({__noPromise: true}, function(error) {
195 return _this.schema.s.hooks.execPost('save:error', _this, [_this], { error: error }, function(error) {
196 next(error);
197 });
198 });
199 } else {
200 this.validate({__noPromise: true}, function(error) {
201 return _this.schema.s.hooks.execPost('save:error', _this, [ _this], { error: error }, function(error) {
202 next(error);
203 });
204 });
205 }
206 } else {
207 next();
208 }
209 }
210 },
211 {
212 kind: 'pre',
213 hook: 'save',
214 isAsync: true,
215 fn: function(next, done) {
216 var _this = this;
217 var subdocs = this.$__getAllSubdocs();
218
219 if (!subdocs.length || this.$__preSavingFromParent) {
220 done();
221 next();
222 return;
223 }
224
225 async.each(subdocs, function(subdoc, cb) {
226 subdoc.$__preSavingFromParent = true;
227 subdoc.save(function(err) {
228 cb(err);
229 });
230 }, function(error) {
231 for (var i = 0; i < subdocs.length; ++i) {
232 delete subdocs[i].$__preSavingFromParent;
233 }
234 if (error) {
235 return _this.schema.s.hooks.execPost('save:error', _this, [_this], { error: error }, function(error) {
236 done(error);
237 });
238 }
239 next();
240 done();
241 });
242 }
243 },
244 {
245 kind: 'pre',
246 hook: 'validate',
247 isAsync: true,
248 fn: function(next, done) {
249 // Hack to ensure that we always wrap validate() in a promise
250 next();
251 done();
252 }
253 },
254 {
255 kind: 'pre',
256 hook: 'remove',
257 isAsync: true,
258 fn: function(next, done) {
259 if (this.ownerDocument) {
260 done();
261 next();
262 return;
263 }
264
265 var subdocs = this.$__getAllSubdocs();
266
267 if (!subdocs.length || this.$__preSavingFromParent) {
268 done();
269 next();
270 return;
271 }
272
273 async.each(subdocs, function(subdoc, cb) {
274 subdoc.remove({ noop: true }, function(err) {
275 cb(err);
276 });
277 }, function(error) {
278 if (error) {
279 done(error);
280 return;
281 }
282 next();
283 done();
284 });
285 }
286 }
287 ]
288});
289
290/**
291 * Schema as flat paths
292 *
293 * ####Example:
294 * {
295 * '_id' : SchemaType,
296 * , 'nested.key' : SchemaType,
297 * }
298 *
299 * @api private
300 * @property paths
301 */
302
303Schema.prototype.paths;
304
305/**
306 * Schema as a tree
307 *
308 * ####Example:
309 * {
310 * '_id' : ObjectId
311 * , 'nested' : {
312 * 'key' : String
313 * }
314 * }
315 *
316 * @api private
317 * @property tree
318 */
319
320Schema.prototype.tree;
321
322/**
323 * Returns default options for this schema, merged with `options`.
324 *
325 * @param {Object} options
326 * @return {Object}
327 * @api private
328 */
329
330Schema.prototype.defaultOptions = function(options) {
331 if (options && options.safe === false) {
332 options.safe = {w: 0};
333 }
334
335 if (options && options.safe && options.safe.w === 0) {
336 // if you turn off safe writes, then versioning goes off as well
337 options.versionKey = false;
338 }
339
340 options = utils.options({
341 strict: true,
342 bufferCommands: true,
343 capped: false, // { size, max, autoIndexId }
344 versionKey: '__v',
345 discriminatorKey: '__t',
346 minimize: true,
347 autoIndex: null,
348 shardKey: null,
349 read: null,
350 validateBeforeSave: true,
351 // the following are only applied at construction time
352 noId: false, // deprecated, use { _id: false }
353 _id: true,
354 noVirtualId: false, // deprecated, use { id: false }
355 id: true,
356 typeKey: 'type'
357 }, options);
358
359 if (options.read) {
360 options.read = readPref(options.read);
361 }
362
363 return options;
364};
365
366/**
367 * Adds key path / schema type pairs to this schema.
368 *
369 * ####Example:
370 *
371 * var ToySchema = new Schema;
372 * ToySchema.add({ name: 'string', color: 'string', price: 'number' });
373 *
374 * @param {Object} obj
375 * @param {String} prefix
376 * @api public
377 */
378
379Schema.prototype.add = function add(obj, prefix) {
380 prefix = prefix || '';
381 var keys = Object.keys(obj);
382
383 for (var i = 0; i < keys.length; ++i) {
384 var key = keys[i];
385
386 if (obj[key] == null) {
387 throw new TypeError('Invalid value for schema path `' + prefix + key + '`');
388 }
389
390 if (Array.isArray(obj[key]) && obj[key].length === 1 && obj[key][0] == null) {
391 throw new TypeError('Invalid value for schema Array path `' + prefix + key + '`');
392 }
393
394 if (utils.isObject(obj[key]) &&
395 (!obj[key].constructor || utils.getFunctionName(obj[key].constructor) === 'Object') &&
396 (!obj[key][this.options.typeKey] || (this.options.typeKey === 'type' && obj[key].type.type))) {
397 if (Object.keys(obj[key]).length) {
398 // nested object { last: { name: String }}
399 this.nested[prefix + key] = true;
400 this.add(obj[key], prefix + key + '.');
401 } else {
402 if (prefix) {
403 this.nested[prefix.substr(0, prefix.length - 1)] = true;
404 }
405 this.path(prefix + key, obj[key]); // mixed type
406 }
407 } else {
408 if (prefix) {
409 this.nested[prefix.substr(0, prefix.length - 1)] = true;
410 }
411 this.path(prefix + key, obj[key]);
412 }
413 }
414};
415
416/**
417 * Reserved document keys.
418 *
419 * Keys in this object are names that are rejected in schema declarations b/c they conflict with mongoose functionality. Using these key name will throw an error.
420 *
421 * on, emit, _events, db, get, set, init, isNew, errors, schema, options, modelName, collection, _pres, _posts, toObject
422 *
423 * _NOTE:_ Use of these terms as method names is permitted, but play at your own risk, as they may be existing mongoose document methods you are stomping on.
424 *
425 * var schema = new Schema(..);
426 * schema.methods.init = function () {} // potentially breaking
427 */
428
429Schema.reserved = Object.create(null);
430var reserved = Schema.reserved;
431// EventEmitter
432reserved.emit =
433reserved.on =
434reserved.once =
435reserved.listeners =
436reserved.removeListener =
437// document properties and functions
438reserved.collection =
439reserved.db =
440reserved.errors =
441reserved.init =
442reserved.isModified =
443reserved.isNew =
444reserved.get =
445reserved.modelName =
446reserved.save =
447reserved.schema =
448reserved.set =
449reserved.toObject =
450reserved.validate =
451// hooks.js
452reserved._pres = reserved._posts = 1;
453
454/*!
455 * Document keys to print warnings for
456 */
457
458var warnings = {};
459warnings.increment = '`increment` should not be used as a schema path name ' +
460 'unless you have disabled versioning.';
461
462/**
463 * Gets/sets schema paths.
464 *
465 * Sets a path (if arity 2)
466 * Gets a path (if arity 1)
467 *
468 * ####Example
469 *
470 * schema.path('name') // returns a SchemaType
471 * schema.path('name', Number) // changes the schemaType of `name` to Number
472 *
473 * @param {String} path
474 * @param {Object} constructor
475 * @api public
476 */
477
478Schema.prototype.path = function(path, obj) {
479 if (obj === undefined) {
480 if (this.paths[path]) {
481 return this.paths[path];
482 }
483 if (this.subpaths[path]) {
484 return this.subpaths[path];
485 }
486 if (this.singleNestedPaths[path]) {
487 return this.singleNestedPaths[path];
488 }
489
490 // subpaths?
491 return /\.\d+\.?.*$/.test(path)
492 ? getPositionalPath(this, path)
493 : undefined;
494 }
495
496 // some path names conflict with document methods
497 if (reserved[path]) {
498 throw new Error('`' + path + '` may not be used as a schema pathname');
499 }
500
501 if (warnings[path]) {
502 console.log('WARN: ' + warnings[path]);
503 }
504
505 // update the tree
506 var subpaths = path.split(/\./),
507 last = subpaths.pop(),
508 branch = this.tree;
509
510 subpaths.forEach(function(sub, i) {
511 if (!branch[sub]) {
512 branch[sub] = {};
513 }
514 if (typeof branch[sub] !== 'object') {
515 var msg = 'Cannot set nested path `' + path + '`. '
516 + 'Parent path `'
517 + subpaths.slice(0, i).concat([sub]).join('.')
518 + '` already set to type ' + branch[sub].name
519 + '.';
520 throw new Error(msg);
521 }
522 branch = branch[sub];
523 });
524
525 branch[last] = utils.clone(obj);
526
527 this.paths[path] = Schema.interpretAsType(path, obj, this.options);
528
529 if (this.paths[path].$isSingleNested) {
530 for (var key in this.paths[path].schema.paths) {
531 this.singleNestedPaths[path + '.' + key] =
532 this.paths[path].schema.paths[key];
533 }
534 for (key in this.paths[path].schema.singleNestedPaths) {
535 this.singleNestedPaths[path + '.' + key] =
536 this.paths[path].schema.singleNestedPaths[key];
537 }
538
539 this.childSchemas.push(this.paths[path].schema);
540 } else if (this.paths[path].$isMongooseDocumentArray) {
541 this.childSchemas.push(this.paths[path].schema);
542 }
543 return this;
544};
545
546/**
547 * Converts type arguments into Mongoose Types.
548 *
549 * @param {String} path
550 * @param {Object} obj constructor
551 * @api private
552 */
553
554Schema.interpretAsType = function(path, obj, options) {
555 if (obj.constructor) {
556 var constructorName = utils.getFunctionName(obj.constructor);
557 if (constructorName !== 'Object') {
558 var oldObj = obj;
559 obj = {};
560 obj[options.typeKey] = oldObj;
561 }
562 }
563
564 // Get the type making sure to allow keys named "type"
565 // and default to mixed if not specified.
566 // { type: { type: String, default: 'freshcut' } }
567 var type = obj[options.typeKey] && (options.typeKey !== 'type' || !obj.type.type)
568 ? obj[options.typeKey]
569 : {};
570
571 if (utils.getFunctionName(type.constructor) === 'Object' || type === 'mixed') {
572 return new MongooseTypes.Mixed(path, obj);
573 }
574
575 if (Array.isArray(type) || Array === type || type === 'array') {
576 // if it was specified through { type } look for `cast`
577 var cast = (Array === type || type === 'array')
578 ? obj.cast
579 : type[0];
580
581 if (cast && cast.instanceOfSchema) {
582 return new MongooseTypes.DocumentArray(path, cast, obj);
583 }
584
585 if (Array.isArray(cast)) {
586 return new MongooseTypes.Array(path, Schema.interpretAsType(path, cast, options), obj);
587 }
588
589 if (typeof cast === 'string') {
590 cast = MongooseTypes[cast.charAt(0).toUpperCase() + cast.substring(1)];
591 } else if (cast && (!cast[options.typeKey] || (options.typeKey === 'type' && cast.type.type))
592 && utils.getFunctionName(cast.constructor) === 'Object'
593 && Object.keys(cast).length) {
594 // The `minimize` and `typeKey` options propagate to child schemas
595 // declared inline, like `{ arr: [{ val: { $type: String } }] }`.
596 // See gh-3560
597 var childSchemaOptions = {minimize: options.minimize};
598 if (options.typeKey) {
599 childSchemaOptions.typeKey = options.typeKey;
600 }
601 var childSchema = new Schema(cast, childSchemaOptions);
602 return new MongooseTypes.DocumentArray(path, childSchema, obj);
603 }
604
605 return new MongooseTypes.Array(path, cast || MongooseTypes.Mixed, obj);
606 }
607
608 if (type && type.instanceOfSchema) {
609 return new MongooseTypes.Embedded(type, path, obj);
610 }
611
612 var name;
613 if (Buffer.isBuffer(type)) {
614 name = 'Buffer';
615 } else {
616 name = typeof type === 'string'
617 ? type
618 // If not string, `type` is a function. Outside of IE, function.name
619 // gives you the function name. In IE, you need to compute it
620 : type.schemaName || utils.getFunctionName(type);
621 }
622
623 if (name) {
624 name = name.charAt(0).toUpperCase() + name.substring(1);
625 }
626
627 if (undefined == MongooseTypes[name]) {
628 throw new TypeError('Undefined type `' + name + '` at `' + path +
629 '`\n Did you try nesting Schemas? ' +
630 'You can only nest using refs or arrays.');
631 }
632
633 return new MongooseTypes[name](path, obj);
634};
635
636/**
637 * Iterates the schemas paths similar to Array#forEach.
638 *
639 * The callback is passed the pathname and schemaType as arguments on each iteration.
640 *
641 * @param {Function} fn callback function
642 * @return {Schema} this
643 * @api public
644 */
645
646Schema.prototype.eachPath = function(fn) {
647 var keys = Object.keys(this.paths),
648 len = keys.length;
649
650 for (var i = 0; i < len; ++i) {
651 fn(keys[i], this.paths[keys[i]]);
652 }
653
654 return this;
655};
656
657/**
658 * Returns an Array of path strings that are required by this schema.
659 *
660 * @api public
661 * @param {Boolean} invalidate refresh the cache
662 * @return {Array}
663 */
664
665Schema.prototype.requiredPaths = function requiredPaths(invalidate) {
666 if (this._requiredpaths && !invalidate) {
667 return this._requiredpaths;
668 }
669
670 var paths = Object.keys(this.paths),
671 i = paths.length,
672 ret = [];
673
674 while (i--) {
675 var path = paths[i];
676 if (this.paths[path].isRequired) {
677 ret.push(path);
678 }
679 }
680 this._requiredpaths = ret;
681 return this._requiredpaths;
682};
683
684/**
685 * Returns indexes from fields and schema-level indexes (cached).
686 *
687 * @api private
688 * @return {Array}
689 */
690
691Schema.prototype.indexedPaths = function indexedPaths() {
692 if (this._indexedpaths) {
693 return this._indexedpaths;
694 }
695 this._indexedpaths = this.indexes();
696 return this._indexedpaths;
697};
698
699/**
700 * Returns the pathType of `path` for this schema.
701 *
702 * Given a path, returns whether it is a real, virtual, nested, or ad-hoc/undefined path.
703 *
704 * @param {String} path
705 * @return {String}
706 * @api public
707 */
708
709Schema.prototype.pathType = function(path) {
710 if (path in this.paths) {
711 return 'real';
712 }
713 if (path in this.virtuals) {
714 return 'virtual';
715 }
716 if (path in this.nested) {
717 return 'nested';
718 }
719 if (path in this.subpaths) {
720 return 'real';
721 }
722 if (path in this.singleNestedPaths) {
723 return 'real';
724 }
725
726 if (/\.\d+\.|\.\d+$/.test(path)) {
727 return getPositionalPathType(this, path);
728 }
729 return 'adhocOrUndefined';
730};
731
732/**
733 * Returns true iff this path is a child of a mixed schema.
734 *
735 * @param {String} path
736 * @return {Boolean}
737 * @api private
738 */
739
740Schema.prototype.hasMixedParent = function(path) {
741 var subpaths = path.split(/\./g);
742 path = '';
743 for (var i = 0; i < subpaths.length; ++i) {
744 path = i > 0 ? path + '.' + subpaths[i] : subpaths[i];
745 if (path in this.paths &&
746 this.paths[path] instanceof MongooseTypes.Mixed) {
747 return true;
748 }
749 }
750
751 return false;
752};
753
754/**
755 * Setup updatedAt and createdAt timestamps to documents if enabled
756 *
757 * @param {Boolean|Object} timestamps timestamps options
758 * @api private
759 */
760Schema.prototype.setupTimestamp = function(timestamps) {
761 if (timestamps) {
762 var createdAt = timestamps.createdAt || 'createdAt';
763 var updatedAt = timestamps.updatedAt || 'updatedAt';
764 var schemaAdditions = {};
765
766 schemaAdditions[updatedAt] = Date;
767
768 if (!this.paths[createdAt]) {
769 schemaAdditions[createdAt] = Date;
770 }
771
772 this.add(schemaAdditions);
773
774 this.pre('save', function(next) {
775 var defaultTimestamp = new Date();
776 var auto_id = this._id && this._id.auto;
777
778 if (!this[createdAt] && this.isSelected(createdAt)) {
779 this[createdAt] = auto_id ? this._id.getTimestamp() : defaultTimestamp;
780 }
781
782 if (this.isNew || this.isModified()) {
783 this[updatedAt] = this.isNew ? this[createdAt] : defaultTimestamp;
784 }
785
786 next();
787 });
788
789 var genUpdates = function() {
790 var now = new Date();
791 var updates = { $set: {}, $setOnInsert: {} };
792 updates.$set[updatedAt] = now;
793 updates.$setOnInsert[createdAt] = now;
794
795 return updates;
796 };
797
798 this.methods.initializeTimestamps = function() {
799 if (!this[createdAt]) {
800 this[createdAt] = new Date();
801 }
802 if (!this[updatedAt]) {
803 this[updatedAt] = new Date();
804 }
805 return this;
806 };
807
808 this.pre('findOneAndUpdate', function(next) {
809 this.findOneAndUpdate({}, genUpdates());
810 applyTimestampsToChildren(this);
811 next();
812 });
813
814 this.pre('update', function(next) {
815 this.update({}, genUpdates());
816 applyTimestampsToChildren(this);
817 next();
818 });
819 }
820};
821
822/*!
823 * ignore
824 */
825
826function applyTimestampsToChildren(query) {
827 var now = new Date();
828 var update = query.getUpdate();
829 var keys = Object.keys(update);
830 var key;
831 var schema = query.model.schema;
832 var len;
833 var createdAt;
834 var updatedAt;
835 var timestamps;
836
837 var hasDollarKey = keys.length && keys[0].charAt(0) === '$';
838
839 if (hasDollarKey) {
840 if (update.$push) {
841 for (key in update.$push) {
842 if (update.$push[key] &&
843 schema.path(key).$isMongooseDocumentArray &&
844 schema.path(key).schema.options.timestamps) {
845 timestamps = schema.path(key).schema.options.timestamps;
846 createdAt = timestamps.createdAt || 'createdAt';
847 updatedAt = timestamps.updatedAt || 'updatedAt';
848 update.$push[key][updatedAt] = now;
849 update.$push[key][createdAt] = now;
850 }
851 }
852 }
853 if (update.$set) {
854 for (key in update.$set) {
855 if (Array.isArray(update.$set[key]) &&
856 schema.path(key).$isMongooseDocumentArray) {
857 len = update.$set[key].length;
858 timestamps = schema.path(key).schema.options.timestamps;
859 createdAt = timestamps.createdAt || 'createdAt';
860 updatedAt = timestamps.updatedAt || 'updatedAt';
861 for (var i = 0; i < len; ++i) {
862 update.$set[key][i][updatedAt] = now;
863 update.$set[key][i][createdAt] = now;
864 }
865 } else if (update.$set[key] && schema.path(key).$isSingleNested) {
866 timestamps = schema.path(key).schema.options.timestamps;
867 createdAt = timestamps.createdAt || 'createdAt';
868 updatedAt = timestamps.updatedAt || 'updatedAt';
869 update.$set[key][updatedAt] = now;
870 update.$set[key][createdAt] = now;
871 }
872 }
873 }
874 }
875}
876
877/*!
878 * ignore
879 */
880
881function getPositionalPathType(self, path) {
882 var subpaths = path.split(/\.(\d+)\.|\.(\d+)$/).filter(Boolean);
883 if (subpaths.length < 2) {
884 return self.paths[subpaths[0]];
885 }
886
887 var val = self.path(subpaths[0]);
888 var isNested = false;
889 if (!val) {
890 return val;
891 }
892
893 var last = subpaths.length - 1,
894 subpath,
895 i = 1;
896
897 for (; i < subpaths.length; ++i) {
898 isNested = false;
899 subpath = subpaths[i];
900
901 if (i === last && val && !val.schema && !/\D/.test(subpath)) {
902 if (val instanceof MongooseTypes.Array) {
903 // StringSchema, NumberSchema, etc
904 val = val.caster;
905 } else {
906 val = undefined;
907 }
908 break;
909 }
910
911 // ignore if its just a position segment: path.0.subpath
912 if (!/\D/.test(subpath)) {
913 continue;
914 }
915
916 if (!(val && val.schema)) {
917 val = undefined;
918 break;
919 }
920
921 var type = val.schema.pathType(subpath);
922 isNested = (type === 'nested');
923 val = val.schema.path(subpath);
924 }
925
926 self.subpaths[path] = val;
927 if (val) {
928 return 'real';
929 }
930 if (isNested) {
931 return 'nested';
932 }
933 return 'adhocOrUndefined';
934}
935
936
937/*!
938 * ignore
939 */
940
941function getPositionalPath(self, path) {
942 getPositionalPathType(self, path);
943 return self.subpaths[path];
944}
945
946/**
947 * Adds a method call to the queue.
948 *
949 * @param {String} name name of the document method to call later
950 * @param {Array} args arguments to pass to the method
951 * @api public
952 */
953
954Schema.prototype.queue = function(name, args) {
955 this.callQueue.push([name, args]);
956 return this;
957};
958
959/**
960 * Defines a pre hook for the document.
961 *
962 * ####Example
963 *
964 * var toySchema = new Schema(..);
965 *
966 * toySchema.pre('save', function (next) {
967 * if (!this.created) this.created = new Date;
968 * next();
969 * })
970 *
971 * toySchema.pre('validate', function (next) {
972 * if (this.name !== 'Woody') this.name = 'Woody';
973 * next();
974 * })
975 *
976 * @param {String} method
977 * @param {Function} callback
978 * @see hooks.js https://github.com/bnoguchi/hooks-js/tree/31ec571cef0332e21121ee7157e0cf9728572cc3
979 * @api public
980 */
981
982Schema.prototype.pre = function() {
983 var name = arguments[0];
984 if (IS_KAREEM_HOOK[name]) {
985 this.s.hooks.pre.apply(this.s.hooks, arguments);
986 return this;
987 }
988 return this.queue('pre', arguments);
989};
990
991/**
992 * Defines a post hook for the document
993 *
994 * var schema = new Schema(..);
995 * schema.post('save', function (doc) {
996 * console.log('this fired after a document was saved');
997 * });
998 *
999 * shema.post('find', function(docs) {
1000 * console.log('this fired after you run a find query');
1001 * });
1002 *
1003 * var Model = mongoose.model('Model', schema);
1004 *
1005 * var m = new Model(..);
1006 * m.save(function(err) {
1007 * console.log('this fires after the `post` hook');
1008 * });
1009 *
1010 * m.find(function(err, docs) {
1011 * console.log('this fires after the post find hook');
1012 * });
1013 *
1014 * @param {String} method name of the method to hook
1015 * @param {Function} fn callback
1016 * @see middleware http://mongoosejs.com/docs/middleware.html
1017 * @see hooks.js https://www.npmjs.com/package/hooks-fixed
1018 * @see kareem http://npmjs.org/package/kareem
1019 * @api public
1020 */
1021
1022Schema.prototype.post = function(method, fn) {
1023 if (IS_KAREEM_HOOK[method]) {
1024 this.s.hooks.post.apply(this.s.hooks, arguments);
1025 return this;
1026 }
1027 // assuming that all callbacks with arity < 2 are synchronous post hooks
1028 if (fn.length < 2) {
1029 return this.queue('on', [arguments[0], function(doc) {
1030 return fn.call(doc, doc);
1031 }]);
1032 }
1033
1034 if (fn.length === 3) {
1035 this.s.hooks.post(method + ':error', fn);
1036 return this;
1037 }
1038
1039 return this.queue('post', [arguments[0], function(next) {
1040 // wrap original function so that the callback goes last,
1041 // for compatibility with old code that is using synchronous post hooks
1042 var _this = this;
1043 var args = Array.prototype.slice.call(arguments, 1);
1044 fn.call(this, this, function(err) {
1045 return next.apply(_this, [err].concat(args));
1046 });
1047 }]);
1048};
1049
1050/**
1051 * Registers a plugin for this schema.
1052 *
1053 * @param {Function} plugin callback
1054 * @param {Object} [opts]
1055 * @see plugins
1056 * @api public
1057 */
1058
1059Schema.prototype.plugin = function(fn, opts) {
1060 fn(this, opts);
1061 return this;
1062};
1063
1064/**
1065 * Adds an instance method to documents constructed from Models compiled from this schema.
1066 *
1067 * ####Example
1068 *
1069 * var schema = kittySchema = new Schema(..);
1070 *
1071 * schema.method('meow', function () {
1072 * console.log('meeeeeoooooooooooow');
1073 * })
1074 *
1075 * var Kitty = mongoose.model('Kitty', schema);
1076 *
1077 * var fizz = new Kitty;
1078 * fizz.meow(); // meeeeeooooooooooooow
1079 *
1080 * If a hash of name/fn pairs is passed as the only argument, each name/fn pair will be added as methods.
1081 *
1082 * schema.method({
1083 * purr: function () {}
1084 * , scratch: function () {}
1085 * });
1086 *
1087 * // later
1088 * fizz.purr();
1089 * fizz.scratch();
1090 *
1091 * @param {String|Object} method name
1092 * @param {Function} [fn]
1093 * @api public
1094 */
1095
1096Schema.prototype.method = function(name, fn) {
1097 if (typeof name !== 'string') {
1098 for (var i in name) {
1099 this.methods[i] = name[i];
1100 }
1101 } else {
1102 this.methods[name] = fn;
1103 }
1104 return this;
1105};
1106
1107/**
1108 * Adds static "class" methods to Models compiled from this schema.
1109 *
1110 * ####Example
1111 *
1112 * var schema = new Schema(..);
1113 * schema.static('findByName', function (name, callback) {
1114 * return this.find({ name: name }, callback);
1115 * });
1116 *
1117 * var Drink = mongoose.model('Drink', schema);
1118 * Drink.findByName('sanpellegrino', function (err, drinks) {
1119 * //
1120 * });
1121 *
1122 * If a hash of name/fn pairs is passed as the only argument, each name/fn pair will be added as statics.
1123 *
1124 * @param {String|Object} name
1125 * @param {Function} [fn]
1126 * @api public
1127 */
1128
1129Schema.prototype.static = function(name, fn) {
1130 if (typeof name !== 'string') {
1131 for (var i in name) {
1132 this.statics[i] = name[i];
1133 }
1134 } else {
1135 this.statics[name] = fn;
1136 }
1137 return this;
1138};
1139
1140/**
1141 * Defines an index (most likely compound) for this schema.
1142 *
1143 * ####Example
1144 *
1145 * schema.index({ first: 1, last: -1 })
1146 *
1147 * @param {Object} fields
1148 * @param {Object} [options] Options to pass to [MongoDB driver's `createIndex()` function](http://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html#createIndex)
1149 * @param {String} [options.expires=null] Mongoose-specific syntactic sugar, uses [ms](https://www.npmjs.com/package/ms) to convert `expires` option into seconds for the `expireAfterSeconds` in the above link.
1150 * @api public
1151 */
1152
1153Schema.prototype.index = function(fields, options) {
1154 options || (options = {});
1155
1156 if (options.expires) {
1157 utils.expires(options);
1158 }
1159
1160 this._indexes.push([fields, options]);
1161 return this;
1162};
1163
1164/**
1165 * Sets/gets a schema option.
1166 *
1167 * ####Example
1168 *
1169 * schema.set('strict'); // 'true' by default
1170 * schema.set('strict', false); // Sets 'strict' to false
1171 * schema.set('strict'); // 'false'
1172 *
1173 * @param {String} key option name
1174 * @param {Object} [value] if not passed, the current option value is returned
1175 * @see Schema ./
1176 * @api public
1177 */
1178
1179Schema.prototype.set = function(key, value, _tags) {
1180 if (arguments.length === 1) {
1181 return this.options[key];
1182 }
1183
1184 switch (key) {
1185 case 'read':
1186 this.options[key] = readPref(value, _tags);
1187 break;
1188 case 'safe':
1189 this.options[key] = value === false
1190 ? {w: 0}
1191 : value;
1192 break;
1193 case 'timestamps':
1194 this.setupTimestamp(value);
1195 this.options[key] = value;
1196 break;
1197 default:
1198 this.options[key] = value;
1199 }
1200
1201 return this;
1202};
1203
1204/**
1205 * Gets a schema option.
1206 *
1207 * @param {String} key option name
1208 * @api public
1209 */
1210
1211Schema.prototype.get = function(key) {
1212 return this.options[key];
1213};
1214
1215/**
1216 * The allowed index types
1217 *
1218 * @static indexTypes
1219 * @receiver Schema
1220 * @api public
1221 */
1222
1223var indexTypes = '2d 2dsphere hashed text'.split(' ');
1224
1225Object.defineProperty(Schema, 'indexTypes', {
1226 get: function() {
1227 return indexTypes;
1228 },
1229 set: function() {
1230 throw new Error('Cannot overwrite Schema.indexTypes');
1231 }
1232});
1233
1234/**
1235 * Compiles indexes from fields and schema-level indexes
1236 *
1237 * @api public
1238 */
1239
1240Schema.prototype.indexes = function() {
1241 'use strict';
1242
1243 var indexes = [];
1244 var seenPrefix = {};
1245
1246 var collectIndexes = function(schema, prefix) {
1247 if (seenPrefix[prefix]) {
1248 return;
1249 }
1250 seenPrefix[prefix] = true;
1251
1252 prefix = prefix || '';
1253 var key, path, index, field, isObject, options, type;
1254 var keys = Object.keys(schema.paths);
1255
1256 for (var i = 0; i < keys.length; ++i) {
1257 key = keys[i];
1258 path = schema.paths[key];
1259
1260 if ((path instanceof MongooseTypes.DocumentArray) || path.$isSingleNested) {
1261 collectIndexes(path.schema, key + '.');
1262 } else {
1263 index = path._index;
1264
1265 if (index !== false && index !== null && index !== undefined) {
1266 field = {};
1267 isObject = utils.isObject(index);
1268 options = isObject ? index : {};
1269 type = typeof index === 'string' ? index :
1270 isObject ? index.type :
1271 false;
1272
1273 if (type && ~Schema.indexTypes.indexOf(type)) {
1274 field[prefix + key] = type;
1275 } else if (options.text) {
1276 field[prefix + key] = 'text';
1277 delete options.text;
1278 } else {
1279 field[prefix + key] = 1;
1280 }
1281
1282 delete options.type;
1283 if (!('background' in options)) {
1284 options.background = true;
1285 }
1286
1287 indexes.push([field, options]);
1288 }
1289 }
1290 }
1291
1292 if (prefix) {
1293 fixSubIndexPaths(schema, prefix);
1294 } else {
1295 schema._indexes.forEach(function(index) {
1296 if (!('background' in index[1])) {
1297 index[1].background = true;
1298 }
1299 });
1300 indexes = indexes.concat(schema._indexes);
1301 }
1302 };
1303
1304 collectIndexes(this);
1305 return indexes;
1306
1307 /*!
1308 * Checks for indexes added to subdocs using Schema.index().
1309 * These indexes need their paths prefixed properly.
1310 *
1311 * schema._indexes = [ [indexObj, options], [indexObj, options] ..]
1312 */
1313
1314 function fixSubIndexPaths(schema, prefix) {
1315 var subindexes = schema._indexes,
1316 len = subindexes.length,
1317 indexObj,
1318 newindex,
1319 klen,
1320 keys,
1321 key,
1322 i = 0,
1323 j;
1324
1325 for (i = 0; i < len; ++i) {
1326 indexObj = subindexes[i][0];
1327 keys = Object.keys(indexObj);
1328 klen = keys.length;
1329 newindex = {};
1330
1331 // use forward iteration, order matters
1332 for (j = 0; j < klen; ++j) {
1333 key = keys[j];
1334 newindex[prefix + key] = indexObj[key];
1335 }
1336
1337 indexes.push([newindex, subindexes[i][1]]);
1338 }
1339 }
1340};
1341
1342/**
1343 * Creates a virtual type with the given name.
1344 *
1345 * @param {String} name
1346 * @param {Object} [options]
1347 * @return {VirtualType}
1348 */
1349
1350Schema.prototype.virtual = function(name, options) {
1351 if (options && options.ref) {
1352 if (!options.localField) {
1353 throw new Error('Reference virtuals require `localField` option');
1354 }
1355
1356 if (!options.foreignField) {
1357 throw new Error('Reference virtuals require `foreignField` option');
1358 }
1359
1360 this.pre('init', function(next, obj) {
1361 if (name in obj) {
1362 if (!this.$$populatedVirtuals) {
1363 this.$$populatedVirtuals = {};
1364 }
1365
1366 if (options.justOne) {
1367 this.$$populatedVirtuals[name] = Array.isArray(obj[name]) ?
1368 obj[name][0] :
1369 obj[name];
1370 } else {
1371 this.$$populatedVirtuals[name] = Array.isArray(obj[name]) ?
1372 obj[name] :
1373 obj[name] == null ? [] : [obj[name]];
1374 }
1375
1376 delete obj[name];
1377 }
1378 next();
1379 });
1380
1381 var virtual = this.virtual(name);
1382 virtual.options = options;
1383 return virtual.
1384 get(function() {
1385 if (!this.$$populatedVirtuals) {
1386 this.$$populatedVirtuals = {};
1387 }
1388 if (name in this.$$populatedVirtuals) {
1389 return this.$$populatedVirtuals[name];
1390 }
1391 return null;
1392 }).
1393 set(function(v) {
1394 if (!this.$$populatedVirtuals) {
1395 this.$$populatedVirtuals = {};
1396 }
1397 this.$$populatedVirtuals[name] = v;
1398 });
1399 }
1400
1401 var virtuals = this.virtuals;
1402 var parts = name.split('.');
1403 virtuals[name] = parts.reduce(function(mem, part, i) {
1404 mem[part] || (mem[part] = (i === parts.length - 1)
1405 ? new VirtualType(options, name)
1406 : {});
1407 return mem[part];
1408 }, this.tree);
1409 return virtuals[name];
1410};
1411
1412/**
1413 * Returns the virtual type with the given `name`.
1414 *
1415 * @param {String} name
1416 * @return {VirtualType}
1417 */
1418
1419Schema.prototype.virtualpath = function(name) {
1420 return this.virtuals[name];
1421};
1422
1423/**
1424 * Removes the given `path` (or [`paths`]).
1425 *
1426 * @param {String|Array} path
1427 *
1428 * @api public
1429 */
1430Schema.prototype.remove = function(path) {
1431 if (typeof path === 'string') {
1432 path = [path];
1433 }
1434 if (Array.isArray(path)) {
1435 path.forEach(function(name) {
1436 if (this.path(name)) {
1437 delete this.paths[name];
1438
1439 var pieces = name.split('.');
1440 var last = pieces.pop();
1441 var branch = this.tree;
1442 for (var i = 0; i < pieces.length; ++i) {
1443 branch = branch[pieces[i]];
1444 }
1445 delete branch[last];
1446 }
1447 }, this);
1448 }
1449};
1450
1451/*!
1452 * ignore
1453 */
1454
1455Schema.prototype._getSchema = function(path) {
1456 var _this = this;
1457 var pathschema = _this.path(path);
1458
1459 if (pathschema) {
1460 return pathschema;
1461 }
1462
1463 function search(parts, schema) {
1464 var p = parts.length + 1,
1465 foundschema,
1466 trypath;
1467
1468 while (p--) {
1469 trypath = parts.slice(0, p).join('.');
1470 foundschema = schema.path(trypath);
1471 if (foundschema) {
1472 if (foundschema.caster) {
1473 // array of Mixed?
1474 if (foundschema.caster instanceof MongooseTypes.Mixed) {
1475 return foundschema.caster;
1476 }
1477
1478 // Now that we found the array, we need to check if there
1479 // are remaining document paths to look up for casting.
1480 // Also we need to handle array.$.path since schema.path
1481 // doesn't work for that.
1482 // If there is no foundschema.schema we are dealing with
1483 // a path like array.$
1484 if (p !== parts.length && foundschema.schema) {
1485 if (parts[p] === '$') {
1486 // comments.$.comments.$.title
1487 return search(parts.slice(p + 1), foundschema.schema);
1488 }
1489 // this is the last path of the selector
1490 return search(parts.slice(p), foundschema.schema);
1491 }
1492 }
1493 return foundschema;
1494 }
1495 }
1496 }
1497
1498 // look for arrays
1499 return search(path.split('.'), _this);
1500};
1501
1502/*!
1503 * ignore
1504 */
1505
1506Schema.prototype._getPathType = function(path) {
1507 var _this = this;
1508 var pathschema = _this.path(path);
1509
1510 if (pathschema) {
1511 return 'real';
1512 }
1513
1514 function search(parts, schema) {
1515 var p = parts.length + 1,
1516 foundschema,
1517 trypath;
1518
1519 while (p--) {
1520 trypath = parts.slice(0, p).join('.');
1521 foundschema = schema.path(trypath);
1522 if (foundschema) {
1523 if (foundschema.caster) {
1524 // array of Mixed?
1525 if (foundschema.caster instanceof MongooseTypes.Mixed) {
1526 return { schema: foundschema, pathType: 'mixed' };
1527 }
1528
1529 // Now that we found the array, we need to check if there
1530 // are remaining document paths to look up for casting.
1531 // Also we need to handle array.$.path since schema.path
1532 // doesn't work for that.
1533 // If there is no foundschema.schema we are dealing with
1534 // a path like array.$
1535 if (p !== parts.length && foundschema.schema) {
1536 if (parts[p] === '$') {
1537 if (p === parts.length - 1) {
1538 return { schema: foundschema, pathType: 'nested' };
1539 }
1540 // comments.$.comments.$.title
1541 return search(parts.slice(p + 1), foundschema.schema);
1542 }
1543 // this is the last path of the selector
1544 return search(parts.slice(p), foundschema.schema);
1545 }
1546 return {
1547 schema: foundschema,
1548 pathType: foundschema.$isSingleNested ? 'nested' : 'array'
1549 };
1550 }
1551 return { schema: foundschema, pathType: 'real' };
1552 } else if (p === parts.length && schema.nested[trypath]) {
1553 return { schema: schema, pathType: 'nested' };
1554 }
1555 }
1556 return { schema: foundschema || schema, pathType: 'undefined' };
1557 }
1558
1559 // look for arrays
1560 return search(path.split('.'), _this);
1561};
1562
1563
1564/*!
1565 * Module exports.
1566 */
1567
1568module.exports = exports = Schema;
1569
1570// require down here because of reference issues
1571
1572/**
1573 * The various built-in Mongoose Schema Types.
1574 *
1575 * ####Example:
1576 *
1577 * var mongoose = require('mongoose');
1578 * var ObjectId = mongoose.Schema.Types.ObjectId;
1579 *
1580 * ####Types:
1581 *
1582 * - [String](#schema-string-js)
1583 * - [Number](#schema-number-js)
1584 * - [Boolean](#schema-boolean-js) | Bool
1585 * - [Array](#schema-array-js)
1586 * - [Buffer](#schema-buffer-js)
1587 * - [Date](#schema-date-js)
1588 * - [ObjectId](#schema-objectid-js) | Oid
1589 * - [Mixed](#schema-mixed-js)
1590 *
1591 * Using this exposed access to the `Mixed` SchemaType, we can use them in our schema.
1592 *
1593 * var Mixed = mongoose.Schema.Types.Mixed;
1594 * new mongoose.Schema({ _user: Mixed })
1595 *
1596 * @api public
1597 */
1598
1599Schema.Types = MongooseTypes = require('./schema/index');
1600
1601/*!
1602 * ignore
1603 */
1604
1605exports.ObjectId = MongooseTypes.ObjectId;