Skip to content

Commit b799265

Browse files
committed
fix(query): add back strictQuery option to avoid empty filter issues, tie it to strict by default for compatibility
Fix #10781 Fix #10763 Re: #10598 Re: #9015 Re: #10605 Re: #9827
1 parent 38d86e1 commit b799265

File tree

9 files changed

+113
-16
lines changed

9 files changed

+113
-16
lines changed

docs/guide.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,7 @@ Valid options:
408408
- [writeConcern](#writeConcern)
409409
- [shardKey](#shardKey)
410410
- [strict](#strict)
411+
- [strictQuery](#strictQuery)
411412
- [toJSON](#toJSON)
412413
- [toObject](#toObject)
413414
- [typeKey](#typeKey)
@@ -755,6 +756,60 @@ thing.iAmNotInTheSchema = true;
755756
thing.save(); // iAmNotInTheSchema is never saved to the db
756757
```
757758

759+
<h3 id="strictQuery"><a href="#strictQuery">option: strictQuery</a></h3>
760+
761+
Mongoose supports a separate `strictQuery` option to avoid strict mode for query filters.
762+
This is because empty query filters cause Mongoose to return all documents in the model, which can cause issues.
763+
764+
```javascript
765+
const mySchema = new Schema({ field: Number }, { strict: true });
766+
const MyModel = mongoose.model('Test', mySchema);
767+
// Mongoose will filter out `notInSchema: 1` because `strict: true`, meaning this query will return
768+
// _all_ documents in the 'tests' collection
769+
MyModel.find({ notInSchema: 1 });
770+
```
771+
772+
The `strict` option does apply to updates.
773+
The `strictQuery` option is **just** for query filters.
774+
775+
```javascript
776+
// Mongoose will strip out `notInSchema` from the update if `strict` is
777+
// not `false`
778+
MyModel.updateMany({}, { $set: { notInSchema: 1 } });
779+
```
780+
781+
Mongoose has a separate `strictQuery` option to toggle strict mode for the `filter` parameter to queries.
782+
783+
```javascript
784+
const mySchema = new Schema({ field: Number }, {
785+
strict: true,
786+
strictQuery: false // Turn off strict mode for query filters
787+
});
788+
const MyModel = mongoose.model('Test', mySchema);
789+
// Mongoose will strip out `notInSchema: 1` because `strictQuery` is `true`
790+
MyModel.find({ notInSchema: 1 });
791+
```
792+
793+
In general, we do **not** recommend passing user-defined objects as query filters:
794+
795+
```javascript
796+
// Don't do this!
797+
const docs = await MyModel.find(req.query);
798+
799+
// Do this instead:
800+
const docs = await MyModel.find({ name: req.query.name, age: req.query.age }).setOptions({ sanitizeFilter: true });
801+
```
802+
803+
In Mongoose 6, `strictQuery` is equal to `strict` by default.
804+
However, you can override this behavior globally:
805+
806+
```javascript
807+
// Set `strictQuery` to `false`, so Mongoose doesn't strip out non-schema
808+
// query filter properties by default.
809+
// This does **not** affect `strict`.
810+
mongoose.set('strictQuery', false);
811+
```
812+
758813
<h3 id="toJSON"><a href="#toJSON">option: toJSON</a></h3>
759814

760815
Exactly the same as the [toObject](#toObject) option but only applies when

docs/migrating_to_6.md

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ If you're still on Mongoose 4.x, please read the [Mongoose 4.x to 5.x migration
1111
* [The `asPromise()` Method for Connections](#the-aspromise-method-for-connections)
1212
* [`mongoose.connect()` Returns a Promise](#mongoose-connect-returns-a-promise)
1313
* [Duplicate Query Execution](#duplicate-query-execution)
14-
* [`strictQuery` is removed and replaced by `strict`](#strictquery-is-removed-and-replaced-by-strict)
14+
* [`strictQuery` is now equal to `strict` by default](#strictquery-is-removed-and-replaced-by-strict)
1515
* [MongoError is now MongoServerError](#mongoerror-is-now-mongoservererror)
1616
* [Clone Discriminator Schemas By Default](#clone-discriminator-schemas-by-default)
1717
* [Schema Defined Document Key Order](#schema-defined-document-key-order)
@@ -113,9 +113,12 @@ await q;
113113
await q.clone(); // Can `clone()` the query to allow executing the query again
114114
```
115115

116-
<h3 id="strictquery-is-removed-and-replaced-by-strict"><a href="#strictquery-is-removed-and-replaced-by-strict">`strictQuery` is removed and replaced by `strict`</a></h3>
116+
<h3 id="strictquery-is-removed-and-replaced-by-strict"><a href="#strictquery-is-removed-and-replaced-by-strict">`strictQuery` is now equal to `strict` by default</a></h3>
117117

118-
Mongoose no longer supports a `strictQuery` option. You must now use `strict`. This means that, by default, Mongoose will filter out filter properties that are not in the schema.
118+
~Mongoose no longer supports a `strictQuery` option. You must now use `strict`.~
119+
As of Mongoose 6.0.10, we brought back the `strictQuery` option.
120+
However, `strictQuery` is tied to `strict` by default.
121+
This means that, by default, Mongoose will filter out query filter properties that are not in the schema.
119122

120123
```javascript
121124
const userSchema = new Schema({ name: String });
@@ -124,10 +127,16 @@ const User = mongoose.model('User', userSchema);
124127
// By default, this is equivalent to `User.find()` because Mongoose filters out `notInSchema`
125128
await User.find({ notInSchema: 1 });
126129

127-
// Set `strict: false` to opt in to filtering by properties that aren't in the schema
128-
await User.find({ notInSchema: 1 }, null, { strict: false });
130+
// Set `strictQuery: false` to opt in to filtering by properties that aren't in the schema
131+
await User.find({ notInSchema: 1 }, null, { strictQuery: false });
129132
// equivalent:
130-
await User.find({ notInSchema: 1 }).setOptions({ strict: false });
133+
await User.find({ notInSchema: 1 }).setOptions({ strictQuery: false });
134+
```
135+
136+
You can also disable `strictQuery` globally to override:
137+
138+
```javascript
139+
mongoose.set('strictQuery', false);
131140
```
132141

133142
<h3 id="mongoerror-is-now-mongoservererror"><a href="#mongoerror-is-now-mongoservererror">MongoError is now MongoServerError</a></h3>

index.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,6 +1065,11 @@ declare module 'mongoose' {
10651065
sort?: any;
10661066
/** overwrites the schema's strict mode option */
10671067
strict?: boolean | string;
1068+
/**
1069+
* equal to `strict` by default, may be `false`, `true`, or `'throw'`. Sets the default
1070+
* [strictQuery](https://mongoosejs.com/docs/guide.html#strictQuery) mode for schemas.
1071+
*/
1072+
strictQuery?: boolean | 'throw';
10681073
tailable?: number;
10691074
/**
10701075
* If set to `false` and schema-level timestamps are enabled,
@@ -1472,6 +1477,11 @@ declare module 'mongoose' {
14721477
* specified in our schema do not get saved to the db.
14731478
*/
14741479
strict?: boolean | 'throw';
1480+
/**
1481+
* equal to `strict` by default, may be `false`, `true`, or `'throw'`. Sets the default
1482+
* [strictQuery](https://mongoosejs.com/docs/guide.html#strictQuery) mode for schemas.
1483+
*/
1484+
strictQuery?: boolean | 'throw';
14751485
/** Exactly the same as the toObject option but only applies when the document's toJSON method is called. */
14761486
toJSON?: ToObjectOptions;
14771487
/**

lib/helpers/update/castArrayFilters.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ module.exports = function castArrayFilters(query) {
1515
_castArrayFilters(arrayFilters, schema, strictQuery, updatedPathsByFilter, query);
1616
};
1717

18-
function _castArrayFilters(arrayFilters, schema, strict, updatedPathsByFilter, query) {
18+
function _castArrayFilters(arrayFilters, schema, strictQuery, updatedPathsByFilter, query) {
1919
if (!Array.isArray(arrayFilters)) {
2020
return;
2121
}
@@ -26,7 +26,7 @@ function _castArrayFilters(arrayFilters, schema, strict, updatedPathsByFilter, q
2626
}
2727
for (const key of Object.keys(filter)) {
2828
if (key === '$and' || key === '$or') {
29-
_castArrayFilters(filter[key], schema, strict, updatedPathsByFilter, query);
29+
_castArrayFilters(filter[key], schema, strictQuery, updatedPathsByFilter, query);
3030
continue;
3131
}
3232
if (filter[key] == null) {

lib/query.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4970,8 +4970,11 @@ Query.prototype.cast = function(model, obj) {
49704970
strict: (this.options && 'strict' in this.options) ?
49714971
this.options.strict :
49724972
get(model, 'schema.options.strict', null),
4973-
strictQuery: (this.options && this.options.strictQuery) ||
4974-
get(model, 'schema.options.strictQuery', null)
4973+
strictQuery: (this.options && 'strictQuery' in this.options) ?
4974+
this.options.strictQuery :
4975+
(this.options && 'strict' in this.options) ?
4976+
this.options.strict :
4977+
get(model, 'schema.options.strictQuery', null)
49754978
}, this);
49764979
} catch (err) {
49774980
// CastError, assign model

lib/schema.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -418,9 +418,15 @@ Schema.prototype.defaultOptions = function(options) {
418418
this._userProvidedOptions = options == null ? {} : utils.clone(options);
419419

420420
const baseOptions = get(this, 'base.options', {});
421+
422+
const strict = 'strict' in baseOptions ? baseOptions.strict : true;
423+
421424
options = utils.options({
422-
strict: 'strict' in baseOptions ? baseOptions.strict : true,
423-
strictQuery: 'strictQuery' in baseOptions ? baseOptions.strictQuery : false,
425+
strict: strict,
426+
strictQuery: 'strict' in this._userProvidedOptions ?
427+
this._userProvidedOptions.strict :
428+
'strictQuery' in baseOptions ?
429+
baseOptions.strictQuery : strict,
424430
bufferCommands: true,
425431
capped: false, // { size, max, autoIndexId }
426432
versionKey: '__v',

test/es-next/virtuals.test.es6.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,8 @@ describe('Virtuals', function() {
133133
// acquit:ignore:end
134134
// Will **not** find any results, because `domain` is not stored in
135135
// MongoDB.
136-
const doc = await User.findOne({ domain: 'gmail.com' }, null, { strict: false });
136+
mongoose.set('debug', true)
137+
const doc = await User.findOne({ domain: 'gmail.com' }, null, { strictQuery: false });
137138
doc; // undefined
138139
// acquit:ignore:start
139140
assert.equal(doc, null);

test/helpers/update.castArrayFilters.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ describe('castArrayFilters', function() {
176176
castArrayFilters(q);
177177
}, /Could not find path.*in schema/);
178178

179-
q.schema.options.strict = false;
179+
q.schema.options.strictQuery = false;
180180
castArrayFilters(q);
181181
assert.strictEqual(q.options.arrayFilters[0]['arr.notInSchema'], '42');
182182
});

test/query.test.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3205,15 +3205,14 @@ describe('Query', function() {
32053205
});
32063206
});
32073207

3208-
it('strictQuery option (gh-4136) (gh-7178)', function() {
3208+
it('strictQuery option (gh-4136) (gh-7178)', async function() {
32093209
const modelSchema = new Schema({
32103210
field: Number,
32113211
nested: { path: String }
32123212
}, { strictQuery: 'throw' });
32133213

32143214
const Model = db.model('Test', modelSchema);
32153215

3216-
32173216
// `find()` on a top-level path not in the schema
32183217
let err = await Model.find({ notInschema: 1 }).then(() => null, e => e);
32193218
assert.ok(err);
@@ -3230,6 +3229,20 @@ describe('Query', function() {
32303229
assert.ok(err.message.indexOf('strictQuery') !== -1, err.message);
32313230
});
32323231

3232+
it('strictQuery inherits from strict (gh-10763) (gh-4136) (gh-7178)', async function() {
3233+
const modelSchema = new Schema({
3234+
field: Number,
3235+
nested: { path: String }
3236+
}, { strict: 'throw' });
3237+
3238+
const Model = db.model('Test', modelSchema);
3239+
3240+
// `find()` on a top-level path not in the schema
3241+
const err = await Model.find({ notInschema: 1 }).then(() => null, e => e);
3242+
assert.ok(err);
3243+
assert.ok(err.message.indexOf('strictQuery') !== -1, err.message);
3244+
});
3245+
32333246
it('strictQuery = true (gh-6032)', async function() {
32343247
const modelSchema = new Schema({ field: Number }, { strictQuery: true });
32353248

0 commit comments

Comments
 (0)