diff --git a/docs/js/navbar-search.js b/docs/js/navbar-search.js
index 1536d0db8ee..bbf32c4318e 100644
--- a/docs/js/navbar-search.js
+++ b/docs/js/navbar-search.js
@@ -1,11 +1,18 @@
-document.getElementById('search-button-nav').onclick = function() {
- var q = document.getElementById('search-input-nav').value;
- window.location.href = '/docs/search.html?q=' + encodeURIComponent(q);
-};
+(function() {
+ var versionFromUrl = window.location.pathname.match(/^\/docs\/(\d+\.x)/);
+ var version = versionFromUrl ? versionFromUrl[1] : defaultVersion;
-var q = document.getElementById('search-input-nav').onkeyup = function(ev) {
- if (ev.keyCode === 13) {
+ var searchPrefix = versionFromUrl ? '/docs/' + version + '/docs/' : '/docs/';
+
+ document.getElementById('search-button-nav').onclick = function() {
var q = document.getElementById('search-input-nav').value;
- window.location.href = '/docs/search.html?q=' + encodeURIComponent(q);
- }
-};
+ window.location.href = searchPrefix + 'search.html?q=' + encodeURIComponent(q);
+ };
+
+ document.getElementById('search-input-nav').onkeyup = function(ev) {
+ if (ev.keyCode === 13) {
+ var q = document.getElementById('search-input-nav').value;
+ window.location.href = searchPrefix + 'search.html?q=' + encodeURIComponent(q);
+ }
+ };
+})();
diff --git a/docs/js/search.js b/docs/js/search.js
index 71c623502e2..c3ad6cbcc96 100644
--- a/docs/js/search.js
+++ b/docs/js/search.js
@@ -9,9 +9,13 @@ for (var i = 0; i < pairs.length; ++i) {
}
}
+var defaultVersion = '6.x';
+var versionFromUrl = window.location.pathname.match(/^\/docs\/(\d+\.x)/);
+var version = versionFromUrl ? versionFromUrl[1] : defaultVersion;
+
if (q != null) {
document.getElementById('search-input').value = decodeURIComponent(q);
- fetch(root + '/search?search=' + q).
+ fetch(root + '/search?search=' + q + '&version=' + version).
then(function(res) { return res.json(); }).
then(
function(result) {
@@ -22,7 +26,7 @@ if (q != null) {
var html = '';
for (var i = 0; i < result.results.length; ++i) {
var res = result.results[i];
- var url = res.url.replace(/^\//, '');
+ var url = res.url;
html += '
' +
'' +
res.title +
diff --git a/docs/search.js b/docs/search.js
index a82d79669a2..53b3cc3eef4 100644
--- a/docs/search.js
+++ b/docs/search.js
@@ -121,8 +121,15 @@ async function run() {
await Content.deleteMany({ version });
for (const content of contents) {
- if (version !== '6.x') {
- content.url = `/docs/${version}/docs${content.url}`;
+ if (version === '6.x') {
+ let url = content.url.startsWith('/') ? content.url : `/${content.url}`;
+ if (!url.startsWith('/docs')) {
+ url = '/docs' + url;
+ }
+ content.url = url;
+ } else {
+ const url = content.url.startsWith('/') ? content.url : `/${content.url}`;
+ content.url = `/docs/${version}/docs${url}`;
}
await content.save();
}
diff --git a/docs/typescript/schemas.md b/docs/typescript/schemas.md
index 656c058d5c6..6640427c8b9 100644
--- a/docs/typescript/schemas.md
+++ b/docs/typescript/schemas.md
@@ -1,11 +1,9 @@
# Schemas in TypeScript
Mongoose [schemas](../guide.html) are how you tell Mongoose what your documents look like.
-Mongoose schemas are separate from TypeScript interfaces, so you need to define both a _document interface_ and a _schema_ until V6.3.1.
-Mongoose supports auto typed schemas so you don't need to define additional typescript interface anymore but you are still able to do so.
-Mongoose provides a `InferSchemaType`, which infers the type of the auto typed schema document when needed.
+Mongoose schemas are separate from TypeScript interfaces, so you need to either define both a _document interface_ and a _schema_; or rely on Mongoose to automatically infer the type from the schema definition.
-`Until mongoose V6.3.1:`
+### Separate document interface definition
```typescript
import { Schema } from 'mongoose';
@@ -25,7 +23,12 @@ const schema = new Schema({
});
```
-`another approach:`
+By default, Mongoose does **not** check if your document interface lines up with your schema.
+For example, the above code won't throw an error if `email` is optional in the document interface, but `required` in `schema`.
+
+### Automatic type inference
+
+Mongoose can also automatically infer the document type from your schema definition as follows.
```typescript
import { Schema, InferSchemaType } from 'mongoose';
@@ -53,11 +56,16 @@ type User = InferSchemaType;
// avatar?: string;
// }
-
+// `UserModel` will have `name: string`, etc.
+const UserModel = mongoose.model('User', schema);
```
-By default, Mongoose does **not** check if your document interface lines up with your schema.
-For example, the above code won't throw an error if `email` is optional in the document interface, but `required` in `schema`.
+There are a few caveats for using automatic type inference:
+
+1. You need to set `strictNullChecks: true` or `strict: true` in your `tsconfig.json`. Or, if you're setting flags at the command line, `--strictNullChecks` or `--strict`. There are [known issues](https://github.com/Automattic/mongoose/issues/12420) with automatic type inference with strict mode disabled.
+2. You need to define your schema in the `new Schema()` call. Don't assign your schema definition to a temporary variable. Doing something like `const schemaDefinition = { name: String }; const schema = new Schema(schemaDefinition);` will not work.
+
+If automatic type inference doesn't work for you, you can always fall back to document interface definitions.
## Generic parameters
diff --git a/lib/helpers/model/discriminator.js b/lib/helpers/model/discriminator.js
index 00135e1ae9c..b7a44a9ca18 100644
--- a/lib/helpers/model/discriminator.js
+++ b/lib/helpers/model/discriminator.js
@@ -1,6 +1,7 @@
'use strict';
const Mixed = require('../../schema/mixed');
+const applyBuiltinPlugins = require('../schema/applyBuiltinPlugins');
const clone = require('../clone');
const defineKey = require('../document/compile').defineKey;
const get = require('../get');
@@ -41,6 +42,8 @@ module.exports = function discriminator(model, name, schema, tiedValue, applyPlu
model.base._applyPlugins(schema, {
skipTopLevel: !applyPluginsToDiscriminators
});
+ } else if (!mergeHooks) {
+ applyBuiltinPlugins(schema);
}
const key = model.schema.options.discriminatorKey;
diff --git a/lib/helpers/schema/applyBuiltinPlugins.js b/lib/helpers/schema/applyBuiltinPlugins.js
new file mode 100644
index 00000000000..8bd7319cbb1
--- /dev/null
+++ b/lib/helpers/schema/applyBuiltinPlugins.js
@@ -0,0 +1,12 @@
+'use strict';
+
+const builtinPlugins = require('../../plugins');
+
+module.exports = function applyBuiltinPlugins(schema) {
+ for (const plugin of Object.values(builtinPlugins)) {
+ plugin(schema, { deduplicate: true });
+ }
+ schema.plugins = Object.values(builtinPlugins).
+ map(fn => ({ fn, opts: { deduplicate: true } })).
+ concat(schema.plugins);
+};
diff --git a/lib/index.js b/lib/index.js
index 5db756e5299..07be72d94d9 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -19,21 +19,17 @@ const Types = require('./types');
const Query = require('./query');
const Model = require('./model');
const applyPlugins = require('./helpers/schema/applyPlugins');
+const builtinPlugins = require('./plugins');
const driver = require('./driver');
const promiseOrCallback = require('./helpers/promiseOrCallback');
const legacyPluralize = require('./helpers/pluralize');
const utils = require('./utils');
const pkg = require('../package.json');
const cast = require('./cast');
-const removeSubdocs = require('./plugins/removeSubdocs');
-const saveSubdocs = require('./plugins/saveSubdocs');
-const trackTransaction = require('./plugins/trackTransaction');
-const validateBeforeSave = require('./plugins/validateBeforeSave');
const Aggregate = require('./aggregate');
const PromiseProvider = require('./promise_provider');
const printStrictQueryWarning = require('./helpers/printStrictQueryWarning');
-const shardingPlugin = require('./plugins/sharding');
const trusted = require('./helpers/query/trusted').trusted;
const sanitizeFilter = require('./helpers/query/sanitizeFilter');
const isBsonType = require('./helpers/isBsonType');
@@ -108,13 +104,7 @@ function Mongoose(options) {
configurable: false,
enumerable: true,
writable: false,
- value: [
- [saveSubdocs, { deduplicate: true }],
- [validateBeforeSave, { deduplicate: true }],
- [shardingPlugin, { deduplicate: true }],
- [removeSubdocs, { deduplicate: true }],
- [trackTransaction, { deduplicate: true }]
- ]
+ value: Object.values(builtinPlugins).map(plugin => ([plugin, { deduplicate: true }]))
});
}
diff --git a/lib/model.js b/lib/model.js
index 73d77557d29..81c17b696a5 100644
--- a/lib/model.js
+++ b/lib/model.js
@@ -37,6 +37,7 @@ const castBulkWrite = require('./helpers/model/castBulkWrite');
const clone = require('./helpers/clone');
const createPopulateQueryFilter = require('./helpers/populate/createPopulateQueryFilter');
const getDefaultBulkwriteResult = require('./helpers/getDefaultBulkwriteResult');
+const getSchemaDiscriminatorByValue = require('./helpers/discriminator/getSchemaDiscriminatorByValue');
const discriminator = require('./helpers/model/discriminator');
const firstKey = require('./helpers/firstKey');
const each = require('./helpers/each');
@@ -4475,7 +4476,11 @@ Model.validate = function validate(obj, pathsToValidate, context, callback) {
}
return this.db.base._promiseOrCallback(callback, cb => {
- const schema = this.schema;
+ let schema = this.schema;
+ const discriminatorKey = schema.options.discriminatorKey;
+ if (schema.discriminators != null && obj != null && obj[discriminatorKey] != null) {
+ schema = getSchemaDiscriminatorByValue(schema, obj[discriminatorKey]) || schema;
+ }
let paths = Object.keys(schema.paths);
if (pathsToValidate != null) {
diff --git a/lib/plugins/clearValidating.js b/lib/plugins/clearValidating.js
deleted file mode 100644
index 50264e33a6b..00000000000
--- a/lib/plugins/clearValidating.js
+++ /dev/null
@@ -1,28 +0,0 @@
-'use strict';
-
-/*!
- * ignore
- */
-
-module.exports = function clearValidating(schema) {
- // `this.$__.validating` tracks whether there are multiple validations running
- // in parallel. We need to clear `this.$__.validating` before post hooks for gh-8597
- const unshift = true;
- schema.s.hooks.post('validate', false, function clearValidatingPostValidate() {
- if (this.$isSubdocument) {
- return;
- }
-
- this.$__.validating = null;
- }, unshift);
-
- schema.s.hooks.post('validate', false, function clearValidatingPostValidateError(error, res, next) {
- if (this.$isSubdocument) {
- next();
- return;
- }
-
- this.$__.validating = null;
- next();
- }, unshift);
-};
diff --git a/lib/plugins/index.js b/lib/plugins/index.js
new file mode 100644
index 00000000000..69fa6ad284c
--- /dev/null
+++ b/lib/plugins/index.js
@@ -0,0 +1,7 @@
+'use strict';
+
+exports.removeSubdocs = require('./removeSubdocs');
+exports.saveSubdocs = require('./saveSubdocs');
+exports.sharding = require('./sharding');
+exports.trackTransaction = require('./trackTransaction');
+exports.validateBeforeSave = require('./validateBeforeSave');
diff --git a/lib/schema.js b/lib/schema.js
index ad4d1ec2122..cc0b054872d 100644
--- a/lib/schema.js
+++ b/lib/schema.js
@@ -1256,6 +1256,9 @@ Schema.prototype.interpretAsType = function(path, obj, options) {
if (options.hasOwnProperty('strict')) {
childSchemaOptions.strict = options.strict;
}
+ if (options.hasOwnProperty('strictQuery')) {
+ childSchemaOptions.strictQuery = options.strictQuery;
+ }
if (this._userProvidedOptions.hasOwnProperty('_id')) {
childSchemaOptions._id = this._userProvidedOptions._id;
diff --git a/test/model.discriminator.test.js b/test/model.discriminator.test.js
index 02f2ba582fb..3e3ea839f40 100644
--- a/test/model.discriminator.test.js
+++ b/test/model.discriminator.test.js
@@ -2077,4 +2077,28 @@ describe('model', function() {
schema.pre('save', function testHook12604() {});
}
});
+
+ it('applies built-in plugins if mergePlugins and mergeHooks disabled (gh-12696) (gh-12604)', async function() {
+ const shapeDef = { name: String };
+ const shapeSchema = Schema(shapeDef, { discriminatorKey: 'kind' });
+
+ const Shape = db.model('Test', shapeSchema);
+
+ let subdocSaveCalls = 0;
+ const nestedSchema = Schema({ test: String });
+ nestedSchema.pre('save', function() {
+ ++subdocSaveCalls;
+ });
+
+ const squareSchema = Schema({ ...shapeDef, nested: nestedSchema });
+ const Square = Shape.discriminator(
+ 'Square',
+ squareSchema,
+ { mergeHooks: false, mergePlugins: false }
+ );
+
+ assert.equal(subdocSaveCalls, 0);
+ await Square.create({ nested: { test: 'foo' } });
+ assert.equal(subdocSaveCalls, 1);
+ });
});
diff --git a/test/model.test.js b/test/model.test.js
index 6157f53e3b5..069a5f64502 100644
--- a/test/model.test.js
+++ b/test/model.test.js
@@ -6916,10 +6916,12 @@ describe('Model', function() {
granularity: 'hours'
},
autoCreate: false,
+ autoIndex: false,
expireAfterSeconds: 86400
});
- const Test = db.model('Test', schema);
+ const Test = db.model('Test', schema, 'Test');
+ await Test.init();
await Test.collection.drop().catch(() => {});
await Test.createCollection();
@@ -7456,119 +7458,6 @@ describe('Model', function() {
});
});
- it('Model.validate() (gh-7587)', async function() {
- const Model = db.model('Test', new Schema({
- name: {
- first: {
- type: String,
- required: true
- },
- last: {
- type: String,
- required: true
- }
- },
- age: {
- type: Number,
- required: true
- },
- comments: [{ name: { type: String, required: true } }]
- }));
-
-
- let err = null;
- let obj = null;
-
- err = await Model.validate({ age: null }, ['age']).
- then(() => null, err => err);
- assert.ok(err);
- assert.deepEqual(Object.keys(err.errors), ['age']);
-
- err = await Model.validate({ name: {} }, ['name']).
- then(() => null, err => err);
- assert.ok(err);
- assert.deepEqual(Object.keys(err.errors), ['name.first', 'name.last']);
-
- obj = { name: { first: 'foo' } };
- err = await Model.validate(obj, ['name']).
- then(() => null, err => err);
- assert.ok(err);
- assert.deepEqual(Object.keys(err.errors), ['name.last']);
-
- obj = { comments: [{ name: 'test' }, {}] };
- err = await Model.validate(obj, ['comments']).
- then(() => null, err => err);
- assert.ok(err);
- assert.deepEqual(Object.keys(err.errors), ['comments.name']);
-
- obj = { age: '42' };
- await Model.validate(obj, ['age']);
- assert.strictEqual(obj.age, 42);
- });
-
- it('Model.validate(...) validates paths in arrays (gh-8821)', async function() {
- const userSchema = new Schema({
- friends: [{ type: String, required: true, minlength: 3 }]
- });
-
- const User = db.model('User', userSchema);
-
- const err = await User.validate({ friends: [null, 'A'] }).catch(err => err);
-
- assert.ok(err.errors['friends.0']);
- assert.ok(err.errors['friends.1']);
-
- });
-
- it('Model.validate() works with arrays (gh-10669)', async function() {
- const testSchema = new Schema({
- docs: [String]
- });
-
- const Test = db.model('Test', testSchema);
-
- const test = { docs: ['6132655f2cdb9d94eaebc09b'] };
-
- const err = await Test.validate(test);
- assert.ifError(err);
- });
-
- it('Model.validate(...) uses document instance as context by default (gh-10132)', async function() {
- const userSchema = new Schema({
- name: {
- type: String,
- required: function() {
- return this.nameRequired;
- }
- },
- nameRequired: Boolean
- });
-
- const User = db.model('User', userSchema);
-
- const user = new User({ name: 'test', nameRequired: false });
- const err = await User.validate(user).catch(err => err);
-
- assert.ifError(err);
-
- });
- it('Model.validate(...) uses object as context by default (gh-10346)', async() => {
-
- const userSchema = new mongoose.Schema({
- name: { type: String, required: true },
- age: { type: Number, required() {return this && this.name === 'John';} }
- });
-
- const User = db.model('User', userSchema);
-
- const err1 = await User.validate({ name: 'John' }).then(() => null, err => err);
- assert.ok(err1);
-
- const err2 = await User.validate({ name: 'Sam' }).then(() => null, err => err);
- assert.ok(err2 === null);
-
- });
-
it('sets correct `Document#op` with `save()` (gh-8439)', function() {
const schema = Schema({ name: String });
const ops = [];
diff --git a/test/model.validate.test.js b/test/model.validate.test.js
new file mode 100644
index 00000000000..30891867ff3
--- /dev/null
+++ b/test/model.validate.test.js
@@ -0,0 +1,147 @@
+'use strict';
+
+const start = require('./common');
+
+const assert = require('assert');
+
+const mongoose = start.mongoose;
+const Schema = mongoose.Schema;
+
+describe('model: validate: ', function() {
+ beforeEach(() => mongoose.deleteModel(/.*/));
+ after(() => mongoose.deleteModel(/.*/));
+
+ it('Model.validate() (gh-7587)', async function() {
+ const Model = mongoose.model('Test', new Schema({
+ name: {
+ first: {
+ type: String,
+ required: true
+ },
+ last: {
+ type: String,
+ required: true
+ }
+ },
+ age: {
+ type: Number,
+ required: true
+ },
+ comments: [{ name: { type: String, required: true } }]
+ }));
+
+
+ let err = null;
+ let obj = null;
+
+ err = await Model.validate({ age: null }, ['age']).
+ then(() => null, err => err);
+ assert.ok(err);
+ assert.deepEqual(Object.keys(err.errors), ['age']);
+
+ err = await Model.validate({ name: {} }, ['name']).
+ then(() => null, err => err);
+ assert.ok(err);
+ assert.deepEqual(Object.keys(err.errors), ['name.first', 'name.last']);
+
+ obj = { name: { first: 'foo' } };
+ err = await Model.validate(obj, ['name']).
+ then(() => null, err => err);
+ assert.ok(err);
+ assert.deepEqual(Object.keys(err.errors), ['name.last']);
+
+ obj = { comments: [{ name: 'test' }, {}] };
+ err = await Model.validate(obj, ['comments']).
+ then(() => null, err => err);
+ assert.ok(err);
+ assert.deepEqual(Object.keys(err.errors), ['comments.name']);
+
+ obj = { age: '42' };
+ await Model.validate(obj, ['age']);
+ assert.strictEqual(obj.age, 42);
+ });
+
+ it('Model.validate(...) validates paths in arrays (gh-8821)', async function() {
+ const userSchema = new Schema({
+ friends: [{ type: String, required: true, minlength: 3 }]
+ });
+
+ const User = mongoose.model('User', userSchema);
+
+ const err = await User.validate({ friends: [null, 'A'] }).catch(err => err);
+
+ assert.ok(err.errors['friends.0']);
+ assert.ok(err.errors['friends.1']);
+ });
+
+ it('Model.validate(...) respects discriminators (gh-12621)', async function() {
+ const CatSchema = new Schema({ meows: { type: Boolean, required: true } });
+ const DogSchema = new Schema({ barks: { type: Boolean, required: true } });
+ const AnimalSchema = new Schema(
+ { id: String },
+ { discriminatorKey: 'kind' }
+ );
+ AnimalSchema.discriminator('cat', CatSchema);
+ AnimalSchema.discriminator('dog', DogSchema);
+
+ const Animal = mongoose.model('Test', AnimalSchema);
+
+ const invalidPet1 = new Animal({
+ id: '123',
+ kind: 'dog',
+ meows: true
+ });
+
+ const err = await Animal.validate(invalidPet1).then(() => null, err => err);
+ assert.ok(err);
+ assert.ok(err.errors['barks']);
+ });
+
+ it('Model.validate() works with arrays (gh-10669)', async function() {
+ const testSchema = new Schema({
+ docs: [String]
+ });
+
+ const Test = mongoose.model('Test', testSchema);
+
+ const test = { docs: ['6132655f2cdb9d94eaebc09b'] };
+
+ const err = await Test.validate(test);
+ assert.ifError(err);
+ });
+
+ it('Model.validate(...) uses document instance as context by default (gh-10132)', async function() {
+ const userSchema = new Schema({
+ name: {
+ type: String,
+ required: function() {
+ return this.nameRequired;
+ }
+ },
+ nameRequired: Boolean
+ });
+
+ const User = mongoose.model('User', userSchema);
+
+ const user = new User({ name: 'test', nameRequired: false });
+ const err = await User.validate(user).catch(err => err);
+
+ assert.ifError(err);
+
+ });
+ it('Model.validate(...) uses object as context by default (gh-10346)', async() => {
+
+ const userSchema = new mongoose.Schema({
+ name: { type: String, required: true },
+ age: { type: Number, required() {return this && this.name === 'John';} }
+ });
+
+ const User = mongoose.model('User', userSchema);
+
+ const err1 = await User.validate({ name: 'John' }).then(() => null, err => err);
+ assert.ok(err1);
+
+ const err2 = await User.validate({ name: 'Sam' }).then(() => null, err => err);
+ assert.ok(err2 === null);
+ });
+});
diff --git a/test/query.test.js b/test/query.test.js
index 66a6f6d8114..5c0e66e35e5 100644
--- a/test/query.test.js
+++ b/test/query.test.js
@@ -1523,7 +1523,7 @@ describe('Query', function() {
const Product = db.model('Product', productSchema);
Product.create(
{ numbers: [3, 4, 5] },
- { strings: 'hi there'.split(' ') }, function(err, doc1, doc2) {
+ { strings: 'hi there'.split(' '), w: 'majority' }, function(err, doc1, doc2) {
assert.ifError(err);
Product.find().setOptions({ limit: 1, sort: { _id: -1 }, read: 'n' }).exec(function(err, docs) {
assert.ifError(err);
diff --git a/test/schema.documentarray.test.js b/test/schema.documentarray.test.js
index 3217e165d88..d9ccb6c1f6b 100644
--- a/test/schema.documentarray.test.js
+++ b/test/schema.documentarray.test.js
@@ -75,6 +75,15 @@ describe('schema.documentarray', function() {
done();
});
+ it('propagates strictQuery to implicitly created schemas (gh-12796)', function() {
+ const schema = new Schema({
+ arr: [{ name: String }]
+ }, { strictQuery: 'throw' });
+
+ assert.equal(schema.childSchemas.length, 1);
+ assert.equal(schema.childSchemas[0].schema.options.strictQuery, 'throw');
+ });
+
it('supports set with array of document arrays (gh-7799)', function() {
const subSchema = new Schema({
title: String