diff --git a/lib/query.js b/lib/query.js index dec846a2dc..0b56c5591e 100644 --- a/lib/query.js +++ b/lib/query.js @@ -1142,6 +1142,38 @@ Query.prototype.select = function select() { throw new TypeError('Invalid select() argument. Must be string or object.'); }; +/** + * Enable or disable schema level projections for this query. Enabled by default. + * Set to `false` to include fields with `select: false` in the query result by default. + * + * #### Example: + * + * const userSchema = new Schema({ + * email: { type: String, required: true }, + * passwordHash: { type: String, select: false, required: true } + * }); + * const UserModel = mongoose.model('User', userSchema); + * + * const doc = await UserModel.findOne().orFail().schemaLevelProjections(false); + * + * // Contains password hash, because `schemaLevelProjections()` overrides `select: false` + * doc.passwordHash; + * + * @method schemaLevelProjections + * @memberOf Query + * @instance + * @param {Boolean} value + * @return {Query} this + * @see SchemaTypeOptions https://mongoosejs.com/docs/schematypes.html#all-schema-types + * @api public + */ + +Query.prototype.schemaLevelProjections = function schemaLevelProjections(value) { + this._mongooseOptions.schemaLevelProjections = value; + + return this; +}; + /** * Sets this query's `sanitizeProjection` option. If set, `sanitizeProjection` does * two things: @@ -1689,6 +1721,10 @@ Query.prototype.setOptions = function(options, overwrite) { this._mongooseOptions.translateAliases = options.translateAliases; delete options.translateAliases; } + if ('schemaLevelProjections' in options) { + this._mongooseOptions.schemaLevelProjections = options.schemaLevelProjections; + delete options.schemaLevelProjections; + } if (options.lean == null && this.schema && 'lean' in this.schema.options) { this._mongooseOptions.lean = this.schema.options.lean; @@ -2222,6 +2258,7 @@ Query.prototype._unsetCastError = function _unsetCastError() { * - `strict`: controls how Mongoose handles keys that aren't in the schema for updates. This option is `true` by default, which means Mongoose will silently strip any paths in the update that aren't in the schema. See the [`strict` mode docs](https://mongoosejs.com/docs/guide.html#strict) for more information. * - `strictQuery`: controls how Mongoose handles keys that aren't in the schema for the query `filter`. This option is `false` by default, which means Mongoose will allow `Model.find({ foo: 'bar' })` even if `foo` is not in the schema. See the [`strictQuery` docs](https://mongoosejs.com/docs/guide.html#strictQuery) for more information. * - `nearSphere`: use `$nearSphere` instead of `near()`. See the [`Query.prototype.nearSphere()` docs](https://mongoosejs.com/docs/api/query.html#Query.prototype.nearSphere()) + * - `schemaLevelProjections`: if `false`, Mongoose will not apply schema-level `select: false` or `select: true` for this query * * Mongoose maintains a separate object for internal options because * Mongoose sends `Query.prototype.options` to the MongoDB server, and the @@ -4946,7 +4983,11 @@ Query.prototype._applyPaths = function applyPaths() { sanitizeProjection = this._mongooseOptions.sanitizeProjection; } - helpers.applyPaths(this._fields, this.model.schema, sanitizeProjection); + const schemaLevelProjections = this._mongooseOptions.schemaLevelProjections ?? true; + + if (schemaLevelProjections) { + helpers.applyPaths(this._fields, this.model.schema, sanitizeProjection); + } let _selectPopulatedPaths = true; diff --git a/test/query.test.js b/test/query.test.js index a3de50044e..e77d01c39b 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -4339,4 +4339,62 @@ describe('Query', function() { await Person.find({ $and: filter }); assert.deepStrictEqual(filter, [{ name: 'Me', age: '20' }, { name: 'You', age: '50' }]); }); + + describe('schemaLevelProjections (gh-11474)', function() { + it('disables schema-level select: false', async function() { + const userSchema = new Schema({ + email: { type: String, required: true }, + passwordHash: { type: String, select: false, required: true } + }); + const UserModel = db.model('User', userSchema); + + const { _id } = await UserModel.create({ email: 'test', passwordHash: 'gh-11474' }); + + const doc = await UserModel.findById(_id).orFail().schemaLevelProjections(false); + assert.strictEqual(doc.email, 'test'); + assert.strictEqual(doc.passwordHash, 'gh-11474'); + }); + + it('disables schema-level select: true', async function() { + const userSchema = new Schema({ + email: { type: String, required: true, select: true }, + otherProp: String + }); + const UserModel = db.model('User', userSchema); + + const { _id } = await UserModel.create({ email: 'test', otherProp: 'gh-11474 select true' }); + + const doc = await UserModel.findById(_id).select('otherProp').orFail().schemaLevelProjections(false); + assert.strictEqual(doc.email, undefined); + assert.strictEqual(doc.otherProp, 'gh-11474 select true'); + }); + + it('works via setOptions()', async function() { + const userSchema = new Schema({ + email: { type: String, required: true }, + passwordHash: { type: String, select: false, required: true } + }); + const UserModel = db.model('User', userSchema); + + const { _id } = await UserModel.create({ email: 'test', passwordHash: 'gh-11474' }); + + const doc = await UserModel.findById(_id).orFail().setOptions({ schemaLevelProjections: false }); + assert.strictEqual(doc.email, 'test'); + assert.strictEqual(doc.passwordHash, 'gh-11474'); + }); + + it('disabled via truthy value', async function() { + const userSchema = new Schema({ + email: { type: String, required: true }, + passwordHash: { type: String, select: false, required: true } + }); + const UserModel = db.model('User', userSchema); + + const { _id } = await UserModel.create({ email: 'test', passwordHash: 'gh-11474' }); + + const doc = await UserModel.findById(_id).orFail().schemaLevelProjections(true); + assert.strictEqual(doc.email, 'test'); + assert.strictEqual(doc.passwordHash, undefined); + }); + }); }); diff --git a/types/query.d.ts b/types/query.d.ts index 9d2114edff..17c109972a 100644 --- a/types/query.d.ts +++ b/types/query.d.ts @@ -24,6 +24,7 @@ declare module 'mongoose' { | 'runValidators' | 'sanitizeProjection' | 'sanitizeFilter' + | 'schemaLevelProjections' | 'setDefaultsOnInsert' | 'strict' | 'strictQuery' @@ -179,6 +180,11 @@ declare module 'mongoose' { * aren't explicitly allowed using `mongoose.trusted()`. */ sanitizeFilter?: boolean; + /** + * Enable or disable schema level projections for this query. Enabled by default. + * Set to `false` to include fields with `select: false` in the query result by default. + */ + schemaLevelProjections?: boolean; setDefaultsOnInsert?: boolean; skip?: number; sort?: any; @@ -734,6 +740,12 @@ declare module 'mongoose' { */ sanitizeProjection(value: boolean): this; + /** + * Enable or disable schema level projections for this query. Enabled by default. + * Set to `false` to include fields with `select: false` in the query result by default. + */ + schemaLevelProjections(value: boolean): this; + /** Specifies which document fields to include or exclude (also known as the query "projection") */ select( arg: string | string[] | Record