UNPKG

@skybloxsystems/ticket-bot

Version:
1,666 lines (1,547 loc) 170 kB
'use strict'; /*! * Module dependencies. */ const CastError = require('./error/cast'); const DocumentNotFoundError = require('./error/notFound'); const Kareem = require('kareem'); const MongooseError = require('./error/mongooseError'); const ObjectParameterError = require('./error/objectParameter'); const QueryCursor = require('./cursor/QueryCursor'); const ReadPreference = require('./driver').get().ReadPreference; const applyGlobalMaxTimeMS = require('./helpers/query/applyGlobalMaxTimeMS'); const applyWriteConcern = require('./helpers/schema/applyWriteConcern'); const cast = require('./cast'); const castArrayFilters = require('./helpers/update/castArrayFilters'); const castUpdate = require('./helpers/query/castUpdate'); const completeMany = require('./helpers/query/completeMany'); const get = require('./helpers/get'); const promiseOrCallback = require('./helpers/promiseOrCallback'); const getDiscriminatorByValue = require('./helpers/discriminator/getDiscriminatorByValue'); const hasDollarKeys = require('./helpers/query/hasDollarKeys'); const helpers = require('./queryhelpers'); const immediate = require('./helpers/immediate'); const isExclusive = require('./helpers/projection/isExclusive'); const isInclusive = require('./helpers/projection/isInclusive'); const isSubpath = require('./helpers/projection/isSubpath'); const mquery = require('mquery'); const parseProjection = require('./helpers/projection/parseProjection'); const removeUnusedArrayFilters = require('./helpers/update/removeUnusedArrayFilters'); const sanitizeFilter = require('./helpers/query/sanitizeFilter'); const sanitizeProjection = require('./helpers/query/sanitizeProjection'); const selectPopulatedFields = require('./helpers/query/selectPopulatedFields'); const setDefaultsOnInsert = require('./helpers/setDefaultsOnInsert'); const slice = require('sliced'); const updateValidators = require('./helpers/updateValidators'); const util = require('util'); const utils = require('./utils'); const validOps = require('./helpers/query/validOps'); const wrapThunk = require('./helpers/query/wrapThunk'); /** * Query constructor used for building queries. You do not need * to instantiate a `Query` directly. Instead use Model functions like * [`Model.find()`](/docs/api.html#find_find). * * ####Example: * * const query = MyModel.find(); // `query` is an instance of `Query` * query.setOptions({ lean : true }); * query.collection(MyModel.collection); * query.where('age').gte(21).exec(callback); * * // You can instantiate a query directly. There is no need to do * // this unless you're an advanced user with a very good reason to. * const query = new mongoose.Query(); * * @param {Object} [options] * @param {Object} [model] * @param {Object} [conditions] * @param {Object} [collection] Mongoose collection * @api public */ function Query(conditions, options, model, collection) { // this stuff is for dealing with custom queries created by #toConstructor if (!this._mongooseOptions) { this._mongooseOptions = {}; } options = options || {}; this._transforms = []; this._hooks = new Kareem(); this._executionStack = null; // this is the case where we have a CustomQuery, we need to check if we got // options passed in, and if we did, merge them in const keys = Object.keys(options); for (const key of keys) { this._mongooseOptions[key] = options[key]; } if (collection) { this.mongooseCollection = collection; } if (model) { this.model = model; this.schema = model.schema; } // this is needed because map reduce returns a model that can be queried, but // all of the queries on said model should be lean if (this.model && this.model._mapreduce) { this.lean(); } // inherit mquery mquery.call(this, this.mongooseCollection, options); if (conditions) { this.find(conditions); } this.options = this.options || {}; // For gh-6880. mquery still needs to support `fields` by default for old // versions of MongoDB this.$useProjection = true; const collation = get(this, 'schema.options.collation', null); if (collation != null) { this.options.collation = collation; } } /*! * inherit mquery */ Query.prototype = new mquery; Query.prototype.constructor = Query; Query.base = mquery.prototype; /** * Flag to opt out of using `$geoWithin`. * * mongoose.Query.use$geoWithin = false; * * MongoDB 2.4 deprecated the use of `$within`, replacing it with `$geoWithin`. Mongoose uses `$geoWithin` by default (which is 100% backward compatible with `$within`). If you are running an older version of MongoDB, set this flag to `false` so your `within()` queries continue to work. * * @see http://docs.mongodb.org/manual/reference/operator/geoWithin/ * @default true * @property use$geoWithin * @memberOf Query * @receiver Query * @api public */ Query.use$geoWithin = mquery.use$geoWithin; /** * Converts this query to a customized, reusable query constructor with all arguments and options retained. * * ####Example * * // Create a query for adventure movies and read from the primary * // node in the replica-set unless it is down, in which case we'll * // read from a secondary node. * const query = Movie.find({ tags: 'adventure' }).read('primaryPreferred'); * * // create a custom Query constructor based off these settings * const Adventure = query.toConstructor(); * * // Adventure is now a subclass of mongoose.Query and works the same way but with the * // default query parameters and options set. * Adventure().exec(callback) * * // further narrow down our query results while still using the previous settings * Adventure().where({ name: /^Life/ }).exec(callback); * * // since Adventure is a stand-alone constructor we can also add our own * // helper methods and getters without impacting global queries * Adventure.prototype.startsWith = function (prefix) { * this.where({ name: new RegExp('^' + prefix) }) * return this; * } * Object.defineProperty(Adventure.prototype, 'highlyRated', { * get: function () { * this.where({ rating: { $gt: 4.5 }}); * return this; * } * }) * Adventure().highlyRated.startsWith('Life').exec(callback) * * @return {Query} subclass-of-Query * @api public */ Query.prototype.toConstructor = function toConstructor() { const model = this.model; const coll = this.mongooseCollection; const CustomQuery = function(criteria, options) { if (!(this instanceof CustomQuery)) { return new CustomQuery(criteria, options); } this._mongooseOptions = utils.clone(p._mongooseOptions); Query.call(this, criteria, options || null, model, coll); }; util.inherits(CustomQuery, model.Query); // set inherited defaults const p = CustomQuery.prototype; p.options = {}; // Need to handle `sort()` separately because entries-style `sort()` syntax // `sort([['prop1', 1]])` confuses mquery into losing the outer nested array. // See gh-8159 const options = Object.assign({}, this.options); if (options.sort != null) { p.sort(options.sort); delete options.sort; } p.setOptions(options); p.op = this.op; p._validateOp(); p._conditions = utils.clone(this._conditions); p._fields = utils.clone(this._fields); p._update = utils.clone(this._update, { flattenDecimals: false }); p._path = this._path; p._distinct = this._distinct; p._collection = this._collection; p._mongooseOptions = this._mongooseOptions; return CustomQuery; }; /** * Make a copy of this query so you can re-execute it. * * ####Example: * const q = Book.findOne({ title: 'Casino Royale' }); * await q.exec(); * await q.exec(); // Throws an error because you can't execute a query twice * * await q.clone().exec(); // Works * * @method clone * @return {Query} copy * @memberOf Query * @instance * @api public */ Query.prototype.clone = function clone() { const model = this.model; const collection = this.mongooseCollection; const q = new this.constructor({}, {}, model, collection); // Need to handle `sort()` separately because entries-style `sort()` syntax // `sort([['prop1', 1]])` confuses mquery into losing the outer nested array. // See gh-8159 const options = Object.assign({}, this.options); if (options.sort != null) { q.sort(options.sort); delete options.sort; } q.setOptions(options); q.op = this.op; q._validateOp(); q._conditions = utils.clone(this._conditions); q._fields = utils.clone(this._fields); q._update = utils.clone(this._update, { flattenDecimals: false }); q._path = this._path; q._distinct = this._distinct; q._collection = this._collection; q._mongooseOptions = this._mongooseOptions; return q; }; /** * Specifies a javascript function or expression to pass to MongoDBs query system. * * ####Example * * query.$where('this.comments.length === 10 || this.name.length === 5') * * // or * * query.$where(function () { * return this.comments.length === 10 || this.name.length === 5; * }) * * ####NOTE: * * Only use `$where` when you have a condition that cannot be met using other MongoDB operators like `$lt`. * **Be sure to read about all of [its caveats](http://docs.mongodb.org/manual/reference/operator/where/) before using.** * * @see $where http://docs.mongodb.org/manual/reference/operator/where/ * @method $where * @param {String|Function} js javascript string or function * @return {Query} this * @memberOf Query * @instance * @method $where * @api public */ /** * Specifies a `path` for use with chaining. * * ####Example * * // instead of writing: * User.find({age: {$gte: 21, $lte: 65}}, callback); * * // we can instead write: * User.where('age').gte(21).lte(65); * * // passing query conditions is permitted * User.find().where({ name: 'vonderful' }) * * // chaining * User * .where('age').gte(21).lte(65) * .where('name', /^vonderful/i) * .where('friends').slice(10) * .exec(callback) * * @method where * @memberOf Query * @instance * @param {String|Object} [path] * @param {any} [val] * @return {Query} this * @api public */ /** * Specifies a `$slice` projection for an array. * * ####Example * * query.slice('comments', 5) * query.slice('comments', -5) * query.slice('comments', [10, 5]) * query.where('comments').slice(5) * query.where('comments').slice([-10, 5]) * * @method slice * @memberOf Query * @instance * @param {String} [path] * @param {Number} val number/range of elements to slice * @return {Query} this * @see mongodb http://www.mongodb.org/display/DOCS/Retrieving+a+Subset+of+Fields#RetrievingaSubsetofFields-RetrievingaSubrangeofArrayElements * @see $slice http://docs.mongodb.org/manual/reference/projection/slice/#prj._S_slice * @api public */ Query.prototype.slice = function() { if (arguments.length === 0) { return this; } this._validate('slice'); let path; let val; if (arguments.length === 1) { const arg = arguments[0]; if (typeof arg === 'object' && !Array.isArray(arg)) { const keys = Object.keys(arg); const numKeys = keys.length; for (let i = 0; i < numKeys; ++i) { this.slice(keys[i], arg[keys[i]]); } return this; } this._ensurePath('slice'); path = this._path; val = arguments[0]; } else if (arguments.length === 2) { if ('number' === typeof arguments[0]) { this._ensurePath('slice'); path = this._path; val = slice(arguments); } else { path = arguments[0]; val = arguments[1]; } } else if (arguments.length === 3) { path = arguments[0]; val = slice(arguments, 1); } const p = {}; p[path] = { $slice: val }; this.select(p); return this; }; /*! * ignore */ const validOpsSet = new Set(validOps); Query.prototype._validateOp = function() { if (this.op != null && !validOpsSet.has(this.op)) { this.error(new Error('Query has invalid `op`: "' + this.op + '"')); } }; /** * Specifies the complementary comparison value for paths specified with `where()` * * ####Example * * User.where('age').equals(49); * * // is the same as * * User.where('age', 49); * * @method equals * @memberOf Query * @instance * @param {Object} val * @return {Query} this * @api public */ /** * Specifies arguments for an `$or` condition. * * ####Example * * query.or([{ color: 'red' }, { status: 'emergency' }]) * * @see $or http://docs.mongodb.org/manual/reference/operator/or/ * @method or * @memberOf Query * @instance * @param {Array} array array of conditions * @return {Query} this * @api public */ /** * Specifies arguments for a `$nor` condition. * * ####Example * * query.nor([{ color: 'green' }, { status: 'ok' }]) * * @see $nor http://docs.mongodb.org/manual/reference/operator/nor/ * @method nor * @memberOf Query * @instance * @param {Array} array array of conditions * @return {Query} this * @api public */ /** * Specifies arguments for a `$and` condition. * * ####Example * * query.and([{ color: 'green' }, { status: 'ok' }]) * * @method and * @memberOf Query * @instance * @see $and http://docs.mongodb.org/manual/reference/operator/and/ * @param {Array} array array of conditions * @return {Query} this * @api public */ /** * Specifies a `$gt` query condition. * * When called with one argument, the most recent path passed to `where()` is used. * * ####Example * * Thing.find().where('age').gt(21) * * // or * Thing.find().gt('age', 21) * * @method gt * @memberOf Query * @instance * @param {String} [path] * @param {Number} val * @see $gt http://docs.mongodb.org/manual/reference/operator/gt/ * @api public */ /** * Specifies a `$gte` query condition. * * When called with one argument, the most recent path passed to `where()` is used. * * @method gte * @memberOf Query * @instance * @param {String} [path] * @param {Number} val * @see $gte http://docs.mongodb.org/manual/reference/operator/gte/ * @api public */ /** * Specifies a `$lt` query condition. * * When called with one argument, the most recent path passed to `where()` is used. * * @method lt * @memberOf Query * @instance * @param {String} [path] * @param {Number} val * @see $lt http://docs.mongodb.org/manual/reference/operator/lt/ * @api public */ /** * Specifies a `$lte` query condition. * * When called with one argument, the most recent path passed to `where()` is used. * * @method lte * @see $lte http://docs.mongodb.org/manual/reference/operator/lte/ * @memberOf Query * @instance * @param {String} [path] * @param {Number} val * @api public */ /** * Specifies a `$ne` query condition. * * When called with one argument, the most recent path passed to `where()` is used. * * @see $ne http://docs.mongodb.org/manual/reference/operator/ne/ * @method ne * @memberOf Query * @instance * @param {String} [path] * @param {any} val * @api public */ /** * Specifies an `$in` query condition. * * When called with one argument, the most recent path passed to `where()` is used. * * @see $in http://docs.mongodb.org/manual/reference/operator/in/ * @method in * @memberOf Query * @instance * @param {String} [path] * @param {Array} val * @api public */ /** * Specifies an `$nin` query condition. * * When called with one argument, the most recent path passed to `where()` is used. * * @see $nin http://docs.mongodb.org/manual/reference/operator/nin/ * @method nin * @memberOf Query * @instance * @param {String} [path] * @param {Array} val * @api public */ /** * Specifies an `$all` query condition. * * When called with one argument, the most recent path passed to `where()` is used. * * ####Example: * * MyModel.find().where('pets').all(['dog', 'cat', 'ferret']); * // Equivalent: * MyModel.find().all('pets', ['dog', 'cat', 'ferret']); * * @see $all http://docs.mongodb.org/manual/reference/operator/all/ * @method all * @memberOf Query * @instance * @param {String} [path] * @param {Array} val * @api public */ /** * Specifies a `$size` query condition. * * When called with one argument, the most recent path passed to `where()` is used. * * ####Example * * const docs = await MyModel.where('tags').size(0).exec(); * assert(Array.isArray(docs)); * console.log('documents with 0 tags', docs); * * @see $size http://docs.mongodb.org/manual/reference/operator/size/ * @method size * @memberOf Query * @instance * @param {String} [path] * @param {Number} val * @api public */ /** * Specifies a `$regex` query condition. * * When called with one argument, the most recent path passed to `where()` is used. * * @see $regex http://docs.mongodb.org/manual/reference/operator/regex/ * @method regex * @memberOf Query * @instance * @param {String} [path] * @param {String|RegExp} val * @api public */ /** * Specifies a `maxDistance` query condition. * * When called with one argument, the most recent path passed to `where()` is used. * * @see $maxDistance http://docs.mongodb.org/manual/reference/operator/maxDistance/ * @method maxDistance * @memberOf Query * @instance * @param {String} [path] * @param {Number} val * @api public */ /** * Specifies a `$mod` condition, filters documents for documents whose * `path` property is a number that is equal to `remainder` modulo `divisor`. * * ####Example * * // All find products whose inventory is odd * Product.find().mod('inventory', [2, 1]); * Product.find().where('inventory').mod([2, 1]); * // This syntax is a little strange, but supported. * Product.find().where('inventory').mod(2, 1); * * @method mod * @memberOf Query * @instance * @param {String} [path] * @param {Array} val must be of length 2, first element is `divisor`, 2nd element is `remainder`. * @return {Query} this * @see $mod http://docs.mongodb.org/manual/reference/operator/mod/ * @api public */ Query.prototype.mod = function() { let val; let path; if (arguments.length === 1) { this._ensurePath('mod'); val = arguments[0]; path = this._path; } else if (arguments.length === 2 && !Array.isArray(arguments[1])) { this._ensurePath('mod'); val = slice(arguments); path = this._path; } else if (arguments.length === 3) { val = slice(arguments, 1); path = arguments[0]; } else { val = arguments[1]; path = arguments[0]; } const conds = this._conditions[path] || (this._conditions[path] = {}); conds.$mod = val; return this; }; /** * Specifies an `$exists` condition * * ####Example * * // { name: { $exists: true }} * Thing.where('name').exists() * Thing.where('name').exists(true) * Thing.find().exists('name') * * // { name: { $exists: false }} * Thing.where('name').exists(false); * Thing.find().exists('name', false); * * @method exists * @memberOf Query * @instance * @param {String} [path] * @param {Boolean} val * @return {Query} this * @see $exists http://docs.mongodb.org/manual/reference/operator/exists/ * @api public */ /** * Specifies an `$elemMatch` condition * * ####Example * * query.elemMatch('comment', { author: 'autobot', votes: {$gte: 5}}) * * query.where('comment').elemMatch({ author: 'autobot', votes: {$gte: 5}}) * * query.elemMatch('comment', function (elem) { * elem.where('author').equals('autobot'); * elem.where('votes').gte(5); * }) * * query.where('comment').elemMatch(function (elem) { * elem.where({ author: 'autobot' }); * elem.where('votes').gte(5); * }) * * @method elemMatch * @memberOf Query * @instance * @param {String|Object|Function} path * @param {Object|Function} filter * @return {Query} this * @see $elemMatch http://docs.mongodb.org/manual/reference/operator/elemMatch/ * @api public */ /** * Defines a `$within` or `$geoWithin` argument for geo-spatial queries. * * ####Example * * query.where(path).within().box() * query.where(path).within().circle() * query.where(path).within().geometry() * * query.where('loc').within({ center: [50,50], radius: 10, unique: true, spherical: true }); * query.where('loc').within({ box: [[40.73, -73.9], [40.7, -73.988]] }); * query.where('loc').within({ polygon: [[],[],[],[]] }); * * query.where('loc').within([], [], []) // polygon * query.where('loc').within([], []) // box * query.where('loc').within({ type: 'LineString', coordinates: [...] }); // geometry * * **MUST** be used after `where()`. * * ####NOTE: * * As of Mongoose 3.7, `$geoWithin` is always used for queries. To change this behavior, see [Query.use$geoWithin](#query_Query-use%2524geoWithin). * * ####NOTE: * * In Mongoose 3.7, `within` changed from a getter to a function. If you need the old syntax, use [this](https://github.com/ebensing/mongoose-within). * * @method within * @see $polygon http://docs.mongodb.org/manual/reference/operator/polygon/ * @see $box http://docs.mongodb.org/manual/reference/operator/box/ * @see $geometry http://docs.mongodb.org/manual/reference/operator/geometry/ * @see $center http://docs.mongodb.org/manual/reference/operator/center/ * @see $centerSphere http://docs.mongodb.org/manual/reference/operator/centerSphere/ * @memberOf Query * @instance * @return {Query} this * @api public */ /** * Specifies the maximum number of documents the query will return. * * ####Example * * query.limit(20) * * ####Note * * Cannot be used with `distinct()` * * @method limit * @memberOf Query * @instance * @param {Number} val * @api public */ /** * Specifies the number of documents to skip. * * ####Example * * query.skip(100).limit(20) * * ####Note * * Cannot be used with `distinct()` * * @method skip * @memberOf Query * @instance * @param {Number} val * @see cursor.skip http://docs.mongodb.org/manual/reference/method/cursor.skip/ * @api public */ /** * Specifies the maxScan option. * * ####Example * * query.maxScan(100) * * ####Note * * Cannot be used with `distinct()` * * @method maxScan * @memberOf Query * @instance * @param {Number} val * @see maxScan http://docs.mongodb.org/manual/reference/operator/maxScan/ * @api public */ /** * Specifies the batchSize option. * * ####Example * * query.batchSize(100) * * ####Note * * Cannot be used with `distinct()` * * @method batchSize * @memberOf Query * @instance * @param {Number} val * @see batchSize http://docs.mongodb.org/manual/reference/method/cursor.batchSize/ * @api public */ /** * Specifies the `comment` option. * * ####Example * * query.comment('login query') * * ####Note * * Cannot be used with `distinct()` * * @method comment * @memberOf Query * @instance * @param {String} val * @see comment http://docs.mongodb.org/manual/reference/operator/comment/ * @api public */ /** * Specifies this query as a `snapshot` query. * * ####Example * * query.snapshot() // true * query.snapshot(true) * query.snapshot(false) * * ####Note * * Cannot be used with `distinct()` * * @method snapshot * @memberOf Query * @instance * @see snapshot http://docs.mongodb.org/manual/reference/operator/snapshot/ * @return {Query} this * @api public */ /** * Sets query hints. * * ####Example * * query.hint({ indexA: 1, indexB: -1}) * * ####Note * * Cannot be used with `distinct()` * * @method hint * @memberOf Query * @instance * @param {Object} val a hint object * @return {Query} this * @see $hint http://docs.mongodb.org/manual/reference/operator/hint/ * @api public */ /** * Get/set the current projection (AKA fields). Pass `null` to remove the * current projection. * * Unlike `projection()`, the `select()` function modifies the current * projection in place. This function overwrites the existing projection. * * ####Example: * * const q = Model.find(); * q.projection(); // null * * q.select('a b'); * q.projection(); // { a: 1, b: 1 } * * q.projection({ c: 1 }); * q.projection(); // { c: 1 } * * q.projection(null); * q.projection(); // null * * * @method projection * @memberOf Query * @instance * @param {Object|null} arg * @return {Object} the current projection * @api public */ Query.prototype.projection = function(arg) { if (arguments.length === 0) { return this._fields; } this._fields = {}; this._userProvidedFields = {}; this.select(arg); return this._fields; }; /** * Specifies which document fields to include or exclude (also known as the query "projection") * * When using string syntax, prefixing a path with `-` will flag that path as excluded. When a path does not have the `-` prefix, it is included. Lastly, if a path is prefixed with `+`, it forces inclusion of the path, which is useful for paths excluded at the [schema level](/docs/api.html#schematype_SchemaType-select). * * A projection _must_ be either inclusive or exclusive. In other words, you must * either list the fields to include (which excludes all others), or list the fields * to exclude (which implies all other fields are included). The [`_id` field is the only exception because MongoDB includes it by default](https://docs.mongodb.com/manual/tutorial/project-fields-from-query-results/#suppress-id-field). * * ####Example * * // include a and b, exclude other fields * query.select('a b'); * // Equivalent syntaxes: * query.select(['a', 'b']); * query.select({ a: 1, b: 1 }); * * // exclude c and d, include other fields * query.select('-c -d'); * * // Use `+` to override schema-level `select: false` without making the * // projection inclusive. * const schema = new Schema({ * foo: { type: String, select: false }, * bar: String * }); * // ... * query.select('+foo'); // Override foo's `select: false` without excluding `bar` * * // or you may use object notation, useful when * // you have keys already prefixed with a "-" * query.select({ a: 1, b: 1 }); * query.select({ c: 0, d: 0 }); * * Additional calls to select can override the previous selection: * query.select({ a: 1, b: 1 }).select({ b: 0 }); // selection is now { a: 1 } * query.select({ a: 0, b: 0 }).select({ b: 1 }); // selection is now { a: 0 } * * * @method select * @memberOf Query * @instance * @param {Object|String|Array<String>} arg * @return {Query} this * @see SchemaType * @api public */ Query.prototype.select = function select() { let arg = arguments[0]; if (!arg) return this; if (arguments.length !== 1) { throw new Error('Invalid select: select only takes 1 argument'); } this._validate('select'); const fields = this._fields || (this._fields = {}); const userProvidedFields = this._userProvidedFields || (this._userProvidedFields = {}); let sanitizeProjection = undefined; if (this.model != null && utils.hasUserDefinedProperty(this.model.db.options, 'sanitizeProjection')) { sanitizeProjection = this.model.db.options.sanitizeProjection; } else if (this.model != null && utils.hasUserDefinedProperty(this.model.base.options, 'sanitizeProjection')) { sanitizeProjection = this.model.base.options.sanitizeProjection; } else { sanitizeProjection = this._mongooseOptions.sanitizeProjection; } function sanitizeValue(value) { return typeof value === 'string' && sanitizeProjection ? value = 1 : value; } arg = parseProjection(arg); if (utils.isObject(arg)) { if (this.selectedInclusively()) { Object.entries(arg).forEach(([key, value]) => { if (value) { // Add the field to the projection fields[key] = userProvidedFields[key] = sanitizeValue(value); } else { // Remove the field from the projection Object.keys(userProvidedFields).forEach(field => { if (isSubpath(key, field)) { delete fields[field]; delete userProvidedFields[field]; } }); } }); } else if (this.selectedExclusively()) { Object.entries(arg).forEach(([key, value]) => { if (!value) { // Add the field to the projection fields[key] = userProvidedFields[key] = sanitizeValue(value); } else { // Remove the field from the projection Object.keys(userProvidedFields).forEach(field => { if (isSubpath(key, field)) { delete fields[field]; delete userProvidedFields[field]; } }); } }); } else { const keys = Object.keys(arg); for (let i = 0; i < keys.length; ++i) { const value = arg[keys[i]]; fields[keys[i]] = sanitizeValue(value); userProvidedFields[keys[i]] = sanitizeValue(value); } } return this; } throw new TypeError('Invalid select() argument. Must be string or object.'); }; /** * _DEPRECATED_ Sets the slaveOk option. * * **Deprecated** in MongoDB 2.2 in favor of [read preferences](#query_Query-read). * * ####Example: * * query.slaveOk() // true * query.slaveOk(true) * query.slaveOk(false) * * @method slaveOk * @memberOf Query * @instance * @deprecated use read() preferences instead if on mongodb >= 2.2 * @param {Boolean} v defaults to true * @see mongodb http://docs.mongodb.org/manual/applications/replication/#read-preference * @see slaveOk http://docs.mongodb.org/manual/reference/method/rs.slaveOk/ * @see read() #query_Query-read * @return {Query} this * @api public */ /** * Determines the MongoDB nodes from which to read. * * ####Preferences: * * primary - (default) Read from primary only. Operations will produce an error if primary is unavailable. Cannot be combined with tags. * secondary Read from secondary if available, otherwise error. * primaryPreferred Read from primary if available, otherwise a secondary. * secondaryPreferred Read from a secondary if available, otherwise read from the primary. * nearest All operations read from among the nearest candidates, but unlike other modes, this option will include both the primary and all secondaries in the random selection. * * Aliases * * p primary * pp primaryPreferred * s secondary * sp secondaryPreferred * n nearest * * ####Example: * * new Query().read('primary') * new Query().read('p') // same as primary * * new Query().read('primaryPreferred') * new Query().read('pp') // same as primaryPreferred * * new Query().read('secondary') * new Query().read('s') // same as secondary * * new Query().read('secondaryPreferred') * new Query().read('sp') // same as secondaryPreferred * * new Query().read('nearest') * new Query().read('n') // same as nearest * * // read from secondaries with matching tags * new Query().read('s', [{ dc:'sf', s: 1 },{ dc:'ma', s: 2 }]) * * Read more about how to use read preferences [here](http://docs.mongodb.org/manual/applications/replication/#read-preference) and [here](http://mongodb.github.com/node-mongodb-native/driver-articles/anintroductionto1_1and2_2.html#read-preferences). * * @method read * @memberOf Query * @instance * @param {String} pref one of the listed preference options or aliases * @param {Array} [tags] optional tags for this query * @see mongodb http://docs.mongodb.org/manual/applications/replication/#read-preference * @see driver http://mongodb.github.com/node-mongodb-native/driver-articles/anintroductionto1_1and2_2.html#read-preferences * @return {Query} this * @api public */ Query.prototype.read = function read(pref, tags) { // first cast into a ReadPreference object to support tags const read = new ReadPreference(pref, tags); this.options.readPreference = read; return this; }; /*! * ignore */ Query.prototype.toString = function toString() { if (this.op === 'count' || this.op === 'countDocuments' || this.op === 'find' || this.op === 'findOne' || this.op === 'deleteMany' || this.op === 'deleteOne' || this.op === 'findOneAndDelete' || this.op === 'findOneAndRemove' || this.op === 'remove') { return `${this.model.modelName}.${this.op}(${util.inspect(this._conditions)})`; } if (this.op === 'distinct') { return `${this.model.modelName}.distinct('${this._distinct}', ${util.inspect(this._conditions)})`; } if (this.op === 'findOneAndReplace' || this.op === 'findOneAndUpdate' || this.op === 'replaceOne' || this.op === 'update' || this.op === 'updateMany' || this.op === 'updateOne') { return `${this.model.modelName}.${this.op}(${util.inspect(this._conditions)}, ${util.inspect(this._update)})`; } // 'estimatedDocumentCount' or any others return `${this.model.modelName}.${this.op}()`; }; /** * Sets the [MongoDB session](https://docs.mongodb.com/manual/reference/server-sessions/) * associated with this query. Sessions are how you mark a query as part of a * [transaction](/docs/transactions.html). * * Calling `session(null)` removes the session from this query. * * ####Example: * * const s = await mongoose.startSession(); * await mongoose.model('Person').findOne({ name: 'Axl Rose' }).session(s); * * @method session * @memberOf Query * @instance * @param {ClientSession} [session] from `await conn.startSession()` * @see Connection.prototype.startSession() /docs/api.html#connection_Connection-startSession * @see mongoose.startSession() /docs/api.html#mongoose_Mongoose-startSession * @return {Query} this * @api public */ Query.prototype.session = function session(v) { if (v == null) { delete this.options.session; } this.options.session = v; return this; }; /** * Sets the 3 write concern parameters for this query: * * - `w`: Sets the specified number of `mongod` servers, or tag set of `mongod` servers, that must acknowledge this write before this write is considered successful. * - `j`: Boolean, set to `true` to request acknowledgement that this operation has been persisted to MongoDB's on-disk journal. * - `wtimeout`: If [`w > 1`](/docs/api.html#query_Query-w), the maximum amount of time to wait for this write to propagate through the replica set before this operation fails. The default is `0`, which means no timeout. * * This option is only valid for operations that write to the database: * * - `deleteOne()` * - `deleteMany()` * - `findOneAndDelete()` * - `findOneAndReplace()` * - `findOneAndUpdate()` * - `remove()` * - `update()` * - `updateOne()` * - `updateMany()` * * Defaults to the schema's [`writeConcern` option](/docs/guide.html#writeConcern) * * ####Example: * * // The 'majority' option means the `deleteOne()` promise won't resolve * // until the `deleteOne()` has propagated to the majority of the replica set * await mongoose.model('Person'). * deleteOne({ name: 'Ned Stark' }). * writeConcern({ w: 'majority' }); * * @method writeConcern * @memberOf Query * @instance * @param {Object} writeConcern the write concern value to set * @see mongodb https://mongodb.github.io/node-mongodb-native/3.1/api/global.html#WriteConcern * @return {Query} this * @api public */ Query.prototype.writeConcern = function writeConcern(val) { if (val == null) { delete this.options.writeConcern; return this; } this.options.writeConcern = val; return this; }; /** * Sets the specified number of `mongod` servers, or tag set of `mongod` servers, * that must acknowledge this write before this write is considered successful. * This option is only valid for operations that write to the database: * * - `deleteOne()` * - `deleteMany()` * - `findOneAndDelete()` * - `findOneAndReplace()` * - `findOneAndUpdate()` * - `remove()` * - `update()` * - `updateOne()` * - `updateMany()` * * Defaults to the schema's [`writeConcern.w` option](/docs/guide.html#writeConcern) * * ####Example: * * // The 'majority' option means the `deleteOne()` promise won't resolve * // until the `deleteOne()` has propagated to the majority of the replica set * await mongoose.model('Person'). * deleteOne({ name: 'Ned Stark' }). * w('majority'); * * @method w * @memberOf Query * @instance * @param {String|number} val 0 for fire-and-forget, 1 for acknowledged by one server, 'majority' for majority of the replica set, or [any of the more advanced options](https://docs.mongodb.com/manual/reference/write-concern/#w-option). * @see mongodb https://docs.mongodb.com/manual/reference/write-concern/#w-option * @return {Query} this * @api public */ Query.prototype.w = function w(val) { if (val == null) { delete this.options.w; } if (this.options.writeConcern != null) { this.options.writeConcern.w = val; } else { this.options.w = val; } return this; }; /** * Requests acknowledgement that this operation has been persisted to MongoDB's * on-disk journal. * This option is only valid for operations that write to the database: * * - `deleteOne()` * - `deleteMany()` * - `findOneAndDelete()` * - `findOneAndReplace()` * - `findOneAndUpdate()` * - `remove()` * - `update()` * - `updateOne()` * - `updateMany()` * * Defaults to the schema's [`writeConcern.j` option](/docs/guide.html#writeConcern) * * ####Example: * * await mongoose.model('Person').deleteOne({ name: 'Ned Stark' }).j(true); * * @method j * @memberOf Query * @instance * @param {boolean} val * @see mongodb https://docs.mongodb.com/manual/reference/write-concern/#j-option * @return {Query} this * @api public */ Query.prototype.j = function j(val) { if (val == null) { delete this.options.j; } if (this.options.writeConcern != null) { this.options.writeConcern.j = val; } else { this.options.j = val; } return this; }; /** * If [`w > 1`](/docs/api.html#query_Query-w), the maximum amount of time to * wait for this write to propagate through the replica set before this * operation fails. The default is `0`, which means no timeout. * * This option is only valid for operations that write to the database: * * - `deleteOne()` * - `deleteMany()` * - `findOneAndDelete()` * - `findOneAndReplace()` * - `findOneAndUpdate()` * - `remove()` * - `update()` * - `updateOne()` * - `updateMany()` * * Defaults to the schema's [`writeConcern.wtimeout` option](/docs/guide.html#writeConcern) * * ####Example: * * // The `deleteOne()` promise won't resolve until this `deleteOne()` has * // propagated to at least `w = 2` members of the replica set. If it takes * // longer than 1 second, this `deleteOne()` will fail. * await mongoose.model('Person'). * deleteOne({ name: 'Ned Stark' }). * w(2). * wtimeout(1000); * * @method wtimeout * @memberOf Query * @instance * @param {number} ms number of milliseconds to wait * @see mongodb https://docs.mongodb.com/manual/reference/write-concern/#wtimeout * @return {Query} this * @api public */ Query.prototype.wtimeout = function wtimeout(ms) { if (ms == null) { delete this.options.wtimeout; } if (this.options.writeConcern != null) { this.options.writeConcern.wtimeout = ms; } else { this.options.wtimeout = ms; } return this; }; /** * Sets the readConcern option for the query. * * ####Example: * * new Query().readConcern('local') * new Query().readConcern('l') // same as local * * new Query().readConcern('available') * new Query().readConcern('a') // same as available * * new Query().readConcern('majority') * new Query().readConcern('m') // same as majority * * new Query().readConcern('linearizable') * new Query().readConcern('lz') // same as linearizable * * new Query().readConcern('snapshot') * new Query().readConcern('s') // same as snapshot * * * ####Read Concern Level: * * local MongoDB 3.2+ The query returns from the instance with no guarantee guarantee that the data has been written to a majority of the replica set members (i.e. may be rolled back). * available MongoDB 3.6+ The query returns from the instance with no guarantee guarantee that the data has been written to a majority of the replica set members (i.e. may be rolled back). * majority MongoDB 3.2+ The query returns the data that has been acknowledged by a majority of the replica set members. The documents returned by the read operation are durable, even in the event of failure. * linearizable MongoDB 3.4+ The query returns data that reflects all successful majority-acknowledged writes that completed prior to the start of the read operation. The query may wait for concurrently executing writes to propagate to a majority of replica set members before returning results. * snapshot MongoDB 4.0+ Only available for operations within multi-document transactions. Upon transaction commit with write concern "majority", the transaction operations are guaranteed to have read from a snapshot of majority-committed data. * * Aliases * * l local * a available * m majority * lz linearizable * s snapshot * * Read more about how to use read concern [here](https://docs.mongodb.com/manual/reference/read-concern/). * * @memberOf Query * @method readConcern * @param {String} level one of the listed read concern level or their aliases * @see mongodb https://docs.mongodb.com/manual/reference/read-concern/ * @return {Query} this * @api public */ /** * Gets query options. * * ####Example: * * const query = new Query(); * query.limit(10); * query.setOptions({ maxTimeMS: 1000 }) * query.getOptions(); // { limit: 10, maxTimeMS: 1000 } * * @return {Object} the options * @api public */ Query.prototype.getOptions = function() { return this.options; }; /** * Sets query options. Some options only make sense for certain operations. * * ####Options: * * The following options are only for `find()`: * * - [tailable](http://www.mongodb.org/display/DOCS/Tailable+Cursors) * - [sort](http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%7B%7Bsort(\)%7D%7D) * - [limit](http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%7B%7Blimit%28%29%7D%7D) * - [skip](http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%7B%7Bskip%28%29%7D%7D) * - [allowDiskUse](https://docs.mongodb.com/manual/reference/method/cursor.allowDiskUse/) * - [batchSize](http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%7B%7BbatchSize%28%29%7D%7D) * - [readPreference](http://docs.mongodb.org/manual/applications/replication/#read-preference) * - [hint](http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%24hint) * - [comment](http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%24comment) * - [snapshot](http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%7B%7Bsnapshot%28%29%7D%7D) * - [maxscan](https://docs.mongodb.org/v3.2/reference/operator/meta/maxScan/#metaOp._S_maxScan) * * The following options are only for write operations: `update()`, `updateOne()`, `updateMany()`, `replaceOne()`, `findOneAndUpdate()`, and `findByIdAndUpdate()`: * * - [upsert](https://docs.mongodb.com/manual/reference/method/db.collection.update/) * - [writeConcern](https://docs.mongodb.com/manual/reference/method/db.collection.update/) * - [timestamps](https://mongoosejs.com/docs/guide.html#timestamps): If `timestamps` is set in the schema, set this option to `false` to skip timestamps for that particular update. Has no effect if `timestamps` is not enabled in the schema options. * - overwriteDiscriminatorKey: allow setting the discriminator key in the update. Will use the correct discriminator schema if the update changes the discriminator key. * * The following options are only for `find()`, `findOne()`, `findById()`, `findOneAndUpdate()`, and `findByIdAndUpdate()`: * * - [lean](./api.html#query_Query-lean) * - [populate](/docs/populate.html) * - [projection](/docs/api/query.html#query_Query-projection) * - sanitizeProjection * * The following options are only for all operations **except** `update()`, `updateOne()`, `updateMany()`, `remove()`, `deleteOne()`, and `deleteMany()`: * * - [maxTimeMS](https://docs.mongodb.com/manual/reference/operator/meta/maxTimeMS/) * * The following options are for `findOneAndUpdate()` and `findOneAndRemove()` * * - rawResult * * The following options are for all operations: * * - [strict](/docs/guide.html#strict) * - [collation](https://docs.mongodb.com/manual/reference/collation/) * - [session](https://docs.mongodb.com/manual/reference/server-sessions/) * - [explain](https://docs.mongodb.com/manual/reference/method/cursor.explain/) * * @param {Object} options * @return {Query} this * @api public */ Query.prototype.setOptions = function(options, overwrite) { // overwrite is only for internal use if (overwrite) { // ensure that _mongooseOptions & options are two different objects this._mongooseOptions = (options && utils.clone(options)) || {}; this.options = options || {}; if ('populate' in options) { this.populate(this._mongooseOptions); } return this; } if (options == null) { return this; } if (typeof options !== 'object') { throw new Error('Options must be an object, got "' + options + '"'); } if (Array.isArray(options.populate)) { const populate = options.populate; delete options.populate; const _numPopulate = populate.length; for (let i = 0; i < _numPopulate; ++i) { this.populate(populate[i]); } } if ('setDefaultsOnInsert' in options) { this._mongooseOptions.setDefaultsOnInsert = options.setDefaultsOnInsert; delete options.setDefaultsOnInsert; } if ('overwriteDiscriminatorKey' in options) { this._mongooseOptions.overwriteDiscriminatorKey = options.overwriteDiscriminatorKey; delete options.overwriteDiscriminatorKey; } if ('sanitizeProjection' in options) { if (options.sanitizeProjection && !this._mongooseOptions.sanitizeProjection) { sanitizeProjection(this._fields); } this._mongooseOptions.sanitizeProjection = options.sanitizeProjection; delete options.sanitizeProjection; } if ('sanitizeFilter' in options) { this._mongooseOptions.sanitizeFilter = options.sanitizeFilter; delete options.sanitizeFilter; } if ('defaults' in options) { this._mongooseOptions.defaults = options.defaults; // deleting options.defaults will cause 7287 to fail } return Query.base.setOptions.call(this, options); }; /** * Sets the [`explain` option](https://docs.mongodb.com/manual/reference/method/cursor.explain/), * which makes this query return detailed execution stats instead of the actual * query result. This method is useful for determining what index your queries * use. * * Calling `query.explain(v)` is equivalent to `query.setOptions({ explain: v })` * * ####Example: * * const query = new Query(); * const res = await query.find({ a: 1 }).explain('queryPlanner'); * console.log(res); * * @param {String} [verbose] The verbosity mode. Either 'queryPlanner', 'executionStats', or 'allPlansExecution'. The default is 'queryPlanner' * @return {Query} this * @api public */ Query.prototype.explain = function(verbose) { if (arguments.length === 0) { this.options.explain = true; } else if (verbose === false) { delete this.options.explain; } else { this.options.explain = verbose; } return this; }; /** * Sets the [`allowDiskUse` option](https://docs.mongodb.com/manual/reference/method/cursor.allowDiskUse/), * which allows the MongoDB server to use more than 100 MB for this query's `sort()`. This option can * let you work around `QueryExceededMemoryLimitNoDiskUseAllowed` errors from the MongoDB server. * * Note that this option requires MongoDB server >= 4.4. Setting this option is a no-op for MongoDB 4.2 * and earlier. * * Calling `query.allowDiskUse(v)` is equivalent to `query.setOptions({ allowDiskUse: v })` * * ####Example: * * await query.find().sort({ name: 1 }).allowDiskUse(true); * // Equivalent: * await query.find().sort({ name: 1 }).allowDiskUse(); * * @param {Boolean} [v] Enable/disable `allowDiskUse`. If called with 0 arguments, sets `allowDiskUse: true` * @return {Query} this * @api public */ Query.prototype.allowDiskUse = function(v) { if (arguments.length === 0) { this.options.allowDiskUse = true; } else if (v === false) { delete this.options.allowDiskUse; } else { this.options.allowDiskUse = v; } return this; }; /** * Sets the [maxTimeMS](https://docs.mongodb.com/manual/reference/method/cursor.maxTimeMS/) * option. This will tell the MongoDB server to abort if the query or write op * has been running for more than `ms` milliseconds. * * Calling `query.maxTimeMS(v)` is equivalent to `query.setOptions({ maxTimeMS: v })` * * ####Example: * * const query = new Query(); * // Throws an error 'operation exceeded time limit' as long as there's * // >= 1 doc in the queried collection * const res = await query.find({ $where: 'sleep(1000) || true' }).maxTimeMS(100); * * @param {Number} [ms] The number of milliseconds * @return {Q