UNPKG

mongoose

Version:
1,493 lines (1,331 loc) 105 kB
'use strict'; /*! * Module dependencies. */ const EventEmitter = require('events').EventEmitter; const Kareem = require('kareem'); const MongooseError = require('./error/mongooseError'); const SchemaType = require('./schemaType'); const SchemaTypeOptions = require('./options/schemaTypeOptions'); const VirtualOptions = require('./options/virtualOptions'); const VirtualType = require('./virtualType'); const addAutoId = require('./helpers/schema/addAutoId'); const clone = require('./helpers/clone'); const get = require('./helpers/get'); const getConstructorName = require('./helpers/getConstructorName'); const getIndexes = require('./helpers/schema/getIndexes'); const handleReadPreferenceAliases = require('./helpers/query/handleReadPreferenceAliases'); const idGetter = require('./helpers/schema/idGetter'); const isIndexSpecEqual = require('./helpers/indexes/isIndexSpecEqual'); const merge = require('./helpers/schema/merge'); const mpath = require('mpath'); const setPopulatedVirtualValue = require('./helpers/populate/setPopulatedVirtualValue'); const setupTimestamps = require('./helpers/timestamps/setupTimestamps'); const utils = require('./utils'); const validateRef = require('./helpers/populate/validateRef'); const hasNumericSubpathRegex = /\.\d+(\.|$)/; let MongooseTypes; const queryHooks = require('./constants').queryMiddlewareFunctions; const documentHooks = require('./helpers/model/applyHooks').middlewareFunctions; const hookNames = queryHooks.concat(documentHooks). reduce((s, hook) => s.add(hook), new Set()); const isPOJO = utils.isPOJO; let id = 0; const numberRE = /^\d+$/; /** * Schema constructor. * * #### Example: * * const child = new Schema({ name: String }); * const schema = new Schema({ name: String, age: Number, children: [child] }); * const Tree = mongoose.model('Tree', schema); * * // setting schema options * new Schema({ name: String }, { id: false, autoIndex: false }) * * #### Options: * * - [autoIndex](https://mongoosejs.com/docs/guide.html#autoIndex): bool - defaults to null (which means use the connection's autoIndex option) * - [autoCreate](https://mongoosejs.com/docs/guide.html#autoCreate): bool - defaults to null (which means use the connection's autoCreate option) * - [bufferCommands](https://mongoosejs.com/docs/guide.html#bufferCommands): bool - defaults to true * - [bufferTimeoutMS](https://mongoosejs.com/docs/guide.html#bufferTimeoutMS): number - defaults to 10000 (10 seconds). If `bufferCommands` is enabled, the amount of time Mongoose will wait for connectivity to be restablished before erroring out. * - [capped](https://mongoosejs.com/docs/guide.html#capped): bool | number | object - defaults to false * - [collection](https://mongoosejs.com/docs/guide.html#collection): string - no default * - [discriminatorKey](https://mongoosejs.com/docs/guide.html#discriminatorKey): string - defaults to `__t` * - [id](https://mongoosejs.com/docs/guide.html#id): bool - defaults to true * - [_id](https://mongoosejs.com/docs/guide.html#_id): bool - defaults to true * - [minimize](https://mongoosejs.com/docs/guide.html#minimize): bool - controls [document#toObject](https://mongoosejs.com/docs/api/document.html#Document.prototype.toObject()) behavior when called manually - defaults to true * - [read](https://mongoosejs.com/docs/guide.html#read): string * - [readConcern](https://mongoosejs.com/docs/guide.html#readConcern): object - defaults to null, use to set a default [read concern](https://www.mongodb.com/docs/manual/reference/read-concern/) for all queries. * - [writeConcern](https://mongoosejs.com/docs/guide.html#writeConcern): object - defaults to null, use to override [the MongoDB server's default write concern settings](https://www.mongodb.com/docs/manual/reference/write-concern/) * - [shardKey](https://mongoosejs.com/docs/guide.html#shardKey): object - defaults to `null` * - [strict](https://mongoosejs.com/docs/guide.html#strict): bool - defaults to true * - [strictQuery](https://mongoosejs.com/docs/guide.html#strictQuery): bool - defaults to false * - [toJSON](https://mongoosejs.com/docs/guide.html#toJSON) - object - no default * - [toObject](https://mongoosejs.com/docs/guide.html#toObject) - object - no default * - [typeKey](https://mongoosejs.com/docs/guide.html#typeKey) - string - defaults to 'type' * - [validateBeforeSave](https://mongoosejs.com/docs/guide.html#validateBeforeSave) - bool - defaults to `true` * - [validateModifiedOnly](https://mongoosejs.com/docs/api/document.html#Document.prototype.validate()) - bool - defaults to `false` * - [versionKey](https://mongoosejs.com/docs/guide.html#versionKey): string or object - defaults to "__v" * - [optimisticConcurrency](https://mongoosejs.com/docs/guide.html#optimisticConcurrency): bool or string[] or { exclude: string[] } - defaults to false. Set to true to enable [optimistic concurrency](https://thecodebarbarian.com/whats-new-in-mongoose-5-10-optimistic-concurrency.html). Set to string array to enable optimistic concurrency for only certain fields, or `{ exclude: string[] }` to define a list of fields to ignore for optimistic concurrency. * - [collation](https://mongoosejs.com/docs/guide.html#collation): object - defaults to null (which means use no collation) * - [timeseries](https://mongoosejs.com/docs/guide.html#timeseries): object - defaults to null (which means this schema's collection won't be a timeseries collection) * - [selectPopulatedPaths](https://mongoosejs.com/docs/guide.html#selectPopulatedPaths): boolean - defaults to `true` * - [skipVersioning](https://mongoosejs.com/docs/guide.html#skipVersioning): object - paths to exclude from versioning * - [timestamps](https://mongoosejs.com/docs/guide.html#timestamps): object or boolean - defaults to `false`. If true, Mongoose adds `createdAt` and `updatedAt` properties to your schema and manages those properties for you. * - [pluginTags](https://mongoosejs.com/docs/guide.html#pluginTags): array of strings - defaults to `undefined`. If set and plugin called with `tags` option, will only apply that plugin to schemas with a matching tag. * - [virtuals](https://mongoosejs.com/docs/tutorials/virtuals.html#virtuals-via-schema-options): object - virtuals to define, alias for [`.virtual`](https://mongoosejs.com/docs/api/schema.html#Schema.prototype.virtual()) * - [collectionOptions]: object with options passed to [`createCollection()`](https://www.mongodb.com/docs/manual/reference/method/db.createCollection/) when calling `Model.createCollection()` or `autoCreate` set to true. * - [encryptionType]: the encryption type for the schema. Valid options are `csfle` or `queryableEncryption`. See https://mongoosejs.com/docs/field-level-encryption. * * #### Options for Nested Schemas: * * - `excludeIndexes`: bool - defaults to `false`. If `true`, skip building indexes on this schema's paths. * * #### Note: * * _When nesting schemas, (`children` in the example above), always declare the child schema first before passing it into its parent._ * * @param {Object|Schema|Array} [definition] Can be one of: object describing schema paths, or schema to copy, or array of objects and schemas * @param {Object} [options] * @inherits NodeJS EventEmitter https://nodejs.org/api/events.html#class-eventemitter * @event `init`: Emitted after the schema is compiled into a `Model`. * @api public */ function Schema(obj, options) { if (!(this instanceof Schema)) { return new Schema(obj, options); } this.obj = obj; this.paths = {}; this.aliases = {}; this.subpaths = {}; this.virtuals = {}; this.singleNestedPaths = {}; this.nested = {}; this.inherits = {}; this.callQueue = []; this._indexes = []; this._searchIndexes = []; this.methods = (options && options.methods) || {}; this.methodOptions = {}; this.statics = (options && options.statics) || {}; this.tree = {}; this.query = (options && options.query) || {}; this.childSchemas = []; this.plugins = []; // For internal debugging. Do not use this to try to save a schema in MDB. this.$id = ++id; this.mapPaths = []; this.encryptedFields = {}; this.s = { hooks: new Kareem() }; this.options = this.defaultOptions(options); // build paths if (Array.isArray(obj)) { for (const definition of obj) { this.add(definition); } } else if (obj) { this.add(obj); } // build virtual paths if (options && options.virtuals) { const virtuals = options.virtuals; const pathNames = Object.keys(virtuals); for (const pathName of pathNames) { const pathOptions = virtuals[pathName].options ? virtuals[pathName].options : undefined; const virtual = this.virtual(pathName, pathOptions); if (virtuals[pathName].get) { virtual.get(virtuals[pathName].get); } if (virtuals[pathName].set) { virtual.set(virtuals[pathName].set); } } } // check if _id's value is a subdocument (gh-2276) const _idSubDoc = obj && obj._id && utils.isObject(obj._id); // ensure the documents get an auto _id unless disabled const auto_id = !this.paths['_id'] && (this.options._id) && !_idSubDoc; if (auto_id) { addAutoId(this); } this.setupTimestamp(this.options.timestamps); } /** * Create virtual properties with alias field * @api private */ function aliasFields(schema, paths) { for (const path of Object.keys(paths)) { let alias = null; if (paths[path] != null) { alias = paths[path]; } else { const options = get(schema.paths[path], 'options'); if (options == null) { continue; } alias = options.alias; } if (!alias) { continue; } const prop = schema.paths[path].path; if (Array.isArray(alias)) { for (const a of alias) { if (typeof a !== 'string') { throw new Error('Invalid value for alias option on ' + prop + ', got ' + a); } schema.aliases[a] = prop; schema. virtual(a). get((function(p) { return function() { if (typeof this.get === 'function') { return this.get(p); } return this[p]; }; })(prop)). set((function(p) { return function(v) { return this.$set(p, v); }; })(prop)); } continue; } if (typeof alias !== 'string') { throw new Error('Invalid value for alias option on ' + prop + ', got ' + alias); } schema.aliases[alias] = prop; schema. virtual(alias). get((function(p) { return function() { if (typeof this.get === 'function') { return this.get(p); } return this[p]; }; })(prop)). set((function(p) { return function(v) { return this.$set(p, v); }; })(prop)); } } /*! * Inherit from EventEmitter. */ Schema.prototype = Object.create(EventEmitter.prototype); Schema.prototype.constructor = Schema; Schema.prototype.instanceOfSchema = true; /*! * ignore */ Object.defineProperty(Schema.prototype, '$schemaType', { configurable: false, enumerable: false, writable: true }); /** * Array of child schemas (from document arrays and single nested subdocs) * and their corresponding compiled models. Each element of the array is * an object with 2 properties: `schema` and `model`. * * This property is typically only useful for plugin authors and advanced users. * You do not need to interact with this property at all to use mongoose. * * @api public * @property childSchemas * @memberOf Schema * @instance */ Object.defineProperty(Schema.prototype, 'childSchemas', { configurable: false, enumerable: true, writable: true }); /** * Object containing all virtuals defined on this schema. * The objects' keys are the virtual paths and values are instances of `VirtualType`. * * This property is typically only useful for plugin authors and advanced users. * You do not need to interact with this property at all to use mongoose. * * #### Example: * * const schema = new Schema({}); * schema.virtual('answer').get(() => 42); * * console.log(schema.virtuals); // { answer: VirtualType { path: 'answer', ... } } * console.log(schema.virtuals['answer'].getters[0].call()); // 42 * * @api public * @property virtuals * @memberOf Schema * @instance */ Object.defineProperty(Schema.prototype, 'virtuals', { configurable: false, enumerable: true, writable: true }); /** * The original object passed to the schema constructor * * #### Example: * * const schema = new Schema({ a: String }).add({ b: String }); * schema.obj; // { a: String } * * @api public * @property obj * @memberOf Schema * @instance */ Schema.prototype.obj; /** * The paths defined on this schema. The keys are the top-level paths * in this schema, and the values are instances of the SchemaType class. * * #### Example: * * const schema = new Schema({ name: String }, { _id: false }); * schema.paths; // { name: SchemaString { ... } } * * schema.add({ age: Number }); * schema.paths; // { name: SchemaString { ... }, age: SchemaNumber { ... } } * * @api public * @property paths * @memberOf Schema * @instance */ Schema.prototype.paths; /** * Schema as a tree * * #### Example: * * { * '_id' : ObjectId * , 'nested' : { * 'key' : String * } * } * * @api private * @property tree * @memberOf Schema * @instance */ Schema.prototype.tree; /** * Returns a deep copy of the schema * * #### Example: * * const schema = new Schema({ name: String }); * const clone = schema.clone(); * clone === schema; // false * clone.path('name'); // SchemaString { ... } * * @return {Schema} the cloned schema * @api public * @memberOf Schema * @instance */ Schema.prototype.clone = function() { const s = this._clone(); // Bubble up `init` for backwards compat s.on('init', v => this.emit('init', v)); return s; }; /*! * ignore */ Schema.prototype._clone = function _clone(Constructor) { Constructor = Constructor || (this.base == null ? Schema : this.base.Schema); const s = new Constructor({}, this._userProvidedOptions); s.base = this.base; s.obj = this.obj; s.options = clone(this.options); s.callQueue = this.callQueue.map(function(f) { return f; }); s.methods = clone(this.methods); s.methodOptions = clone(this.methodOptions); s.statics = clone(this.statics); s.query = clone(this.query); s.plugins = Array.prototype.slice.call(this.plugins); s._indexes = clone(this._indexes); s._searchIndexes = clone(this._searchIndexes); s.s.hooks = this.s.hooks.clone(); s.tree = clone(this.tree); s.paths = Object.fromEntries( Object.entries(this.paths).map(([key, value]) => ([key, value.clone()])) ); s.nested = clone(this.nested); s.subpaths = clone(this.subpaths); for (const schemaType of Object.values(s.paths)) { if (schemaType.$isSingleNested) { const path = schemaType.path; for (const key of Object.keys(schemaType.schema.paths)) { s.singleNestedPaths[path + '.' + key] = schemaType.schema.paths[key]; } for (const key of Object.keys(schemaType.schema.singleNestedPaths)) { s.singleNestedPaths[path + '.' + key] = schemaType.schema.singleNestedPaths[key]; } for (const key of Object.keys(schemaType.schema.subpaths)) { s.singleNestedPaths[path + '.' + key] = schemaType.schema.subpaths[key]; } for (const key of Object.keys(schemaType.schema.nested)) { s.singleNestedPaths[path + '.' + key] = 'nested'; } } } s._gatherChildSchemas(); s.virtuals = clone(this.virtuals); s.$globalPluginsApplied = this.$globalPluginsApplied; s.$isRootDiscriminator = this.$isRootDiscriminator; s.$implicitlyCreated = this.$implicitlyCreated; s.$id = ++id; s.$originalSchemaId = this.$id; s.mapPaths = [].concat(this.mapPaths); if (this.discriminatorMapping != null) { s.discriminatorMapping = Object.assign({}, this.discriminatorMapping); } if (this.discriminators != null) { s.discriminators = Object.assign({}, this.discriminators); } if (this._applyDiscriminators != null) { s._applyDiscriminators = new Map(this._applyDiscriminators); } s.aliases = Object.assign({}, this.aliases); s.encryptedFields = clone(this.encryptedFields); return s; }; /** * Returns a new schema that has the picked `paths` from this schema. * * This method is analagous to [Lodash's `pick()` function](https://lodash.com/docs/4.17.15#pick) for Mongoose schemas. * * #### Example: * * const schema = Schema({ name: String, age: Number }); * // Creates a new schema with the same `name` path as `schema`, * // but no `age` path. * const newSchema = schema.pick(['name']); * * newSchema.path('name'); // SchemaString { ... } * newSchema.path('age'); // undefined * * @param {String[]} paths List of Paths to pick for the new Schema * @param {Object} [options] Options to pass to the new Schema Constructor (same as `new Schema(.., Options)`). Defaults to `this.options` if not set. * @return {Schema} * @api public */ Schema.prototype.pick = function(paths, options) { const newSchema = new Schema({}, options || this.options); if (!Array.isArray(paths)) { throw new MongooseError('Schema#pick() only accepts an array argument, ' + 'got "' + typeof paths + '"'); } for (const path of paths) { if (this._hasEncryptedField(path)) { const encrypt = this.encryptedFields[path]; const schemaType = this.path(path); newSchema.add({ [path]: { encrypt, [this.options.typeKey]: schemaType } }); } else if (this.nested[path]) { newSchema.add({ [path]: get(this.tree, path) }); } else { const schematype = this.path(path); if (schematype == null) { throw new MongooseError('Path `' + path + '` is not in the schema'); } newSchema.add({ [path]: schematype }); } } if (!this._hasEncryptedFields()) { newSchema.options.encryptionType = null; } return newSchema; }; /** * Returns a new schema that has the `paths` from the original schema, minus the omitted ones. * * This method is analagous to [Lodash's `omit()` function](https://lodash.com/docs/#omit) for Mongoose schemas. * * #### Example: * * const schema = Schema({ name: String, age: Number }); * // Creates a new schema omitting the `age` path * const newSchema = schema.omit(['age']); * * newSchema.path('name'); // SchemaString { ... } * newSchema.path('age'); // undefined * * @param {String[]} paths List of Paths to omit for the new Schema * @param {Object} [options] Options to pass to the new Schema Constructor (same as `new Schema(.., Options)`). Defaults to `this.options` if not set. * @return {Schema} * @api public */ Schema.prototype.omit = function(paths, options) { const newSchema = new Schema(this, options || this.options); if (!Array.isArray(paths)) { throw new MongooseError( 'Schema#omit() only accepts an array argument, ' + 'got "' + typeof paths + '"' ); } newSchema.remove(paths); for (const nested in newSchema.singleNestedPaths) { if (paths.includes(nested)) { delete newSchema.singleNestedPaths[nested]; } } return newSchema; }; /** * Returns default options for this schema, merged with `options`. * * @param {Object} [options] Options to overwrite the default options * @return {Object} The merged options of `options` and the default options * @api private */ Schema.prototype.defaultOptions = function(options) { this._userProvidedOptions = options == null ? {} : clone(options); const baseOptions = this.base && this.base.options || {}; const strict = 'strict' in baseOptions ? baseOptions.strict : true; const strictQuery = 'strictQuery' in baseOptions ? baseOptions.strictQuery : false; const id = 'id' in baseOptions ? baseOptions.id : true; options = { strict, strictQuery, bufferCommands: true, capped: false, // { size, max, autoIndexId } versionKey: '__v', optimisticConcurrency: false, minimize: true, autoIndex: null, discriminatorKey: '__t', shardKey: null, read: null, validateBeforeSave: true, validateModifiedOnly: false, // the following are only applied at construction time _id: true, id: id, typeKey: 'type', ...options }; if (options.versionKey && typeof options.versionKey !== 'string') { throw new MongooseError('`versionKey` must be falsy or string, got `' + (typeof options.versionKey) + '`'); } if (typeof options.read === 'string') { options.read = handleReadPreferenceAliases(options.read); } else if (Array.isArray(options.read) && typeof options.read[0] === 'string') { options.read = { mode: handleReadPreferenceAliases(options.read[0]), tags: options.read[1] }; } if (options.optimisticConcurrency && !options.versionKey) { throw new MongooseError('Must set `versionKey` if using `optimisticConcurrency`'); } return options; }; /** * Inherit a Schema by applying a discriminator on an existing Schema. * * * #### Example: * * const eventSchema = new mongoose.Schema({ timestamp: Date }, { discriminatorKey: 'kind' }); * * const clickedEventSchema = new mongoose.Schema({ element: String }, { discriminatorKey: 'kind' }); * const ClickedModel = eventSchema.discriminator('clicked', clickedEventSchema); * * const Event = mongoose.model('Event', eventSchema); * * Event.discriminators['clicked']; // Model { clicked } * * const doc = await Event.create({ kind: 'clicked', element: '#hero' }); * doc.element; // '#hero' * doc instanceof ClickedModel; // true * * @param {String} name the name of the discriminator * @param {Schema} schema the discriminated Schema * @param {Object} [options] discriminator options * @param {String} [options.value] the string stored in the `discriminatorKey` property. If not specified, Mongoose uses the `name` parameter. * @param {Boolean} [options.clone=true] By default, `discriminator()` clones the given `schema`. Set to `false` to skip cloning. * @param {Boolean} [options.overwriteModels=false] by default, Mongoose does not allow you to define a discriminator with the same name as another discriminator. Set this to allow overwriting discriminators with the same name. * @param {Boolean} [options.mergeHooks=true] By default, Mongoose merges the base schema's hooks with the discriminator schema's hooks. Set this option to `false` to make Mongoose use the discriminator schema's hooks instead. * @param {Boolean} [options.mergePlugins=true] By default, Mongoose merges the base schema's plugins with the discriminator schema's plugins. Set this option to `false` to make Mongoose use the discriminator schema's plugins instead. * @return {Schema} the Schema instance * @api public */ Schema.prototype.discriminator = function(name, schema, options) { this._applyDiscriminators = this._applyDiscriminators || new Map(); this._applyDiscriminators.set(name, { schema, options }); return this; }; /*! * Get this schema's default toObject/toJSON options, including Mongoose global * options. */ Schema.prototype._defaultToObjectOptions = function(json) { const path = json ? 'toJSON' : 'toObject'; if (this._defaultToObjectOptionsMap && this._defaultToObjectOptionsMap[path]) { return this._defaultToObjectOptionsMap[path]; } const baseOptions = this.base && this.base.options && this.base.options[path] || {}; const schemaOptions = this.options[path] || {}; // merge base default options with Schema's set default options if available. // `clone` is necessary here because `utils.options` directly modifies the second input. const defaultOptions = Object.assign({}, baseOptions, schemaOptions); this._defaultToObjectOptionsMap = this._defaultToObjectOptionsMap || {}; this._defaultToObjectOptionsMap[path] = defaultOptions; return defaultOptions; }; /** * Sets the encryption type of the schema, if a value is provided, otherwise * returns the encryption type. * * @param {'csfle' | 'queryableEncryption' | null | undefined} encryptionType plain object with paths to add, or another schema */ Schema.prototype.encryptionType = function encryptionType(encryptionType) { if (arguments.length === 0) { return this.options.encryptionType; } if (!(typeof encryptionType === 'string' || encryptionType === null)) { throw new Error('invalid `encryptionType`: ${encryptionType}'); } this.options.encryptionType = encryptionType; }; /** * Adds key path / schema type pairs to this schema. * * #### Example: * * const ToySchema = new Schema(); * ToySchema.add({ name: 'string', color: 'string', price: 'number' }); * * const TurboManSchema = new Schema(); * // You can also `add()` another schema and copy over all paths, virtuals, * // getters, setters, indexes, methods, and statics. * TurboManSchema.add(ToySchema).add({ year: Number }); * * @param {Object|Schema} obj plain object with paths to add, or another schema * @param {String} [prefix] path to prefix the newly added paths with * @return {Schema} the Schema instance * @api public */ Schema.prototype.add = function add(obj, prefix) { if (obj instanceof Schema || (obj != null && obj.instanceOfSchema)) { merge(this, obj); return this; } // Special case: setting top-level `_id` to false should convert to disabling // the `_id` option. This behavior never worked before 5.4.11 but numerous // codebases use it (see gh-7516, gh-7512). if (obj._id === false && prefix == null) { this.options._id = false; } prefix = prefix || ''; // avoid prototype pollution if (prefix === '__proto__.' || prefix === 'constructor.' || prefix === 'prototype.') { return this; } const keys = Object.keys(obj); const typeKey = this.options.typeKey; for (const key of keys) { if (utils.specialProperties.has(key)) { continue; } const fullPath = prefix + key; const val = obj[key]; if (val == null) { throw new TypeError('Invalid value for schema path `' + fullPath + '`, got value "' + val + '"'); } // Retain `_id: false` but don't set it as a path, re: gh-8274. if (key === '_id' && val === false) { continue; } // Deprecate setting schema paths to primitive types (gh-7558) let isMongooseTypeString = false; if (typeof val === 'string') { // Handle the case in which the type is specified as a string (eg. 'date', 'oid', ...) const MongooseTypes = this.base != null ? this.base.Schema.Types : Schema.Types; const upperVal = val.charAt(0).toUpperCase() + val.substring(1); isMongooseTypeString = MongooseTypes[upperVal] != null; } if ( key !== '_id' && ((typeof val !== 'object' && typeof val !== 'function' && !isMongooseTypeString) || val == null) ) { throw new TypeError(`Invalid schema configuration: \`${val}\` is not ` + `a valid type at path \`${key}\`. See ` + 'https://bit.ly/mongoose-schematypes for a list of valid schema types.'); } if (val instanceof VirtualType || (val.constructor && val.constructor.name || null) === 'VirtualType') { this.virtual(val); continue; } if (Array.isArray(val) && val.length === 1 && val[0] == null) { throw new TypeError('Invalid value for schema Array path `' + fullPath + '`, got value "' + val[0] + '"'); } if (!(isPOJO(val) || val instanceof SchemaTypeOptions)) { // Special-case: Non-options definitely a path so leaf at this node // Examples: Schema instances, SchemaType instances if (prefix) { this.nested[prefix.substring(0, prefix.length - 1)] = true; } this.path(prefix + key, val); if (val[0] != null && !(val[0].instanceOfSchema) && utils.isPOJO(val[0].discriminators)) { const schemaType = this.path(prefix + key); for (const key in val[0].discriminators) { schemaType.discriminator(key, val[0].discriminators[key]); } } } else if (Object.keys(val).length < 1) { // Special-case: {} always interpreted as Mixed path so leaf at this node if (prefix) { this.nested[prefix.substring(0, prefix.length - 1)] = true; } this.path(fullPath, val); // mixed type } else if (!val[typeKey] || (typeKey === 'type' && isPOJO(val.type) && val.type.type)) { // Special-case: POJO with no bona-fide type key - interpret as tree of deep paths so recurse // nested object `{ last: { name: String } }`. Avoid functions with `.type` re: #10807 because // NestJS sometimes adds `Date.type`. this.nested[fullPath] = true; this.add(val, fullPath + '.'); } else { // There IS a bona-fide type key that may also be a POJO const _typeDef = val[typeKey]; if (isPOJO(_typeDef) && Object.keys(_typeDef).length > 0) { // If a POJO is the value of a type key, make it a subdocument if (prefix) { this.nested[prefix.substring(0, prefix.length - 1)] = true; } const childSchemaOptions = {}; if (this._userProvidedOptions.typeKey) { childSchemaOptions.typeKey = this._userProvidedOptions.typeKey; } // propagate 'strict' option to child schema if (this._userProvidedOptions.strict != null) { childSchemaOptions.strict = this._userProvidedOptions.strict; } if (this._userProvidedOptions.toObject != null) { childSchemaOptions.toObject = utils.omit(this._userProvidedOptions.toObject, ['transform']); } if (this._userProvidedOptions.toJSON != null) { childSchemaOptions.toJSON = utils.omit(this._userProvidedOptions.toJSON, ['transform']); } const _schema = new Schema(_typeDef, childSchemaOptions); _schema.$implicitlyCreated = true; const schemaWrappedPath = Object.assign({}, val, { [typeKey]: _schema }); this.path(prefix + key, schemaWrappedPath); } else { // Either the type is non-POJO or we interpret it as Mixed anyway if (prefix) { this.nested[prefix.substring(0, prefix.length - 1)] = true; } this.path(prefix + key, val); if (val != null && !(val.instanceOfSchema) && utils.isPOJO(val.discriminators)) { const schemaType = this.path(prefix + key); for (const key in val.discriminators) { schemaType.discriminator(key, val.discriminators[key]); } } } } if (val.instanceOfSchema && val.encryptionType() != null) { // schema.add({ field: <instance of encrypted schema> }) if (this.encryptionType() != val.encryptionType()) { throw new Error('encryptionType of a nested schema must match the encryption type of the parent schema.'); } for (const [encryptedField, encryptedFieldConfig] of Object.entries(val.encryptedFields)) { const path = fullPath + '.' + encryptedField; this._addEncryptedField(path, encryptedFieldConfig); } } else if (typeof val === 'object' && 'encrypt' in val) { // schema.add({ field: { type: <schema type>, encrypt: { ... }}}) const { encrypt } = val; if (this.encryptionType() == null) { throw new Error('encryptionType must be provided'); } this._addEncryptedField(fullPath, encrypt); } else { // if the field was already encrypted and we re-configure it to be unencrypted, remove // the encrypted field configuration this._removeEncryptedField(fullPath); } } const aliasObj = Object.fromEntries( Object.entries(obj).map(([key]) => ([prefix + key, null])) ); aliasFields(this, aliasObj); return this; }; /** * @param {string} path * @param {object} fieldConfig * * @api private */ Schema.prototype._addEncryptedField = function _addEncryptedField(path, fieldConfig) { const type = this.path(path).autoEncryptionType(); if (type == null) { throw new Error(`Invalid BSON type for FLE field: '${path}'`); } this.encryptedFields[path] = clone(fieldConfig); }; /** * @param {string} path * * @api private */ Schema.prototype._removeEncryptedField = function _removeEncryptedField(path) { delete this.encryptedFields[path]; }; /** * @api private * * @returns {boolean} */ Schema.prototype._hasEncryptedFields = function _hasEncryptedFields() { return Object.keys(this.encryptedFields).length > 0; }; /** * @param {string} path * @returns {boolean} * * @api private */ Schema.prototype._hasEncryptedField = function _hasEncryptedField(path) { return path in this.encryptedFields; }; /** * Builds an encryptedFieldsMap for the schema. * * @api private */ Schema.prototype._buildEncryptedFields = function() { const fields = Object.entries(this.encryptedFields).map( ([path, config]) => { const bsonType = this.path(path).autoEncryptionType(); // { path, bsonType, keyId, queries? } return { path, bsonType, ...config }; }); return { fields }; }; /** * Builds a schemaMap for the schema, if the schema is configured for client-side field level encryption. * * @api private */ Schema.prototype._buildSchemaMap = function() { /** * `schemaMap`s are JSON schemas, which use the following structure to represent objects: * { field: { bsonType: 'object', properties: { ... } } } * * for example, a schema that looks like this `{ a: { b: int32 } }` would be encoded as * `{ a: { bsonType: 'object', properties: { b: < encryption configuration > } } }` * * This function takes an array of path segments, an output object (that gets mutated) and * a value to be associated with the full path, and constructs a valid CSFLE JSON schema path for * the object. This works for deeply nested properties as well. * * @param {string[]} path array of path components * @param {object} object the object in which to build a JSON schema of `path`'s properties * @param {object} value the value to associate with the path in object */ function buildNestedPath(path, object, value) { let i = 0, component = path[i]; for (; i < path.length - 1; ++i, component = path[i]) { object[component] = object[component] == null ? { bsonType: 'object', properties: {} } : object[component]; object = object[component].properties; } object[component] = value; } const schemaMapPropertyReducer = (accum, [path, propertyConfig]) => { const bsonType = this.path(path).autoEncryptionType(); const pathComponents = path.split('.'); const configuration = { encrypt: { ...propertyConfig, bsonType } }; buildNestedPath(pathComponents, accum, configuration); return accum; }; const properties = Object.entries(this.encryptedFields).reduce( schemaMapPropertyReducer, {}); return { bsonType: 'object', properties }; }; /** * Add an alias for `path`. This means getting or setting the `alias` * is equivalent to getting or setting the `path`. * * #### Example: * * const toySchema = new Schema({ n: String }); * * // Make 'name' an alias for 'n' * toySchema.alias('n', 'name'); * * const Toy = mongoose.model('Toy', toySchema); * const turboMan = new Toy({ n: 'Turbo Man' }); * * turboMan.name; // 'Turbo Man' * turboMan.n; // 'Turbo Man' * * turboMan.name = 'Turbo Man Action Figure'; * turboMan.n; // 'Turbo Man Action Figure' * * await turboMan.save(); // Saves { _id: ..., n: 'Turbo Man Action Figure' } * * * @param {String} path real path to alias * @param {String|String[]} alias the path(s) to use as an alias for `path` * @return {Schema} the Schema instance * @api public */ Schema.prototype.alias = function alias(path, alias) { aliasFields(this, { [path]: alias }); return this; }; /** * Remove an index by name or index specification. * * removeIndex only removes indexes from your schema object. Does **not** affect the indexes * in MongoDB. * * #### Example: * * const ToySchema = new Schema({ name: String, color: String, price: Number }); * * // Add a new index on { name, color } * ToySchema.index({ name: 1, color: 1 }); * * // Remove index on { name, color } * // Keep in mind that order matters! `removeIndex({ color: 1, name: 1 })` won't remove the index * ToySchema.removeIndex({ name: 1, color: 1 }); * * // Add an index with a custom name * ToySchema.index({ color: 1 }, { name: 'my custom index name' }); * // Remove index by name * ToySchema.removeIndex('my custom index name'); * * @param {Object|string} index name or index specification * @return {Schema} the Schema instance * @api public */ Schema.prototype.removeIndex = function removeIndex(index) { if (arguments.length > 1) { throw new Error('removeIndex() takes only 1 argument'); } if (typeof index !== 'object' && typeof index !== 'string') { throw new Error('removeIndex() may only take either an object or a string as an argument'); } if (typeof index === 'object') { for (let i = this._indexes.length - 1; i >= 0; --i) { if (isIndexSpecEqual(this._indexes[i][0], index)) { this._indexes.splice(i, 1); } } } else { for (let i = this._indexes.length - 1; i >= 0; --i) { if (this._indexes[i][1] != null && this._indexes[i][1].name === index) { this._indexes.splice(i, 1); } } } return this; }; /** * Remove all indexes from this schema. * * clearIndexes only removes indexes from your schema object. Does **not** affect the indexes * in MongoDB. * * #### Example: * * const ToySchema = new Schema({ name: String, color: String, price: Number }); * ToySchema.index({ name: 1 }); * ToySchema.index({ color: 1 }); * * // Remove all indexes on this schema * ToySchema.clearIndexes(); * * ToySchema.indexes(); // [] * * @return {Schema} the Schema instance * @api public */ Schema.prototype.clearIndexes = function clearIndexes() { this._indexes.length = 0; return this; }; /** * Add an [Atlas search index](https://www.mongodb.com/docs/atlas/atlas-search/create-index/) that Mongoose will create using `Model.createSearchIndex()`. * This function only works when connected to MongoDB Atlas. * * #### Example: * * const ToySchema = new Schema({ name: String, color: String, price: Number }); * ToySchema.searchIndex({ name: 'test', definition: { mappings: { dynamic: true } } }); * * @param {Object} description index options, including `name` and `definition` * @param {String} description.name * @param {Object} description.definition * @return {Schema} the Schema instance * @api public */ Schema.prototype.searchIndex = function searchIndex(description) { this._searchIndexes.push(description); return this; }; /** * Reserved document keys. * * Keys in this object are names that are warned in schema declarations * because they have the potential to break Mongoose/ Mongoose plugins functionality. If you create a schema * using `new Schema()` with one of these property names, Mongoose will log a warning. * * - _posts * - _pres * - collection * - emit * - errors * - get * - init * - isModified * - isNew * - listeners * - modelName * - on * - once * - populated * - prototype * - remove * - removeListener * - save * - schema * - toObject * - validate * * _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. * * const schema = new Schema(..); * schema.methods.init = function () {} // potentially breaking * * @property reserved * @memberOf Schema * @static */ Schema.reserved = Object.create(null); Schema.prototype.reserved = Schema.reserved; const reserved = Schema.reserved; // Core object reserved['prototype'] = // EventEmitter reserved.emit = reserved.listeners = reserved.removeListener = // document properties and functions reserved.collection = reserved.errors = reserved.get = reserved.init = reserved.isModified = reserved.isNew = reserved.populated = reserved.remove = reserved.save = reserved.toObject = reserved.validate = 1; reserved.collection = 1; /** * Gets/sets schema paths. * * Sets a path (if arity 2) * Gets a path (if arity 1) * * #### Example: * * schema.path('name') // returns a SchemaType * schema.path('name', Number) // changes the schemaType of `name` to Number * * @param {String} path The name of the Path to get / set * @param {Object} [obj] The Type to set the path to, if provided the path will be SET, otherwise the path will be GET * @api public */ Schema.prototype.path = function(path, obj) { if (obj === undefined) { if (this.paths[path] != null) { return this.paths[path]; } // Convert to '.$' to check subpaths re: gh-6405 const cleanPath = _pathToPositionalSyntax(path); let schematype = _getPath(this, path, cleanPath); if (schematype != null) { return schematype; } // Look for maps const mapPath = getMapPath(this, path); if (mapPath != null) { return mapPath; } // Look if a parent of this path is mixed schematype = this.hasMixedParent(cleanPath); if (schematype != null) { return schematype; } // subpaths? return hasNumericSubpathRegex.test(path) ? getPositionalPath(this, path, cleanPath) : undefined; } // some path names conflict with document methods const firstPieceOfPath = path.split('.')[0]; if (reserved[firstPieceOfPath] && !this.options.suppressReservedKeysWarning) { const errorMessage = `\`${firstPieceOfPath}\` is a reserved schema pathname and may break some functionality. ` + 'You are allowed to use it, but use at your own risk. ' + 'To disable this warning pass `suppressReservedKeysWarning` as a schema option.'; utils.warn(errorMessage); } if (typeof obj === 'object' && utils.hasUserDefinedProperty(obj, 'ref')) { validateRef(obj.ref, path); } // update the tree const subpaths = path.split(/\./); const last = subpaths.pop(); let branch = this.tree; let fullPath = ''; for (const sub of subpaths) { if (utils.specialProperties.has(sub)) { throw new Error('Cannot set special property `' + sub + '` on a schema'); } fullPath = fullPath += (fullPath.length > 0 ? '.' : '') + sub; if (!branch[sub]) { this.nested[fullPath] = true; branch[sub] = {}; } if (typeof branch[sub] !== 'object') { const msg = 'Cannot set nested path `' + path + '`. ' + 'Parent path `' + fullPath + '` already set to type ' + branch[sub].name + '.'; throw new Error(msg); } branch = branch[sub]; } branch[last] = clone(obj); this.paths[path] = this.interpretAsType(path, obj, this.options); const schemaType = this.paths[path]; // If overwriting an existing path, make sure to clear the childSchemas this.childSchemas = this.childSchemas.filter(childSchema => childSchema.path !== path); if (schemaType.$isSchemaMap) { // Maps can have arbitrary keys, so `$*` is internal shorthand for "any key" // The '$' is to imply this path should never be stored in MongoDB so we // can easily build a regexp out of this path, and '*' to imply "any key." const mapPath = path + '.$*'; this.paths[mapPath] = schemaType.$__schemaType; this.mapPaths.push(this.paths[mapPath]); if (schemaType.$__schemaType.$isSingleNested) { this.childSchemas.push({ schema: schemaType.$__schemaType.schema, model: schemaType.$__schemaType.caster, path: path }); } } if (schemaType.$isSingleNested) { for (const key of Object.keys(schemaType.schema.paths)) { this.singleNestedPaths[path + '.' + key] = schemaType.schema.paths[key]; } for (const key of Object.keys(schemaType.schema.singleNestedPaths)) { this.singleNestedPaths[path + '.' + key] = schemaType.schema.singleNestedPaths[key]; } for (const key of Object.keys(schemaType.schema.subpaths)) { this.singleNestedPaths[path + '.' + key] = schemaType.schema.subpaths[key]; } for (const key of Object.keys(schemaType.schema.nested)) { this.singleNestedPaths[path + '.' + key] = 'nested'; } Object.defineProperty(schemaType.schema, 'base', { configurable: true, enumerable: false, writable: false, value: this.base }); schemaType.caster.base = this.base; this.childSchemas.push({ schema: schemaType.schema, model: schemaType.caster, path: path }); } else if (schemaType.$isMongooseDocumentArray) { Object.defineProperty(schemaType.schema, 'base', { configurable: true, enumerable: false, writable: false, value: this.base }); schemaType.casterConstructor.base = this.base; this.childSchemas.push({ schema: schemaType.schema, model: schemaType.casterConstructor, path: path }); } if (schemaType.$isMongooseArray && schemaType.caster instanceof SchemaType) { let arrayPath = path; let _schemaType = schemaType; const toAdd = []; while (_schemaType.$isMongooseArray) { arrayPath = arrayPath + '.$'; // Skip arrays of document arrays if (_schemaType.$isMongooseDocumentArray) { _schemaType.$embeddedSchemaType._arrayPath = arrayPath; _schemaType.$embeddedSchemaType._arrayParentPath = path; _schemaType = _schemaType.$embeddedSchemaType; } else { _schemaType.caster._arrayPath = arrayPath; _schemaType.caster._arrayParentPath = path; _schemaType = _schemaType.caster; } this.subpaths[arrayPath] = _schemaType; } for (const _schemaType of toAdd) { this.subpaths[_schemaType.path] = _schemaType; } } if (schemaType.$isMongooseDocumentArray) { for (const key of Object.keys(schemaType.schema.paths)) { const _schemaType = schemaType.schema.paths[key]; this.subpaths[path + '.' + key] = _schemaType; if (typeof _schemaType === 'object' && _schemaType != null && _schemaType.$parentSchemaDocArray == null) { _schemaType.$parentSchemaDocArray = schemaType; } } for (const key of Object.keys(schemaType.schema.subpaths)) { const _schemaType = schemaType.schema.subpaths[key]; this.subpaths[path + '.' + key] = _schemaType; if (typeof _schemaType === 'object' && _schemaType != null && _schemaType.$parentSchemaDocArray == null) { _schemaType.$parentSchemaDocArray = schemaType; } } for (const key of Object.keys(schemaType.schema.singleNestedPaths)) { const _schemaType = schemaType.schema.singleNestedPaths[key]; this.subpaths[path + '.' + key] = _schemaType; if (typeof _schemaType === 'object' && _schemaType != null && _schemaType.$parentSchemaDocArray == null) { _schemaType.$parentSchemaDocArray = schemaType; } } } return this; }; /*! * ignore */ Schema.prototype._gatherChildSchemas = function _gatherChildSchemas() { const childSchemas = []; for (const path of Object.keys(this.paths)) { if (typeof path !== 'string') { continue; } const schematype = this.paths[path]; if (schematype.$isMongooseDocumentArray || schematype.$isSingleNested) { childSchemas.push({ schema: schematype.schema, model: schematype.caster, path: path }); } else if (schematype.$isSchemaMap && schematype.$__schemaType.$isSingleNested) { childSchemas.push({ schema: schematype.$__schemaType.schema, model: schematype.$__schemaType.caster, path: path }); } } this.childSchemas = childSchemas; return childSchemas; }; /*! * ignore */ function _getPath(schema, path, cleanPath) { if (schema.paths.hasOwnProperty(path)) { return schema.paths[path]; } if (schema.subpaths.hasOwnProperty(cleanPath)) { const subpath = schema.subpaths[cleanPath]; if (subpath === 'nested') { return undefined; } return subpath; } if (schema.singleNestedPaths.hasOwnProperty(cleanPath) && typeof schema.singleNestedPaths[cleanPath] === 'object') { const singleNestedPath = schema.singleNestedPaths[cleanPath]; if (singleNestedPath === 'nested') { return undefined; } return singleNestedPath; } return null; } /*! * ignore */ function _pathToPositionalSyntax(path) { if (!/\.\d+/.test(path)) { return path; } return path.replace(/\.\d+\./g, '.$.').replace(/\.\d+$/, '.$'); } /*! * ignore */ function getMapPath(schema, path) { if (schema.mapPaths.length === 0) { return null; } for (const val of schema.mapPaths) { const cleanPath = val.path.replace(/\.\$\*/g, ''); if (path === cleanPath || (path.startsWith(cleanPath + '.') && path.slice(cleanPath.length + 1).indexOf('.') === -1)) { return val; } else if (val.schema && path.startsWith(cleanPath + '.')) { let remnant = path.slice(cleanPath.length + 1); remnant = remnant.slice(remnant.indexOf('.') + 1); return val.schema.paths[remnant]; } else if (val.$isSchemaMap && path.startsWith(cleanPath + '.')) {