Skip to content

Commit

Permalink
fix(query): add back strictQuery option to avoid empty filter issue…
Browse files Browse the repository at this point in the history
…s, tie it to `strict` by default for compatibility

Fix #10781
Fix #10763
Re: #10598
Re: #9015
Re: #10605
Re: #9827
  • Loading branch information
vkarpov15 committed Oct 7, 2021
1 parent 38d86e1 commit b799265
Show file tree
Hide file tree
Showing 9 changed files with 113 additions and 16 deletions.
55 changes: 55 additions & 0 deletions docs/guide.md
Expand Up @@ -408,6 +408,7 @@ Valid options:
- [writeConcern](#writeConcern)
- [shardKey](#shardKey)
- [strict](#strict)
- [strictQuery](#strictQuery)
- [toJSON](#toJSON)
- [toObject](#toObject)
- [typeKey](#typeKey)
Expand Down Expand Up @@ -755,6 +756,60 @@ thing.iAmNotInTheSchema = true;
thing.save(); // iAmNotInTheSchema is never saved to the db
```

<h3 id="strictQuery"><a href="#strictQuery">option: strictQuery</a></h3>

Mongoose supports a separate `strictQuery` option to avoid strict mode for query filters.
This is because empty query filters cause Mongoose to return all documents in the model, which can cause issues.

```javascript
const mySchema = new Schema({ field: Number }, { strict: true });
const MyModel = mongoose.model('Test', mySchema);
// Mongoose will filter out `notInSchema: 1` because `strict: true`, meaning this query will return
// _all_ documents in the 'tests' collection
MyModel.find({ notInSchema: 1 });
```

The `strict` option does apply to updates.
The `strictQuery` option is **just** for query filters.

```javascript
// Mongoose will strip out `notInSchema` from the update if `strict` is
// not `false`
MyModel.updateMany({}, { $set: { notInSchema: 1 } });
```

Mongoose has a separate `strictQuery` option to toggle strict mode for the `filter` parameter to queries.

```javascript
const mySchema = new Schema({ field: Number }, {
strict: true,
strictQuery: false // Turn off strict mode for query filters
});
const MyModel = mongoose.model('Test', mySchema);
// Mongoose will strip out `notInSchema: 1` because `strictQuery` is `true`

This comment has been minimized.

Copy link
@Kamikadze4GAME

Kamikadze4GAME Oct 7, 2021

Contributor

Hello @vkarpov15.

I think there is a typo with strictQuery is true. Should it be like this?

// Mongoose will strip out `notInSchema: 1` because `strictQuery` is `false`

This comment has been minimized.

Copy link
@vkarpov15

vkarpov15 Oct 8, 2021

Author Collaborator

Fixed in 759278a, good catch 👍

MyModel.find({ notInSchema: 1 });
```

In general, we do **not** recommend passing user-defined objects as query filters:

```javascript
// Don't do this!
const docs = await MyModel.find(req.query);

// Do this instead:
const docs = await MyModel.find({ name: req.query.name, age: req.query.age }).setOptions({ sanitizeFilter: true });
```

In Mongoose 6, `strictQuery` is equal to `strict` by default.
However, you can override this behavior globally:

```javascript
// Set `strictQuery` to `false`, so Mongoose doesn't strip out non-schema
// query filter properties by default.
// This does **not** affect `strict`.
mongoose.set('strictQuery', false);
```

<h3 id="toJSON"><a href="#toJSON">option: toJSON</a></h3>

Exactly the same as the [toObject](#toObject) option but only applies when
Expand Down
21 changes: 15 additions & 6 deletions docs/migrating_to_6.md
Expand Up @@ -11,7 +11,7 @@ If you're still on Mongoose 4.x, please read the [Mongoose 4.x to 5.x migration
* [The `asPromise()` Method for Connections](#the-aspromise-method-for-connections)
* [`mongoose.connect()` Returns a Promise](#mongoose-connect-returns-a-promise)
* [Duplicate Query Execution](#duplicate-query-execution)
* [`strictQuery` is removed and replaced by `strict`](#strictquery-is-removed-and-replaced-by-strict)
* [`strictQuery` is now equal to `strict` by default](#strictquery-is-removed-and-replaced-by-strict)
* [MongoError is now MongoServerError](#mongoerror-is-now-mongoservererror)
* [Clone Discriminator Schemas By Default](#clone-discriminator-schemas-by-default)
* [Schema Defined Document Key Order](#schema-defined-document-key-order)
Expand Down Expand Up @@ -113,9 +113,12 @@ await q;
await q.clone(); // Can `clone()` the query to allow executing the query again
```

<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>
<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>

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.
~Mongoose no longer supports a `strictQuery` option. You must now use `strict`.~
As of Mongoose 6.0.10, we brought back the `strictQuery` option.
However, `strictQuery` is tied to `strict` by default.
This means that, by default, Mongoose will filter out query filter properties that are not in the schema.

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

// Set `strict: false` to opt in to filtering by properties that aren't in the schema
await User.find({ notInSchema: 1 }, null, { strict: false });
// Set `strictQuery: false` to opt in to filtering by properties that aren't in the schema
await User.find({ notInSchema: 1 }, null, { strictQuery: false });
// equivalent:
await User.find({ notInSchema: 1 }).setOptions({ strict: false });
await User.find({ notInSchema: 1 }).setOptions({ strictQuery: false });
```

You can also disable `strictQuery` globally to override:

```javascript
mongoose.set('strictQuery', false);
```

<h3 id="mongoerror-is-now-mongoservererror"><a href="#mongoerror-is-now-mongoservererror">MongoError is now MongoServerError</a></h3>
Expand Down
10 changes: 10 additions & 0 deletions index.d.ts
Expand Up @@ -1065,6 +1065,11 @@ declare module 'mongoose' {
sort?: any;
/** overwrites the schema's strict mode option */
strict?: boolean | string;
/**
* equal to `strict` by default, may be `false`, `true`, or `'throw'`. Sets the default
* [strictQuery](https://mongoosejs.com/docs/guide.html#strictQuery) mode for schemas.
*/
strictQuery?: boolean | 'throw';
tailable?: number;
/**
* If set to `false` and schema-level timestamps are enabled,
Expand Down Expand Up @@ -1472,6 +1477,11 @@ declare module 'mongoose' {
* specified in our schema do not get saved to the db.
*/
strict?: boolean | 'throw';
/**
* equal to `strict` by default, may be `false`, `true`, or `'throw'`. Sets the default
* [strictQuery](https://mongoosejs.com/docs/guide.html#strictQuery) mode for schemas.
*/
strictQuery?: boolean | 'throw';
/** Exactly the same as the toObject option but only applies when the document's toJSON method is called. */
toJSON?: ToObjectOptions;
/**
Expand Down
4 changes: 2 additions & 2 deletions lib/helpers/update/castArrayFilters.js
Expand Up @@ -15,7 +15,7 @@ module.exports = function castArrayFilters(query) {
_castArrayFilters(arrayFilters, schema, strictQuery, updatedPathsByFilter, query);
};

function _castArrayFilters(arrayFilters, schema, strict, updatedPathsByFilter, query) {
function _castArrayFilters(arrayFilters, schema, strictQuery, updatedPathsByFilter, query) {
if (!Array.isArray(arrayFilters)) {
return;
}
Expand All @@ -26,7 +26,7 @@ function _castArrayFilters(arrayFilters, schema, strict, updatedPathsByFilter, q
}
for (const key of Object.keys(filter)) {
if (key === '$and' || key === '$or') {
_castArrayFilters(filter[key], schema, strict, updatedPathsByFilter, query);
_castArrayFilters(filter[key], schema, strictQuery, updatedPathsByFilter, query);
continue;
}
if (filter[key] == null) {
Expand Down
7 changes: 5 additions & 2 deletions lib/query.js
Expand Up @@ -4970,8 +4970,11 @@ Query.prototype.cast = function(model, obj) {
strict: (this.options && 'strict' in this.options) ?
this.options.strict :
get(model, 'schema.options.strict', null),
strictQuery: (this.options && this.options.strictQuery) ||
get(model, 'schema.options.strictQuery', null)
strictQuery: (this.options && 'strictQuery' in this.options) ?
this.options.strictQuery :
(this.options && 'strict' in this.options) ?
this.options.strict :
get(model, 'schema.options.strictQuery', null)
}, this);
} catch (err) {
// CastError, assign model
Expand Down
10 changes: 8 additions & 2 deletions lib/schema.js
Expand Up @@ -418,9 +418,15 @@ Schema.prototype.defaultOptions = function(options) {
this._userProvidedOptions = options == null ? {} : utils.clone(options);

const baseOptions = get(this, 'base.options', {});

const strict = 'strict' in baseOptions ? baseOptions.strict : true;

options = utils.options({
strict: 'strict' in baseOptions ? baseOptions.strict : true,
strictQuery: 'strictQuery' in baseOptions ? baseOptions.strictQuery : false,
strict: strict,
strictQuery: 'strict' in this._userProvidedOptions ?
this._userProvidedOptions.strict :
'strictQuery' in baseOptions ?
baseOptions.strictQuery : strict,
bufferCommands: true,
capped: false, // { size, max, autoIndexId }
versionKey: '__v',
Expand Down
3 changes: 2 additions & 1 deletion test/es-next/virtuals.test.es6.js
Expand Up @@ -133,7 +133,8 @@ describe('Virtuals', function() {
// acquit:ignore:end
// Will **not** find any results, because `domain` is not stored in
// MongoDB.
const doc = await User.findOne({ domain: 'gmail.com' }, null, { strict: false });
mongoose.set('debug', true)
const doc = await User.findOne({ domain: 'gmail.com' }, null, { strictQuery: false });
doc; // undefined
// acquit:ignore:start
assert.equal(doc, null);
Expand Down
2 changes: 1 addition & 1 deletion test/helpers/update.castArrayFilters.test.js
Expand Up @@ -176,7 +176,7 @@ describe('castArrayFilters', function() {
castArrayFilters(q);
}, /Could not find path.*in schema/);

q.schema.options.strict = false;
q.schema.options.strictQuery = false;
castArrayFilters(q);
assert.strictEqual(q.options.arrayFilters[0]['arr.notInSchema'], '42');
});
Expand Down
17 changes: 15 additions & 2 deletions test/query.test.js
Expand Up @@ -3205,15 +3205,14 @@ describe('Query', function() {
});
});

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

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


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

it('strictQuery inherits from strict (gh-10763) (gh-4136) (gh-7178)', async function() {
const modelSchema = new Schema({
field: Number,
nested: { path: String }
}, { strict: 'throw' });

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

// `find()` on a top-level path not in the schema
const err = await Model.find({ notInschema: 1 }).then(() => null, e => e);
assert.ok(err);
assert.ok(err.message.indexOf('strictQuery') !== -1, err.message);
});

it('strictQuery = true (gh-6032)', async function() {
const modelSchema = new Schema({ field: Number }, { strictQuery: true });

Expand Down

0 comments on commit b799265

Please sign in to comment.