Skip to content

Commit

Permalink
Merge pull request #14986 from Automattic/vkarpov15/gh-11474
Browse files Browse the repository at this point in the history
feat(query): add schemaLevelProjections option to query to disable schema-level select: false
  • Loading branch information
vkarpov15 authored Oct 28, 2024
2 parents a60e72d + 6a7d98e commit 429f855
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 1 deletion.
43 changes: 42 additions & 1 deletion lib/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;

Expand Down
58 changes: 58 additions & 0 deletions test/query.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
12 changes: 12 additions & 0 deletions types/query.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ declare module 'mongoose' {
| 'runValidators'
| 'sanitizeProjection'
| 'sanitizeFilter'
| 'schemaLevelProjections'
| 'setDefaultsOnInsert'
| 'strict'
| 'strictQuery'
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<RawDocTypeOverride extends { [P in keyof RawDocType]?: any } = {}>(
arg: string | string[] | Record<string, number | boolean | string | object>
Expand Down

0 comments on commit 429f855

Please sign in to comment.