diff --git a/History.md b/History.md index e3f40f5caf3..91130da0e10 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,26 @@ +5.11.15 / 2021-02-03 +==================== + * fix(document): fix issues with `isSelected` as an path in a nested schema #9884 #9873 [IslandRhythms](https://github.com/IslandRhythms) + * fix(index.d.ts): better support for `SchemaDefinition` generics when creating schema #9863 #9862 #9789 + * fix(index.d.ts): allow required function in array definition #9888 [Ugzuzg](https://github.com/Ugzuzg) + * fix(index.d.ts): reorder create typings to allow array desctructuring #9890 [Ugzuzg](https://github.com/Ugzuzg) + * fix(index.d.ts): add missing overload to Model.validate #9878 #9877 [jonamat](https://github.com/jonamat) + * fix(index.d.ts): throw compiler error if schema says path is a String, but interface says path is a number #9857 + * fix(index.d.ts): make `Query` a class, allow calling `Query#where()` with object argument and with no arguments #9856 + * fix(index.d.ts): support calling `Schema#pre()` and `Schema#post()` with options and array of hooked function names #9844 + * docs(faq): mention other debug options than console #9887 [dandv](https://github.com/dandv) + * docs(connections): clarify that Mongoose can emit 'error' both during initial connection and after initial connection #9853 + +5.11.14 / 2021-01-28 +==================== + * fix(populate): avoid inferring `justOne` from parent when populating a POJO with a manually populated path #9833 [IslandRhythms](https://github.com/IslandRhythms) + * fix(document): apply setters on each element of the array when setting a populated array #9838 + * fix(map): handle change tracking on maps of subdocs #9811 [IslandRhythms](https://github.com/IslandRhythms) + * fix(document): remove dependency on `documentIsSelected` symbol #9841 [IslandRhythms](https://github.com/IslandRhythms) + * fix(error): make ValidationError.toJSON to include the error name correctly #9849 [hanzki](https://github.com/hanzki) + * fix(index.d.ts): indicate that `Document#remove()` returns a promise, not a query #9826 + * fix(index.d.ts): allow setting `SchemaType#enum` to TypeScript enum with `required: true` #9546 + 5.11.13 / 2021-01-20 ==================== * fix(map): handle change tracking on map of arrays #9813 diff --git a/docs/connections.pug b/docs/connections.pug index 6be93e02741..c2b92aa0714 100644 --- a/docs/connections.pug +++ b/docs/connections.pug @@ -118,7 +118,7 @@ block content There are two classes of errors that can occur with a Mongoose connection. - - Error on initial connection. If initial connection fails, Mongoose will **not** attempt to reconnect, it will emit an 'error' event, and the promise `mongoose.connect()` returns will reject. + - Error on initial connection. If initial connection fails, Mongoose will emit an 'error' event and the promise `mongoose.connect()` returns will reject. However, Mongoose will **not** automatically try to reconnect. - Error after initial connection was established. Mongoose will attempt to reconnect, and it will emit an 'error' event. To handle initial connection errors, you should use `.catch()` or `try/catch` with async/await. @@ -145,9 +145,8 @@ block content }); ``` - Note that the `error` event in the code above does not fire when mongoose loses - connection after the initial connection was established. You can listen to the - `disconnected` event for that purpose. + Note that Mongoose does not necessarily emit an 'error' event if it loses connectivity to MongoDB. You should + listen to the `disconnected` event to report when Mongoose is disconnected from MongoDB.

Options

