Skip to content

Commit 429f855

Browse files
authored
Merge pull request #14986 from Automattic/vkarpov15/gh-11474
feat(query): add schemaLevelProjections option to query to disable schema-level select: false
2 parents a60e72d + 6a7d98e commit 429f855

File tree

3 files changed

+112
-1
lines changed

3 files changed

+112
-1
lines changed

lib/query.js

+42-1
Original file line numberDiff line numberDiff line change
@@ -1142,6 +1142,38 @@ Query.prototype.select = function select() {
11421142
throw new TypeError('Invalid select() argument. Must be string or object.');
11431143
};
11441144

1145+
/**
1146+
* Enable or disable schema level projections for this query. Enabled by default.
1147+
* Set to `false` to include fields with `select: false` in the query result by default.
1148+
*
1149+
* #### Example:
1150+
*
1151+
* const userSchema = new Schema({
1152+
* email: { type: String, required: true },
1153+
* passwordHash: { type: String, select: false, required: true }
1154+
* });
1155+
* const UserModel = mongoose.model('User', userSchema);
1156+
*
1157+
* const doc = await UserModel.findOne().orFail().schemaLevelProjections(false);
1158+
*
1159+
* // Contains password hash, because `schemaLevelProjections()` overrides `select: false`
1160+
* doc.passwordHash;
1161+
*
1162+
* @method schemaLevelProjections
1163+
* @memberOf Query
1164+
* @instance
1165+
* @param {Boolean} value
1166+
* @return {Query} this
1167+
* @see SchemaTypeOptions https://mongoosejs.com/docs/schematypes.html#all-schema-types
1168+
* @api public
1169+
*/
1170+
1171+
Query.prototype.schemaLevelProjections = function schemaLevelProjections(value) {
1172+
this._mongooseOptions.schemaLevelProjections = value;
1173+
1174+
return this;
1175+
};
1176+
11451177
/**
11461178
* Sets this query's `sanitizeProjection` option. If set, `sanitizeProjection` does
11471179
* two things:
@@ -1689,6 +1721,10 @@ Query.prototype.setOptions = function(options, overwrite) {
16891721
this._mongooseOptions.translateAliases = options.translateAliases;
16901722
delete options.translateAliases;
16911723
}
1724+
if ('schemaLevelProjections' in options) {
1725+
this._mongooseOptions.schemaLevelProjections = options.schemaLevelProjections;
1726+
delete options.schemaLevelProjections;
1727+
}
16921728

16931729
if (options.lean == null && this.schema && 'lean' in this.schema.options) {
16941730
this._mongooseOptions.lean = this.schema.options.lean;
@@ -2222,6 +2258,7 @@ Query.prototype._unsetCastError = function _unsetCastError() {
22222258
* - `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.
22232259
* - `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.
22242260
* - `nearSphere`: use `$nearSphere` instead of `near()`. See the [`Query.prototype.nearSphere()` docs](https://mongoosejs.com/docs/api/query.html#Query.prototype.nearSphere())
2261+
* - `schemaLevelProjections`: if `false`, Mongoose will not apply schema-level `select: false` or `select: true` for this query
22252262
*
22262263
* Mongoose maintains a separate object for internal options because
22272264
* Mongoose sends `Query.prototype.options` to the MongoDB server, and the
@@ -4946,7 +4983,11 @@ Query.prototype._applyPaths = function applyPaths() {
49464983
sanitizeProjection = this._mongooseOptions.sanitizeProjection;
49474984
}
49484985

4949-
helpers.applyPaths(this._fields, this.model.schema, sanitizeProjection);
4986+
const schemaLevelProjections = this._mongooseOptions.schemaLevelProjections ?? true;
4987+
4988+
if (schemaLevelProjections) {
4989+
helpers.applyPaths(this._fields, this.model.schema, sanitizeProjection);
4990+
}
49504991

49514992
let _selectPopulatedPaths = true;
49524993

test/query.test.js

+58
Original file line numberDiff line numberDiff line change
@@ -4339,4 +4339,62 @@ describe('Query', function() {
43394339
await Person.find({ $and: filter });
43404340
assert.deepStrictEqual(filter, [{ name: 'Me', age: '20' }, { name: 'You', age: '50' }]);
43414341
});
4342+
4343+
describe('schemaLevelProjections (gh-11474)', function() {
4344+
it('disables schema-level select: false', async function() {
4345+
const userSchema = new Schema({
4346+
email: { type: String, required: true },
4347+
passwordHash: { type: String, select: false, required: true }
4348+
});
4349+
const UserModel = db.model('User', userSchema);
4350+
4351+
const { _id } = await UserModel.create({ email: 'test', passwordHash: 'gh-11474' });
4352+
4353+
const doc = await UserModel.findById(_id).orFail().schemaLevelProjections(false);
4354+
assert.strictEqual(doc.email, 'test');
4355+
assert.strictEqual(doc.passwordHash, 'gh-11474');
4356+
});
4357+
4358+
it('disables schema-level select: true', async function() {
4359+
const userSchema = new Schema({
4360+
email: { type: String, required: true, select: true },
4361+
otherProp: String
4362+
});
4363+
const UserModel = db.model('User', userSchema);
4364+
4365+
const { _id } = await UserModel.create({ email: 'test', otherProp: 'gh-11474 select true' });
4366+
4367+
const doc = await UserModel.findById(_id).select('otherProp').orFail().schemaLevelProjections(false);
4368+
assert.strictEqual(doc.email, undefined);
4369+
assert.strictEqual(doc.otherProp, 'gh-11474 select true');
4370+
});
4371+
4372+
it('works via setOptions()', async function() {
4373+
const userSchema = new Schema({
4374+
email: { type: String, required: true },
4375+
passwordHash: { type: String, select: false, required: true }
4376+
});
4377+
const UserModel = db.model('User', userSchema);
4378+
4379+
const { _id } = await UserModel.create({ email: 'test', passwordHash: 'gh-11474' });
4380+
4381+
const doc = await UserModel.findById(_id).orFail().setOptions({ schemaLevelProjections: false });
4382+
assert.strictEqual(doc.email, 'test');
4383+
assert.strictEqual(doc.passwordHash, 'gh-11474');
4384+
});
4385+
4386+
it('disabled via truthy value', async function() {
4387+
const userSchema = new Schema({
4388+
email: { type: String, required: true },
4389+
passwordHash: { type: String, select: false, required: true }
4390+
});
4391+
const UserModel = db.model('User', userSchema);
4392+
4393+
const { _id } = await UserModel.create({ email: 'test', passwordHash: 'gh-11474' });
4394+
4395+
const doc = await UserModel.findById(_id).orFail().schemaLevelProjections(true);
4396+
assert.strictEqual(doc.email, 'test');
4397+
assert.strictEqual(doc.passwordHash, undefined);
4398+
});
4399+
});
43424400
});

types/query.d.ts

+12
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ declare module 'mongoose' {
2424
| 'runValidators'
2525
| 'sanitizeProjection'
2626
| 'sanitizeFilter'
27+
| 'schemaLevelProjections'
2728
| 'setDefaultsOnInsert'
2829
| 'strict'
2930
| 'strictQuery'
@@ -179,6 +180,11 @@ declare module 'mongoose' {
179180
* aren't explicitly allowed using `mongoose.trusted()`.
180181
*/
181182
sanitizeFilter?: boolean;
183+
/**
184+
* Enable or disable schema level projections for this query. Enabled by default.
185+
* Set to `false` to include fields with `select: false` in the query result by default.
186+
*/
187+
schemaLevelProjections?: boolean;
182188
setDefaultsOnInsert?: boolean;
183189
skip?: number;
184190
sort?: any;
@@ -734,6 +740,12 @@ declare module 'mongoose' {
734740
*/
735741
sanitizeProjection(value: boolean): this;
736742

743+
/**
744+
* Enable or disable schema level projections for this query. Enabled by default.
745+
* Set to `false` to include fields with `select: false` in the query result by default.
746+
*/
747+
schemaLevelProjections(value: boolean): this;
748+
737749
/** Specifies which document fields to include or exclude (also known as the query "projection") */
738750
select<RawDocTypeOverride extends { [P in keyof RawDocType]?: any } = {}>(
739751
arg: string | string[] | Record<string, number | boolean | string | object>

0 commit comments

Comments
 (0)