arnaucube
arnaucube/thoughtsPublic Self-hosted repository
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;