diff --git a/docs/faq.pug b/docs/faq.pug index 54a6636b21d..29ef6affbd2 100644 --- a/docs/faq.pug +++ b/docs/faq.pug @@ -293,9 +293,10 @@ block content **Q**. How can I enable debugging? - **A**. Set the `debug` option to `true`: + **A**. Set the `debug` option: ```javascript + // all executed methods log output to console mongoose.set('debug', true) // disable colors in debug mode @@ -305,8 +306,7 @@ block content mongoose.set('debug', { shell: true }) ``` - All executed collection methods will log output of their arguments to your - console. + For more debugging options (streams, callbacks), see the ['debug' option under `.set()`](./api.html#mongoose_Mongoose-set).
diff --git a/docs/images/fortunegames.jpg b/docs/images/fortunegames.jpg new file mode 100644 index 00000000000..795eca76cf2 Binary files /dev/null and b/docs/images/fortunegames.jpg differ diff --git a/index.d.ts b/index.d.ts index 9ebe8855cb0..5df726f4c40 100644 --- a/index.d.ts +++ b/index.d.ts @@ -630,11 +630,11 @@ declare module 'mongoose' { countDocuments(filter: FilterQuery, callback?: (err: any, count: number) => void): Query; /** Creates a new document or documents */ - create>(doc: DocContents): Promise; create>(docs: DocContents[], options?: SaveOptions): Promise; + create>(doc: DocContents): Promise; create>(...docs: DocContents[]): Promise; - create>(doc: DocContents, callback: (err: CallbackError, doc: T) => void): void; create>(docs: DocContents[], callback: (err: CallbackError, docs: T[]) => void): void; + create>(doc: DocContents, callback: (err: CallbackError, doc: T) => void): void; /** * Create the collection for this model. By default, if no indexes are specified, @@ -753,6 +753,7 @@ declare module 'mongoose' { /** Casts and validates the given object against this model's schema, passing the given `context` to custom validators. */ validate(callback?: (err: any) => void): Promise; validate(optional: any, callback?: (err: any) => void): Promise; + validate(optional: any, pathsToValidate: string[], callback?: (err: any) => void): Promise; /** Watches the underlying collection for changes using [MongoDB change streams](https://docs.mongodb.com/manual/changeStreams/). */ watch(pipeline?: Array>, options?: mongodb.ChangeStreamOptions): mongodb.ChangeStream; @@ -844,6 +845,8 @@ declare module 'mongoose' { /** Creates a Query, applies the passed conditions, and returns the Query. */ where(path: string, val?: any): Query, T>; + where(obj: object): Query, T>; + where(): Query, T>; } interface QueryOptions { @@ -1037,16 +1040,20 @@ declare module 'mongoose' { useProjection?: boolean; } + type MongooseDocumentMiddleware = 'validate' | 'save' | 'remove' | 'updateOne' | 'deleteOne' | 'init'; type MongooseQueryMiddleware = 'count' | 'deleteMany' | 'deleteOne' | 'find' | 'findOne' | 'findOneAndDelete' | 'findOneAndRemove' | 'findOneAndUpdate' | 'remove' | 'update' | 'updateOne' | 'updateMany'; - class Schema = Model> extends events.EventEmitter { + type SchemaPreOptions = { document?: boolean, query?: boolean }; + type SchemaPostOptions = { document?: boolean, query?: boolean }; + + class Schema = Model, SchemaDefinitionType = undefined> extends events.EventEmitter { /** * Create a new schema */ - constructor(definition?: SchemaDefinition, options?: SchemaOptions); + constructor(definition?: SchemaDefinition>, options?: SchemaOptions); /** Adds key path / schema type pairs to this schema. */ - add(obj: SchemaDefinition | Schema, prefix?: string): this; + add(obj: SchemaDefinition> | Schema, prefix?: string): this; /** * Array of child schemas (from document arrays and single nested subdocs) @@ -1113,21 +1120,33 @@ declare module 'mongoose' { plugin(fn: (schema: Schema, opts?: any) => void, opts?: any): this; /** Defines a post hook for the model. */ - post(method: 'validate' | 'save' | 'remove' | 'updateOne' | 'deleteOne' | 'init' | RegExp, fn: (this: T, res: any, next: (err?: CallbackError) => void) => void): this; - post = Query>(method: MongooseQueryMiddleware | string | RegExp, fn: (this: T, res: any, next: (err: CallbackError) => void) => void): this; + post(method: MongooseDocumentMiddleware | MongooseDocumentMiddleware[] | RegExp, fn: (this: T, res: any, next: (err?: CallbackError) => void) => void): this; + post(method: MongooseDocumentMiddleware | MongooseDocumentMiddleware[] | RegExp, options: SchemaPostOptions, fn: (this: T, res: any, next: (err?: CallbackError) => void) => void): this; + post = Query>(method: MongooseQueryMiddleware | MongooseQueryMiddleware[] | string | RegExp, fn: (this: T, res: any, next: (err: CallbackError) => void) => void): this; + post = Query>(method: MongooseQueryMiddleware | MongooseQueryMiddleware[] | string | RegExp, options: SchemaPostOptions, fn: (this: T, res: any, next: (err: CallbackError) => void) => void): this; post = Aggregate>(method: 'aggregate' | RegExp, fn: (this: T, res: Array, next: (err: CallbackError) => void) => void): this; + post = Aggregate>(method: 'aggregate' | RegExp, options: SchemaPostOptions, fn: (this: T, res: Array, next: (err: CallbackError) => void) => void): this; post = M>(method: 'insertMany' | RegExp, fn: (this: T, res: any, next: (err: CallbackError) => void) => void): this; + post = M>(method: 'insertMany' | RegExp, options: SchemaPostOptions, fn: (this: T, res: any, next: (err: CallbackError) => void) => void): this; - post(method: 'validate' | 'save' | 'remove' | 'updateOne' | 'deleteOne' | 'init' | RegExp, fn: (this: T, err: NativeError, res: any, next: (err?: CallbackError) => void) => void): this; - post = Query>(method: MongooseQueryMiddleware | string | RegExp, fn: (this: T, err: NativeError, res: any, next: (err: CallbackError) => void) => void): this; + post(method: MongooseDocumentMiddleware | MongooseDocumentMiddleware[] | RegExp, fn: (this: T, err: NativeError, res: any, next: (err?: CallbackError) => void) => void): this; + post(method: MongooseDocumentMiddleware | MongooseDocumentMiddleware[] | RegExp, options: SchemaPostOptions, fn: (this: T, err: NativeError, res: any, next: (err?: CallbackError) => void) => void): this; + post = Query>(method: MongooseQueryMiddleware | MongooseQueryMiddleware[] | string | RegExp, fn: (this: T, err: NativeError, res: any, next: (err: CallbackError) => void) => void): this; + post = Query>(method: MongooseQueryMiddleware | MongooseQueryMiddleware[] | string | RegExp, options: SchemaPostOptions, fn: (this: T, err: NativeError, res: any, next: (err: CallbackError) => void) => void): this; post = Aggregate>(method: 'aggregate' | RegExp, fn: (this: T, err: NativeError, res: Array, next: (err: CallbackError) => void) => void): this; + post = Aggregate>(method: 'aggregate' | RegExp, options: SchemaPostOptions, fn: (this: T, err: NativeError, res: Array, next: (err: CallbackError) => void) => void): this; post = M>(method: 'insertMany' | RegExp, fn: (this: T, err: NativeError, res: any, next: (err: CallbackError) => void) => void): this; + post = M>(method: 'insertMany' | RegExp, options: SchemaPostOptions, fn: (this: T, err: NativeError, res: any, next: (err: CallbackError) => void) => void): this; /** Defines a pre hook for the model. */ - pre(method: 'validate' | 'save' | 'remove' | 'updateOne' | 'deleteOne' | 'init' | RegExp, fn: (this: T, next: (err?: CallbackError) => void) => void): this; - pre = Query>(method: MongooseQueryMiddleware | string | RegExp, fn: (this: T, next: (err: CallbackError) => void) => void): this; + pre(method: MongooseDocumentMiddleware | MongooseDocumentMiddleware[] | RegExp, fn: (this: T, next: (err?: CallbackError) => void) => void): this; + pre(method: MongooseDocumentMiddleware | MongooseDocumentMiddleware[] | RegExp, options: SchemaPreOptions, fn: (this: T, next: (err?: CallbackError) => void) => void): this; + pre = Query>(method: MongooseQueryMiddleware | MongooseQueryMiddleware[] | string | RegExp, fn: (this: T, next: (err: CallbackError) => void) => void): this; + pre = Query>(method: MongooseQueryMiddleware | MongooseQueryMiddleware[] | string | RegExp, options: SchemaPreOptions, fn: (this: T, next: (err: CallbackError) => void) => void): this; pre = Aggregate>(method: 'aggregate' | RegExp, fn: (this: T, next: (err: CallbackError) => void) => void): this; + pre = Aggregate>(method: 'aggregate' | RegExp, options: SchemaPreOptions, fn: (this: T, next: (err: CallbackError) => void) => void): this; pre = M>(method: 'insertMany' | RegExp, fn: (this: T, next: (err: CallbackError) => void) => void): this; + pre = M>(method: 'insertMany' | RegExp, options: SchemaPreOptions, fn: (this: T, next: (err: CallbackError) => void) => void): this; /** Object of currently defined query helpers on this schema. */ query: any; @@ -1163,11 +1182,17 @@ declare module 'mongoose' { virtualpath(name: string): VirtualType | null; } + type SchemaDefinitionWithBuiltInClass = T extends number + ? (typeof Number | 'number' | 'Number') + : T extends string + ? (typeof String | 'string' | 'String') + : (Function | string); + type SchemaDefinitionProperty = SchemaTypeOptions | - Function | - string | - Schema | - Schema[] | + SchemaDefinitionWithBuiltInClass | + typeof SchemaType | + Schema> | + Schema>[] | SchemaTypeOptions[] | Function[] | SchemaDefinition | @@ -1175,7 +1200,7 @@ declare module 'mongoose' { type SchemaDefinition = T extends undefined ? { [path: string]: SchemaDefinitionProperty; } - : { [path in keyof T]-?: SchemaDefinitionProperty; }; + : { [path in keyof T]?: SchemaDefinitionProperty; }; interface SchemaOptions { /** @@ -1340,7 +1365,7 @@ declare module 'mongoose' { } interface SchemaTypeOptions { - type?: T; + type: T; /** Defines a virtual with the given name that gets/sets this path. */ alias?: string; @@ -1357,7 +1382,7 @@ declare module 'mongoose' { * path cannot be set to a nullish value. If a function, Mongoose calls the * function and only checks for nullish values if the function returns a truthy value. */ - required?: boolean | (() => boolean) | [boolean, string]; + required?: boolean | (() => boolean) | [boolean, string] | [() => boolean, string]; /** * The default value for this path. If a function, Mongoose executes the function @@ -1757,7 +1782,7 @@ declare module 'mongoose' { type ReturnsNewDoc = { new: true } | { returnOriginal: false }; - interface Query { + class Query { _mongooseOptions: MongooseQueryOptions; /** Executes the query */ @@ -2135,6 +2160,8 @@ declare module 'mongoose' { /** Specifies a path for use with chaining. */ where(path: string, val?: any): this; + where(obj: object): this; + where(): this; /** Defines a `$within` or `$geoWithin` argument for geo-spatial queries. */ within(val?: any): this; diff --git a/index.pug b/index.pug index b6c5da11371..b1daf0034b9 100644 --- a/index.pug +++ b/index.pug @@ -226,8 +226,8 @@ html(lang='en') - - + + diff --git a/lib/aggregate.js b/lib/aggregate.js index 7e4093f4e97..d4a65b2d261 100644 --- a/lib/aggregate.js +++ b/lib/aggregate.js @@ -1099,15 +1099,15 @@ function prepareDiscriminatorPipeline(aggregate) { // If the first pipeline stage is a match and it doesn't specify a `__t` // key, add the discriminator key to it. This allows for potential // aggregation query optimizations not to be disturbed by this feature. - if (originalPipeline[0] && originalPipeline[0].$match && !originalPipeline[0].$match[discriminatorKey]) { + if (originalPipeline[0] != null && originalPipeline[0].$match && !originalPipeline[0].$match[discriminatorKey]) { originalPipeline[0].$match[discriminatorKey] = discriminatorValue; // `originalPipeline` is a ref, so there's no need for // aggregate._pipeline = originalPipeline - } else if (originalPipeline[0] && originalPipeline[0].$geoNear) { + } else if (originalPipeline[0] != null && originalPipeline[0].$geoNear) { originalPipeline[0].$geoNear.query = originalPipeline[0].$geoNear.query || {}; originalPipeline[0].$geoNear.query[discriminatorKey] = discriminatorValue; - } else if (originalPipeline[0] && originalPipeline[0].$search) { + } else if (originalPipeline[0] != null && originalPipeline[0].$search) { if (originalPipeline[1] && originalPipeline[1].$match != null) { originalPipeline[1].$match[discriminatorKey] = originalPipeline[1].$match[discriminatorKey] || discriminatorValue; } else { diff --git a/lib/cast.js b/lib/cast.js index 1c24bc7fe65..74ebb5200b6 100644 --- a/lib/cast.js +++ b/lib/cast.js @@ -259,10 +259,11 @@ module.exports = function cast(schema, obj, options, context) { } throw new StrictModeError(path, 'Path "' + path + '" is not in ' + 'schema, strict mode is `true`, and upsert is `true`.'); - } else if (options.strictQuery === 'throw') { - throw new StrictModeError(path, 'Path "' + path + '" is not in ' + - 'schema and strictQuery is \'throw\'.'); - } else if (options.strictQuery) { + } else if (options.strict) { + if (options.strict === 'throw') { + throw new StrictModeError(path, 'Path "' + path + '" is not in ' + + 'schema and strict is \'throw\'.'); + } delete obj[path]; } } else if (val == null) { diff --git a/lib/document.js b/lib/document.js index 453b4a2a9ca..3d7100128ff 100644 --- a/lib/document.js +++ b/lib/document.js @@ -139,7 +139,6 @@ function Document(obj, fields, skipId, options) { }); } } - if (obj) { // Skip set hooks if (this.$__original_set) { @@ -663,7 +662,7 @@ function init(self, obj, doc, opts, prefix) { // Should still work if not a model-level discriminator, but should not be // necessary. This is *only* to catch the case where we queried using the // base model and the discriminated model has a projection - if (self.schema.$isRootDiscriminator && !self.isSelected(path)) { + if (self.schema.$isRootDiscriminator && !self.$__isSelected(path)) { return; } @@ -897,6 +896,7 @@ Document.prototype.overwrite = function overwrite(obj) { */ Document.prototype.$set = function $set(path, val, type, options) { + if (utils.isPOJO(type)) { options = type; type = undefined; @@ -935,9 +935,13 @@ Document.prototype.$set = function $set(path, val, type, options) { path = path._doc; } } + if (path == null) { + const _ = path; + path = val; + val = _; + } prefix = val ? val + '.' : ''; - keys = Object.keys(path); const len = keys.length; @@ -1134,7 +1138,7 @@ Document.prototype.$set = function $set(path, val, type, options) { // traverse the element ({nested: null})` is not likely. If user gets // that error, its their fault for now. We should reconsider disallowing // modifying not selected paths for 6.x - if (!this.isSelected(curPath)) { + if (!this.$__isSelected(curPath)) { this.unmarkModified(curPath); } cur = this.$__getValue(curPath); @@ -1415,7 +1419,7 @@ Document.prototype.$__shouldModify = function(pathToMark, path, constructing, pa return false; } - if (val === void 0 && !this.isSelected(path)) { + if (val === void 0 && !this.$__isSelected(path)) { // when a path is not selected in a query, its initial // value will be undefined. return true; @@ -2059,7 +2063,7 @@ Document.prototype.isSelected = function isSelected(path) { path = path.split(' '); } if (Array.isArray(path)) { - return path.some(p => this.isSelected(p)); + return path.some(p => this.$__isSelected(p)); } const paths = Object.keys(this.$__.selected); @@ -2257,7 +2261,7 @@ function _getPathsToValidate(doc) { // only validate required fields when necessary let paths = new Set(Object.keys(doc.$__.activePaths.states.require).filter(function(path) { - if (!doc.isSelected(path) && !doc.isModified(path)) { + if (!doc.$__isSelected(path) && !doc.isModified(path)) { return false; } if (path in doc.$__.cachedRequired) { @@ -3566,7 +3570,7 @@ function applyGetters(self, json, options) { let part; cur = self._doc; - if (!self.isSelected(path)) { + if (!self.$__isSelected(path)) { continue; } diff --git a/lib/helpers/timestamps/setupTimestamps.js b/lib/helpers/timestamps/setupTimestamps.js index 752a565738c..76140099df8 100644 --- a/lib/helpers/timestamps/setupTimestamps.js +++ b/lib/helpers/timestamps/setupTimestamps.js @@ -51,7 +51,7 @@ module.exports = function setupTimestamps(schema, timestamps) { (this.ownerDocument ? this.ownerDocument() : this).constructor.base.now(); const auto_id = this._id && this._id.auto; - if (!skipCreatedAt && createdAt && !this.get(createdAt) && this.isSelected(createdAt)) { + if (!skipCreatedAt && createdAt && !this.get(createdAt) && this.$__isSelected(createdAt)) { this.$set(createdAt, auto_id ? this._id.getTimestamp() : defaultTimestamp); } diff --git a/lib/helpers/update/castArrayFilters.js b/lib/helpers/update/castArrayFilters.js index 57018d9fb10..8cfd00243f5 100644 --- a/lib/helpers/update/castArrayFilters.js +++ b/lib/helpers/update/castArrayFilters.js @@ -10,13 +10,10 @@ module.exports = function castArrayFilters(query) { if (!Array.isArray(arrayFilters)) { return; } - const update = query.getUpdate(); const schema = query.schema; - const strictQuery = schema.options.strictQuery; - + const strict = schema.options.strict; const updatedPaths = modifiedPaths(update); - const updatedPathsByFilter = Object.keys(updatedPaths).reduce((cur, path) => { const matches = path.match(/\$\[[^\]]+\]/g); if (matches == null) { @@ -33,22 +30,23 @@ module.exports = function castArrayFilters(query) { } return cur; }, {}); - for (const filter of arrayFilters) { if (filter == null) { throw new Error(`Got null array filter in ${arrayFilters}`); } - for (const key in filter) { + for (const key in filter) { if (filter[key] == null) { continue; } - + if (updatedPathsByFilter[key] === null) { + continue; + } + if (Object.keys(updatedPathsByFilter).length === 0) continue; const dot = key.indexOf('.'); let filterPath = dot === -1 ? updatedPathsByFilter[key] + '.0' : updatedPathsByFilter[key.substr(0, dot)] + '.0' + key.substr(dot); - if (filterPath == null) { throw new Error(`Filter path not found for ${key}`); } @@ -56,14 +54,13 @@ module.exports = function castArrayFilters(query) { // If there are multiple array filters in the path being updated, make sure // to replace them so we can get the schema path. filterPath = cleanPositionalOperators(filterPath); - const schematype = getPath(schema, filterPath); if (schematype == null) { - if (!strictQuery) { + if (!strict) { return; } - // For now, treat `strictQuery = true` and `strictQuery = 'throw'` as - // equivalent for casting array filters. `strictQuery = true` doesn't + // For now, treat `strict = true` and `strict = 'throw'` as + // equivalent for casting array filters. `strict = true` doesn't // quite work in this context because we never want to silently strip out // array filters, even if the path isn't in the schema. throw new Error(`Could not find path "${filterPath}" in schema`); diff --git a/lib/index.js b/lib/index.js index 934a4e58568..e0172c5d066 100644 --- a/lib/index.js +++ b/lib/index.js @@ -159,7 +159,6 @@ Mongoose.prototype.driver = require('./driver'); * - 'toObject': `{ transform: true, flattenDecimals: true }` by default. Overwrites default objects to [`toObject()`](/docs/api.html#document_Document-toObject) * - 'toJSON': `{ transform: true, flattenDecimals: true }` by default. Overwrites default objects to [`toJSON()`](/docs/api.html#document_Document-toJSON), for determining how Mongoose documents get serialized by `JSON.stringify()` * - 'strict': true by default, may be `false`, `true`, or `'throw'`. Sets the default strict mode for schemas. - * - 'strictQuery': false by default, may be `false`, `true`, or `'throw'`. Sets the default [strictQuery](/docs/guide.html#strictQuery) mode for schemas. * - 'selectPopulatedPaths': true by default. Set to false to opt out of Mongoose adding all fields that you `populate()` to your `select()`. The schema-level option `selectPopulatedPaths` overwrites this one. * - 'maxTimeMS': If set, attaches [maxTimeMS](https://docs.mongodb.com/manual/reference/operator/meta/maxTimeMS/) to every query * - 'autoIndex': true by default. Set to false to disable automatic index creation for all models associated with this Mongoose instance. diff --git a/lib/model.js b/lib/model.js index 58545a358d0..d66e9510746 100644 --- a/lib/model.js +++ b/lib/model.js @@ -819,7 +819,7 @@ Model.prototype.$__version = function(where, delta) { // there is no way to select the correct version. we could fail // fast here and force them to include the versionKey but // thats a bit intrusive. can we do this automatically? - if (!this.isSelected(key)) { + if (!this.$__isSelected(key)) { return; } diff --git a/lib/query.js b/lib/query.js index d98501258fd..4fc25dce6cf 100644 --- a/lib/query.js +++ b/lib/query.js @@ -1932,7 +1932,6 @@ Query.prototype._unsetCastError = function _unsetCastError() { * - `populate`: an array representing what paths will be populated. Should have one entry for each call to [`Query.prototype.populate()`](/docs/api.html#query_Query-populate) * - `lean`: if truthy, Mongoose will not [hydrate](/docs/api.html#model_Model.hydrate) any documents that are returned from this query. See [`Query.prototype.lean()`](/docs/api.html#query_Query-lean) for more information. * - `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](/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 for backwards compatibility, which means Mongoose will allow `Model.find({ foo: 'bar' })` even if `foo` is not in the schema. See the [`strictQuery` docs](/docs/guide.html#strictQuery) for more information. * - `nearSphere`: use `$nearSphere` instead of `near()`. See the [`Query.prototype.nearSphere()` docs](/docs/api.html#query_Query-nearSphere) * * Mongoose maintains a separate object for internal options because @@ -4814,9 +4813,7 @@ Query.prototype.cast = function(model, obj) { upsert: this.options && this.options.upsert, 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) + get(model, 'schema.options.strict', null) }, this); } catch (err) { // CastError, assign model diff --git a/lib/schema.js b/lib/schema.js index 2d8dffcd6a1..edfbc6b34c6 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -60,7 +60,6 @@ let id = 0; * - [writeConcern](/docs/guide.html#writeConcern): object - defaults to null, use to override [the MongoDB server's default write concern settings](https://docs.mongodb.com/manual/reference/write-concern/) * - [shardKey](/docs/guide.html#shardKey): object - defaults to `null` * - [strict](/docs/guide.html#strict): bool - defaults to true - * - [strictQuery](/docs/guide.html#strictQuery): bool - defaults to false * - [toJSON](/docs/guide.html#toJSON) - object - no default * - [toObject](/docs/guide.html#toObject) - object - no default * - [typeKey](/docs/guide.html#typeKey) - string - defaults to 'type' @@ -400,14 +399,13 @@ Schema.prototype.defaultOptions = function(options) { const baseOptions = get(this, 'base.options', {}); options = utils.options({ strict: 'strict' in baseOptions ? baseOptions.strict : true, - strictQuery: 'strictQuery' in baseOptions ? baseOptions.strictQuery : false, bufferCommands: true, capped: false, // { size, max, autoIndexId } versionKey: '__v', optimisticConcurrency: false, - discriminatorKey: '__t', minimize: true, autoIndex: null, + discriminatorKey: '__t', shardKey: null, read: null, validateBeforeSave: true, diff --git a/lib/validoptions.js b/lib/validoptions.js index 86706c66e8c..0f070b48844 100644 --- a/lib/validoptions.js +++ b/lib/validoptions.js @@ -22,7 +22,6 @@ const VALID_OPTIONS = Object.freeze([ 'selectPopulatedPaths', 'setDefaultsOnInsert', 'strict', - 'strictQuery', 'toJSON', 'toObject', 'typePojoToMixed', diff --git a/package.json b/package.json index 34a8d05fd29..e9c4d5acb45 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "5.11.13", + "version": "5.11.15", "author": "Guillermo Rauch ", "keywords": [ "mongodb", diff --git a/test/document.strict.test.js b/test/document.strict.test.js index 2aa6e235293..9b676f836e5 100644 --- a/test/document.strict.test.js +++ b/test/document.strict.test.js @@ -455,7 +455,6 @@ describe('document: strict mode:', function() { }); nestedSchema.options.strict = 'throw'; - nestedSchema.options.strictQuery = 'throw'; const schema = new mongoose.Schema({ nested: { diff --git a/test/document.test.js b/test/document.test.js index 2f4e3daaf3f..7c838573b76 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -9905,4 +9905,99 @@ describe('document', function() { assert.equal(ss.nested[0].toHexString(), updatedElID); }); }); + it('gh9884', function() { + return co(function*() { + + const obi = new Schema({ + eType: { + type: String, + required: true, + uppercase: true + }, + eOrigin: { + type: String, + required: true + }, + eIds: [ + { + type: String + } + ] + }, { _id: false }); + + const schema = new Schema({ + name: String, + description: String, + isSelected: { + type: Boolean, + default: false + }, + wan: { + type: [obi], + default: undefined, + required: true + } + }); + + const newDoc = { + name: 'name', + description: 'new desc', + isSelected: true, + wan: [ + { + eType: 'X', + eOrigin: 'Y', + eIds: ['Y', 'Z'] + } + ] + }; + + const Model = db.model('Test', schema); + yield Model.create(newDoc); + const doc = yield Model.findOne(); + assert.ok(doc); + }); + }); + + it('gh9880', function(done) { + const testSchema = new Schema({ + prop: String, + nestedProp: { + prop: String + } + }); + const Test = db.model('Test', testSchema); + + new Test({ + prop: 'Test', + nestedProp: null + }).save((err, doc) => { + doc.id; + doc.nestedProp; + + // let's clone this document: + new Test({ + prop: 'Test 2', + nestedProp: doc.nestedProp + }); + + Test.updateOne({ + _id: doc._id + }, { + nestedProp: null + }, (err) => { + assert.ifError(err); + Test.findOne({ + _id: doc._id + }, (err, updatedDoc) => { + assert.ifError(err); + new Test({ + prop: 'Test 3', + nestedProp: updatedDoc.nestedProp + }); + done(); + }); + }); + }); + }); }); diff --git a/test/es-next/cast.test.es6.js b/test/es-next/cast.test.es6.js index efd58eac1ed..07d8280019b 100644 --- a/test/es-next/cast.test.es6.js +++ b/test/es-next/cast.test.es6.js @@ -97,10 +97,10 @@ describe('Cast Tutorial', function() { await query.exec(); }); - it('strictQuery true', async function() { + it('strict true', async function() { mongoose.deleteModel('Character'); const schema = new mongoose.Schema({ name: String, age: Number }, { - strictQuery: true + strict: true }); Character = mongoose.model('Character', schema); @@ -113,10 +113,10 @@ describe('Cast Tutorial', function() { // acquit:ignore:end }); - it('strictQuery throw', async function() { + it('strict throw', async function() { mongoose.deleteModel('Character'); const schema = new mongoose.Schema({ name: String, age: Number }, { - strictQuery: 'throw' + strict: 'throw' }); Character = mongoose.model('Character', schema); @@ -124,12 +124,12 @@ describe('Cast Tutorial', function() { const err = await query.exec().then(() => null, err => err); err.name; // 'StrictModeError' - // Path "notInSchema" is not in schema and strictQuery is 'throw'. + // Path "notInSchema" is not in schema and strict is 'throw'. err.message; // acquit:ignore:start assert.equal(err.name, 'StrictModeError'); assert.equal(err.message, 'Path "notInSchema" is not in schema and ' + - 'strictQuery is \'throw\'.'); + 'strict is \'throw\'.'); // acquit:ignore:end }); diff --git a/test/es-next/virtuals.test.es6.js b/test/es-next/virtuals.test.es6.js index a39b3e999b5..b530d80b14f 100644 --- a/test/es-next/virtuals.test.es6.js +++ b/test/es-next/virtuals.test.es6.js @@ -133,7 +133,7 @@ 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' }); + const doc = await User.findOne({ domain: 'gmail.com' }, null, { strict: false }); doc; // undefined // acquit:ignore:start assert.equal(doc, null); diff --git a/test/helpers/update.castArrayFilters.test.js b/test/helpers/update.castArrayFilters.test.js index fac39ca9fc1..d89fc11a07e 100644 --- a/test/helpers/update.castArrayFilters.test.js +++ b/test/helpers/update.castArrayFilters.test.js @@ -35,7 +35,6 @@ describe('castArrayFilters', function() { arrayFilters: [{ 'x.text': 123 }, { 'y.date': { $gte: '2018-01-01' } }] }); castArrayFilters(q); - assert.strictEqual(q.options.arrayFilters[0]['x.text'], '123'); assert.ok(q.options.arrayFilters[1]['y.date'].$gte instanceof Date); @@ -163,7 +162,7 @@ describe('castArrayFilters', function() { assert.strictEqual(q.options.arrayFilters[1]['nArr.nestedId'], 2); }); - it('respects `strictQuery` option (gh-7728)', function() { + it('respects `strict` option (gh-7728)', function() { const schema = new Schema({ arr: [{ id: Number @@ -180,13 +179,13 @@ describe('castArrayFilters', function() { }; q.updateOne({}, p, opts); - - castArrayFilters(q); - assert.strictEqual(q.options.arrayFilters[0]['arr.notInSchema'], '42'); - - q.schema.options.strictQuery = true; + q.schema.options.strict = true; assert.throws(function() { castArrayFilters(q); }, /Could not find path.*in schema/); + + q.schema.options.strict = false; + castArrayFilters(q); + assert.strictEqual(q.options.arrayFilters[0]['arr.notInSchema'], '42'); }); }); diff --git a/test/model.test.js b/test/model.test.js index 436abb75d7f..28c47ffd326 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -6602,7 +6602,7 @@ describe('Model', function() { return Model.create({ name: 'foo' }). then(() => Model.exists({ name: 'bar' })). then(res => assert.ok(!res)). - then(() => Model.exists({ otherProp: 'foo' })). + then(() => Model.exists({ otherProp: 'foo' }, { strict: false })). then(res => assert.ok(!res)); }); diff --git a/test/query.test.js b/test/query.test.js index 8f0ad74fd8a..7ec98104b9c 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -3260,11 +3260,11 @@ describe('Query', function() { }); }); - it('strictQuery option (gh-4136) (gh-7178)', function() { + it('strict option (gh-4136) (gh-7178)', function() { const modelSchema = new Schema({ field: Number, nested: { path: String } - }, { strictQuery: 'throw' }); + }, { strict: 'throw' }); const Model = db.model('Test', modelSchema); @@ -3272,7 +3272,7 @@ describe('Query', function() { // `find()` on a top-level path not in the schema let err = yield Model.find({ notInschema: 1 }).then(() => null, e => e); assert.ok(err); - assert.ok(err.message.indexOf('strictQuery') !== -1, err.message); + assert.ok(err.message.indexOf('strict') !== -1, err.message); // Shouldn't throw on nested path re: gh-7178 yield Model.create({ nested: { path: 'a' } }); @@ -3282,12 +3282,12 @@ describe('Query', function() { // `find()` on a nested path not in the schema err = yield Model.find({ 'nested.bad': 'foo' }).then(() => null, e => e); assert.ok(err); - assert.ok(err.message.indexOf('strictQuery') !== -1, err.message); + assert.ok(err.message.indexOf('strict') !== -1, err.message); }); }); - it('strictQuery = true (gh-6032)', function() { - const modelSchema = new Schema({ field: Number }, { strictQuery: true }); + it('strict = true (gh-6032)', function() { + const modelSchema = new Schema({ field: Number }, { strict: true }); return co(function*() { const Model = db.model('Test', modelSchema); diff --git a/test/schema.test.js b/test/schema.test.js index 9bf6ccc2022..cb9280e2663 100644 --- a/test/schema.test.js +++ b/test/schema.test.js @@ -2427,35 +2427,6 @@ describe('schema', function() { assert.ok(!schema.virtuals.id); }); - describe('mongoose.set(`strictQuery`, value); (gh-6658)', function() { - let strictQueryOriginalValue; - - this.beforeEach(() => strictQueryOriginalValue = mongoose.get('strictQuery')); - this.afterEach(() => mongoose.set('strictQuery', strictQueryOriginalValue)); - - it('setting `strictQuery` on base sets strictQuery to schema (gh-6658)', function() { - // Arrange - mongoose.set('strictQuery', 'some value'); - - // Act - const schema = new Schema(); - - // Assert - assert.equal(schema.get('strictQuery'), 'some value'); - }); - - it('`strictQuery` set on base gets overwritten by option set on schema (gh-6658)', function() { - // Arrange - mongoose.set('strictQuery', 'base option'); - - // Act - const schema = new Schema({}, { strictQuery: 'schema option' }); - - // Assert - assert.equal(schema.get('strictQuery'), 'schema option'); - }); - }); - it('treats dotted paths with no parent as a nested path (gh-9020)', function() { const customerSchema = new Schema({ 'card.brand': String, diff --git a/test/schema.validation.test.js b/test/schema.validation.test.js index 1e773fea9ee..01db9695c47 100644 --- a/test/schema.validation.test.js +++ b/test/schema.validation.test.js @@ -1190,9 +1190,10 @@ describe('schema', function() { }); const Breakfast = mongoose.model('gh2832', breakfast, 'gh2832'); - Breakfast.create({ description: undefined }, function(error) { + const bad = new Breakfast({ description: 'test' }); + bad.validate(function(error) { assert.ok(error); - const errorMessage = 'ValidationError: description: Cast to String failed for value "undefined" at path "description"'; + const errorMessage = 'ValidationError: description: Cast to String failed for value "test" at path "description"'; assert.equal(errorMessage, error.toString()); assert.ok(error.errors.description); assert.equal(error.errors.description.reason.toString(), 'Error: oops'); diff --git a/test/typescript/create.ts b/test/typescript/create.ts index c20f56cc308..cb76ce45172 100644 --- a/test/typescript/create.ts +++ b/test/typescript/create.ts @@ -18,4 +18,11 @@ Test.create({ name: 'test' }, { name: 'test2' }).then((docs: ITest[]) => console Test.insertMany({ name: 'test' }).then((doc: ITest) => console.log(doc.name)); Test.insertMany([{ name: 'test' }], { session: null }).then((docs: ITest[]) => console.log(docs[0].name)); -Test.create([{ name: 'test' }], { validateBeforeSave: true }).then((docs: ITest[]) => console.log(docs[0].name)); \ No newline at end of file +Test.create([{ name: 'test' }], { validateBeforeSave: true }).then((docs: ITest[]) => console.log(docs[0].name)); + +(async() => { + const [t1] = await Test.create([{ name: 'test' }]); + const [t2, t3, t4] = await Test.create({ name: 'test' }, { name: 'test' }, { name: 'test' }); + (await Test.create([{ name: 'test' }]))[0]; + (await Test.create({ name: 'test' }))._id; +})(); diff --git a/test/typescript/main.test.js b/test/typescript/main.test.js index fc9d2f06f46..2d6b1188074 100644 --- a/test/typescript/main.test.js +++ b/test/typescript/main.test.js @@ -178,7 +178,9 @@ describe('typescript syntax', function() { if (process.env.D && errors.length) { console.log(errors); } - assert.equal(errors.length, 0); + assert.equal(errors.length, 1); + const messageText = errors[0].messageText.messageText; + assert.ok(/Type 'StringConstructor' is not assignable to type.*number/.test(messageText), messageText); }); it('document', function() { diff --git a/test/typescript/middleware.ts b/test/typescript/middleware.ts index 5c941ea887a..7a89a3e34c0 100644 --- a/test/typescript/middleware.ts +++ b/test/typescript/middleware.ts @@ -18,6 +18,10 @@ schema.post>('aggregate', async function(res: Array) { console.log('Pipeline', this.pipeline(), res[0]); }); +schema.pre(['save', 'validate'], { query: false, document: true }, async function applyChanges() { + await Test.findOne({}); +}); + interface ITest extends Document { name?: string; } diff --git a/test/typescript/queries.ts b/test/typescript/queries.ts index 699742dd757..54807420698 100644 --- a/test/typescript/queries.ts +++ b/test/typescript/queries.ts @@ -1,4 +1,4 @@ -import { Schema, model, Document, Types } from 'mongoose'; +import { Schema, model, Document, Types, Query } from 'mongoose'; const schema: Schema = new Schema({ name: { type: 'String' }, tags: [String] }); @@ -13,9 +13,11 @@ const Test = model('Test', schema); Test.count({ name: /Test/ }).exec().then((res: number) => console.log(res)); +// ObjectId casting Test.find({ parent: new Types.ObjectId('0'.repeat(24)) }); Test.find({ parent: '0'.repeat(24) }); +// Operators Test.find({ name: { $in: ['Test'] } }).exec().then((res: Array) => console.log(res)); Test.find({ name: 'test' }, (err: Error, docs: ITest[]) => { @@ -45,4 +47,11 @@ Test.findOneAndUpdate({ name: 'test' }, { name: 'test3' }, { new: true, upsert: Test.findOneAndReplace({ name: 'test' }, { _id: new Types.ObjectId(), name: 'test2' }).exec().then((res: ITest | null) => console.log(res)); Test.findOneAndUpdate({ name: 'test' }, { $addToSet: { tags: 'each' } }); -Test.findOneAndUpdate({ name: 'test' }, { $push: { tags: 'each' } }); \ No newline at end of file +Test.findOneAndUpdate({ name: 'test' }, { $push: { tags: 'each' } }); + +const query: Query = Test.findOne(); +query instanceof Query; + +// Chaining +Test.findOne().where({ name: 'test' }); +Test.where().find({ name: 'test' }); \ No newline at end of file diff --git a/test/typescript/schema.ts b/test/typescript/schema.ts index 79348e2ba29..4aed0505b31 100644 --- a/test/typescript/schema.ts +++ b/test/typescript/schema.ts @@ -1,4 +1,4 @@ -import { Schema } from 'mongoose'; +import { Schema, Document, SchemaDefinition, SchemaDefinitionProperty, Model } from 'mongoose'; enum Genre { Action, @@ -23,5 +23,58 @@ const movieSchema: Schema = new Schema({ type: String, enum: Genre, required: true + }, + actionIntensity: { + type: Number, + required: [ + function(this: { genre: Genre }) { + return this.genre === Genre.Action; + }, + 'Action intensity required for action genre' + ] } }); + +// Using `SchemaDefinition` +interface IProfile { age: number; } +interface ProfileDoc extends Document, IProfile {} +const ProfileSchemaDef: SchemaDefinition = { age: Number }; +export const ProfileSchema = new Schema, ProfileDoc>(ProfileSchemaDef); + +interface IUser { + email: string; + profile: ProfileDoc; +} + +interface UserDoc extends Document, IUser {} + +const ProfileSchemaDef2: SchemaDefinition = { + age: Schema.Types.Number +}; + +const ProfileSchema2: Schema> = new Schema(ProfileSchemaDef2); + +const UserSchemaDef: SchemaDefinition = { + email: String, + profile: ProfileSchema2 +}; + +async function gh9857() { + interface User { + name: number; + active: boolean; + points: number; + } + + type UserDocument = Document; + type UserSchemaDefinition = SchemaDefinition; + type UserModel = Model; + + const schemaDefinition: UserSchemaDefinition = { + name: String, + active: Boolean, + points: Number + }; + + const schema = new Schema(schemaDefinition); +}