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