You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1605 lines
40 KiB

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