diff --git a/.eslintrc.js b/.eslintrc.js index 9d067d9de2..1bf13a95d2 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -13,7 +13,8 @@ module.exports = { '*.min.js', '**/docs/js/native.js', '!.*', - 'node_modules' + 'node_modules', + '.git' ], overrides: [ { diff --git a/CHANGELOG.md b/CHANGELOG.md index 122ecdfdae..8ecb851fb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +7.5.3 / 2023-09-25 +================== + * fix(document): handle MongoDB Long when casting BigInts #13869 #13791 + * fix(model): make bulkSave() persist changes that happen in pre('save') middleware #13885 #13799 + * fix: handle casting $elemMatch underneath $not underneath another $elemMatch #13893 #13880 + * fix(model): make bulkWrite casting respect global setDefaultsOnInsert #13870 #13823 + * fix(document): handle default values for discriminator key with embedded discriminators #13891 #13835 + * fix: account for null values when assigning isNew property within document array #13883 + * types: avoid "interface can only extend object types with statically known members" error in TypeScript 4 #13871 + * docs(deprecations): fix typo in includeResultMetadata deprecation docs #13884 #13844 + * docs: fix pre element overflow in home page #13868 [ghoshRitesh12](https://github.com/ghoshRitesh12) + +7.5.2 / 2023-09-15 +================== + * fix(schema): handle number discriminator keys when using Schema.prototype.discriminator() #13858 #13788 + * fix: ignore `id` property when calling `set()` with both `id` and `_id` specified to avoid `id` setter overwriting #13762 + * types: pass correct document type to required and default function #13851 #13797 + * docs(model): add examples of using diffIndexes() to syncIndexes()and diffIndexes() api docs #13850 #13771 + 7.5.1 / 2023-09-11 ================== * fix: set default value for _update when no update object is provided and versionKey is set to false #13795 #13783 [MohOraby](https://github.com/MohOraby) diff --git a/docs/css/style.css b/docs/css/style.css index f5ab8822aa..32edaaea47 100644 --- a/docs/css/style.css +++ b/docs/css/style.css @@ -46,6 +46,7 @@ pre { background: #eee; padding: 5px; border-radius: 3px; + overflow-x: auto; } code { color: #333; diff --git a/docs/deprecations.md b/docs/deprecations.md index be004d7ced..bb194674e1 100644 --- a/docs/deprecations.md +++ b/docs/deprecations.md @@ -9,7 +9,7 @@ cause any problems for your application. Please [report any issues on GitHub](ht To fix all deprecation warnings, follow the below steps: -* Replace `rawResult: true` with `includeResultMetadata: false` in `findOneAndUpdate()`, `findOneAndReplace()`, `findOneAndDelete()` calls. +* Replace `rawResult: true` with `includeResultMetadata: true` in `findOneAndUpdate()`, `findOneAndReplace()`, `findOneAndDelete()` calls. Read below for more a more detailed description of each deprecation warning. diff --git a/lib/cast.js b/lib/cast.js index 61b1f8caf1..4b9556b41a 100644 --- a/lib/cast.js +++ b/lib/cast.js @@ -309,8 +309,21 @@ module.exports = function cast(schema, obj, options, context) { while (k--) { $cond = ks[k]; nested = val[$cond]; - - if ($cond === '$not') { + if ($cond === '$elemMatch') { + if (nested && schematype != null && schematype.schema != null) { + cast(schematype.schema, nested, options, context); + } else if (nested && schematype != null && schematype.$isMongooseArray) { + if (utils.isPOJO(nested) && nested.$not != null) { + cast(schema, nested, options, context); + } else { + val[$cond] = schematype.castForQuery( + $cond, + nested, + context + ); + } + } + } else if ($cond === '$not') { if (nested && schematype) { _keys = Object.keys(nested); if (_keys.length && isOperator(_keys[0])) { @@ -337,6 +350,7 @@ module.exports = function cast(schema, obj, options, context) { context ); } + } } } else if (Array.isArray(val) && ['Buffer', 'Array'].indexOf(schematype.instance) === -1) { diff --git a/lib/cast/bigint.js b/lib/cast/bigint.js index 7191311d64..20e01ae58b 100644 --- a/lib/cast/bigint.js +++ b/lib/cast/bigint.js @@ -1,6 +1,7 @@ 'use strict'; const assert = require('assert'); +const { Long } = require('bson'); /** * Given a value, cast it to a BigInt, or throw an `Error` if the value @@ -23,6 +24,10 @@ module.exports = function castBigInt(val) { return val; } + if (val instanceof Long) { + return val.toBigInt(); + } + if (typeof val === 'string' || typeof val === 'number') { return BigInt(val); } diff --git a/lib/document.js b/lib/document.js index c31afcaca5..ba8d7bf145 100644 --- a/lib/document.js +++ b/lib/document.js @@ -1667,7 +1667,11 @@ Document.prototype.$__set = function(pathToMark, path, options, constructing, pa val[arrayAtomicsSymbol] = priorVal[arrayAtomicsSymbol]; val[arrayAtomicsBackupSymbol] = priorVal[arrayAtomicsBackupSymbol]; if (utils.isMongooseDocumentArray(val)) { - val.forEach(doc => { doc.isNew = false; }); + val.forEach(doc => { + if (doc != null) { + doc.$isNew = false; + } + }); } } diff --git a/lib/drivers/node-mongodb-native/connection.js b/lib/drivers/node-mongodb-native/connection.js index 6898d602e1..cfdfd075b8 100644 --- a/lib/drivers/node-mongodb-native/connection.js +++ b/lib/drivers/node-mongodb-native/connection.js @@ -349,7 +349,7 @@ function _setClient(conn, client, options, dbName) { client.s.options.hosts && client.s.options.hosts[0] && client.s.options.hosts[0].port || void 0; - conn.name = dbName != null ? dbName : client && client.s && client.s.options && client.s.options.dbName || void 0; + conn.name = dbName != null ? dbName : db.databaseName; conn._closeCalled = client._closeCalled; const _handleReconnect = () => { diff --git a/lib/helpers/discriminator/getConstructor.js b/lib/helpers/discriminator/getConstructor.js index 7a821c5c99..17583e271e 100644 --- a/lib/helpers/discriminator/getConstructor.js +++ b/lib/helpers/discriminator/getConstructor.js @@ -7,15 +7,18 @@ const getDiscriminatorByValue = require('./getDiscriminatorByValue'); * @api private */ -module.exports = function getConstructor(Constructor, value) { +module.exports = function getConstructor(Constructor, value, defaultDiscriminatorValue) { const discriminatorKey = Constructor.schema.options.discriminatorKey; - if (value != null && - Constructor.discriminators && - value[discriminatorKey] != null) { - if (Constructor.discriminators[value[discriminatorKey]]) { - Constructor = Constructor.discriminators[value[discriminatorKey]]; + let discriminatorValue = (value != null && value[discriminatorKey]); + if (discriminatorValue == null) { + discriminatorValue = defaultDiscriminatorValue; + } + if (Constructor.discriminators && + discriminatorValue != null) { + if (Constructor.discriminators[discriminatorValue]) { + Constructor = Constructor.discriminators[discriminatorValue]; } else { - const constructorByValue = getDiscriminatorByValue(Constructor.discriminators, value[discriminatorKey]); + const constructorByValue = getDiscriminatorByValue(Constructor.discriminators, discriminatorValue); if (constructorByValue) { Constructor = constructorByValue; } diff --git a/lib/helpers/model/castBulkWrite.js b/lib/helpers/model/castBulkWrite.js index 94a3880602..fb3dab0616 100644 --- a/lib/helpers/model/castBulkWrite.js +++ b/lib/helpers/model/castBulkWrite.js @@ -20,6 +20,8 @@ const setDefaultsOnInsert = require('../setDefaultsOnInsert'); module.exports = function castBulkWrite(originalModel, op, options) { const now = originalModel.base.now(); + + const globalSetDefaultsOnInsert = originalModel.base.options.setDefaultsOnInsert; if (op['insertOne']) { return (callback) => { const model = decideModelByObject(originalModel, op['insertOne']['document']); @@ -69,7 +71,10 @@ module.exports = function castBulkWrite(originalModel, op, options) { applyTimestampsToChildren(now, op['updateOne']['update'], model.schema); } - if (op['updateOne'].setDefaultsOnInsert !== false) { + const shouldSetDefaultsOnInsert = op['updateOne'].setDefaultsOnInsert == null ? + globalSetDefaultsOnInsert : + op['updateOne'].setDefaultsOnInsert; + if (shouldSetDefaultsOnInsert !== false) { setDefaultsOnInsert(op['updateOne']['filter'], model.schema, op['updateOne']['update'], { setDefaultsOnInsert: true, upsert: op['updateOne'].upsert @@ -106,7 +111,11 @@ module.exports = function castBulkWrite(originalModel, op, options) { const schema = model.schema; const strict = options.strict != null ? options.strict : model.schema.options.strict; - if (op['updateMany'].setDefaultsOnInsert !== false) { + const shouldSetDefaultsOnInsert = op['updateMany'].setDefaultsOnInsert == null ? + globalSetDefaultsOnInsert : + op['updateMany'].setDefaultsOnInsert; + + if (shouldSetDefaultsOnInsert !== false) { setDefaultsOnInsert(op['updateMany']['filter'], model.schema, op['updateMany']['update'], { setDefaultsOnInsert: true, upsert: op['updateMany'].upsert diff --git a/lib/index.js b/lib/index.js index d251828e4f..082ae237fd 100644 --- a/lib/index.js +++ b/lib/index.js @@ -624,8 +624,8 @@ Mongoose.prototype._model = function(name, schema, collection, options) { connection.emit('model', model); if (schema._applyDiscriminators != null) { - for (const disc of Object.keys(schema._applyDiscriminators)) { - model.discriminator(disc, schema._applyDiscriminators[disc]); + for (const disc of schema._applyDiscriminators.keys()) { + model.discriminator(disc, schema._applyDiscriminators.get(disc)); } } diff --git a/lib/model.js b/lib/model.js index 3a67d4578e..09d924a7a5 100644 --- a/lib/model.js +++ b/lib/model.js @@ -3464,11 +3464,9 @@ Model.bulkWrite = async function bulkWrite(ops, options) { * @param {Boolean} [options.j=true] If false, disable [journal acknowledgement](https://www.mongodb.com/docs/manual/reference/write-concern/#j-option) * */ -Model.bulkSave = async function(documents, options) { +Model.bulkSave = async function bulkSave(documents, options) { options = options || {}; - const writeOperations = this.buildBulkWriteOperations(documents, { skipValidation: true, timestamps: options.timestamps }); - if (options.timestamps != null) { for (const document of documents) { document.$__.saveOptions = document.$__.saveOptions || {}; @@ -3485,6 +3483,8 @@ Model.bulkSave = async function(documents, options) { await Promise.all(documents.map(buildPreSavePromise)); + const writeOperations = this.buildBulkWriteOperations(documents, { skipValidation: true, timestamps: options.timestamps }); + const { bulkWriteResult, bulkWriteError } = await this.bulkWrite(writeOperations, options).then( (res) => ({ bulkWriteResult: res, bulkWriteError: null }), (err) => ({ bulkWriteResult: null, bulkWriteError: err }) diff --git a/lib/schema.js b/lib/schema.js index e0e0b03bf1..0f04e836c7 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -449,7 +449,7 @@ Schema.prototype._clone = function _clone(Constructor) { s.discriminators = Object.assign({}, this.discriminators); } if (this._applyDiscriminators != null) { - s._applyDiscriminators = Object.assign({}, this._applyDiscriminators); + s._applyDiscriminators = new Map(this._applyDiscriminators); } s.aliases = Object.assign({}, this.aliases); @@ -621,7 +621,8 @@ Schema.prototype.defaultOptions = function(options) { * @api public */ Schema.prototype.discriminator = function(name, schema) { - this._applyDiscriminators = Object.assign(this._applyDiscriminators || {}, { [name]: schema }); + this._applyDiscriminators = this._applyDiscriminators || new Map(); + this._applyDiscriminators.set(name, schema); return this; }; @@ -722,18 +723,18 @@ Schema.prototype.add = function add(obj, prefix) { for (const key in val[0].discriminators) { schemaType.discriminator(key, val[0].discriminators[key]); } - } else if (val[0] != null && val[0].instanceOfSchema && utils.isPOJO(val[0]._applyDiscriminators)) { - const applyDiscriminators = val[0]._applyDiscriminators || []; + } else if (val[0] != null && val[0].instanceOfSchema && val[0]._applyDiscriminators instanceof Map) { + const applyDiscriminators = val[0]._applyDiscriminators; const schemaType = this.path(prefix + key); - for (const disc in applyDiscriminators) { - schemaType.discriminator(disc, applyDiscriminators[disc]); + for (const disc of applyDiscriminators.keys()) { + schemaType.discriminator(disc, applyDiscriminators.get(disc)); } } - else if (val != null && val.instanceOfSchema && utils.isPOJO(val._applyDiscriminators)) { - const applyDiscriminators = val._applyDiscriminators || []; + else if (val != null && val.instanceOfSchema && val._applyDiscriminators instanceof Map) { + const applyDiscriminators = val._applyDiscriminators; const schemaType = this.path(prefix + key); - for (const disc in applyDiscriminators) { - schemaType.discriminator(disc, applyDiscriminators[disc]); + for (const disc of applyDiscriminators.keys()) { + schemaType.discriminator(disc, applyDiscriminators.get(disc)); } } } else if (Object.keys(val).length < 1) { diff --git a/lib/schema/subdocument.js b/lib/schema/subdocument.js index 7a837427f3..031c887a83 100644 --- a/lib/schema/subdocument.js +++ b/lib/schema/subdocument.js @@ -161,7 +161,9 @@ SchemaSubdocument.prototype.cast = function(val, doc, init, priorVal, options) { throw new ObjectExpectedError(this.path, val); } - const Constructor = getConstructor(this.caster, val); + const discriminatorKeyPath = this.schema.path(this.schema.options.discriminatorKey); + const defaultDiscriminatorValue = discriminatorKeyPath == null ? null : discriminatorKeyPath.getDefault(doc); + const Constructor = getConstructor(this.caster, val, defaultDiscriminatorValue); let subdoc; diff --git a/package.json b/package.json index 49ba8faafd..1239b1065c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "7.5.1", + "version": "7.5.3", "author": "Guillermo Rauch ", "keywords": [ "mongodb", diff --git a/scripts/generateSearch.js b/scripts/generateSearch.js index 8cf41f5646..e98421bdad 100644 --- a/scripts/generateSearch.js +++ b/scripts/generateSearch.js @@ -132,6 +132,7 @@ async function run() { await Content.init(); await Content.deleteMany({ version }); + let count = 0; for (const content of contents) { if (version === '7.x') { let url = content.url.startsWith('/') ? content.url : `/${content.url}`; @@ -143,6 +144,7 @@ async function run() { const url = content.url.startsWith('/') ? content.url : `/${content.url}`; content.url = `/docs/${version}/docs${url}`; } + console.log(`${++count} / ${contents.length}`); await content.save(); } diff --git a/test/document.test.js b/test/document.test.js index 6f41e2f2c9..a6600a92e7 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -12402,6 +12402,97 @@ describe('document', function() { const fromDb = await Test.findById(x._id).lean(); assert.equal(fromDb._id, 1); }); + it('handles bigint (gh-13791)', async function() { + const testSchema = new mongoose.Schema({ + n: Number, + reward: BigInt + }); + const Test = db.model('Test', testSchema); + + const a = await Test.create({ n: 1, reward: 14055648105137340n }); + const b = await Test.findOne({ n: 1 }); + assert.equal(a.reward, 14055648105137340n); + assert.equal(b.reward, 14055648105137340n); + }); + it('should allow null values in list in self assignment (gh-13859)', async function() { + const objSchema = new Schema({ + date: Date, + value: Number + }); + + const testSchema = new Schema({ + intArray: [Number], + strArray: [String], + objArray: [objSchema] + }); + const Test = db.model('Test', testSchema); + + const doc = new Test({ + intArray: [1, 2, 3, null], + strArray: ['b', null, 'c'], + objArray: [ + { date: new Date(1000), value: 1 }, + null, + { date: new Date(3000), value: 3 } + ] + }); + await doc.save(); + doc.intArray = doc.intArray; + doc.strArray = doc.strArray; + doc.objArray = doc.objArray; // this is the trigger for the error + assert.ok(doc); + await doc.save(); + assert.ok(doc); + }); + + it('bulkSave() picks up changes in pre("save") middleware (gh-13799)', async() => { + const schema = new Schema({ name: String, _age: { type: Number, min: 0, default: 0 } }); + schema.pre('save', function() { + this._age = this._age + 1; + }); + + const Person = db.model('Person', schema, 'Persons'); + const person = new Person({ name: 'Jean-Luc Picard', _age: 59 }); + + await Person.bulkSave([person]); + + let updatedPerson = await Person.findById(person._id); + + assert.equal(updatedPerson?._age, 60); + + await Person.bulkSave([updatedPerson]); + + updatedPerson = await Person.findById(person._id); + + assert.equal(updatedPerson?._age, 61); + }); + + it('handles default embedded discriminator values (gh-13835)', async function() { + const childAbstractSchema = new Schema( + { kind: { type: Schema.Types.String, enum: ['concreteKind'], required: true, default: 'concreteKind' } }, + { discriminatorKey: 'kind', _id: false } + ); + const childConcreteSchema = new Schema({ concreteProp: { type: Number, required: true } }); + + const parentSchema = new Schema( + { + child: { + type: childAbstractSchema, + required: true + } + }, + { _id: false } + ); + + parentSchema.path('child').discriminator('concreteKind', childConcreteSchema); + + const ParentModel = db.model('Test', parentSchema); + + const parent = new ParentModel({ child: { concreteProp: 123 } }); + assert.strictEqual(parent.child.concreteProp, 123); + assert.strictEqual(parent.get('child.concreteProp'), 123); + assert.strictEqual(parent.toObject().child.concreteProp, 123); + }); }); describe('Check if instance function that is supplied in schema option is availabe', function() { diff --git a/test/model.query.casting.test.js b/test/model.query.casting.test.js index 85a88513e3..a88e74dde5 100644 --- a/test/model.query.casting.test.js +++ b/test/model.query.casting.test.js @@ -754,6 +754,18 @@ describe('model query casting', function() { assert.strictEqual(doc.outerArray[0].innerArray[0], 'onetwothree'); }); }); + it('should not throw a cast error when dealing with an array of an array of strings in combination with $elemMach and $not (gh-13880)', async function() { + const testSchema = new Schema({ + arr: [[String]] + }); + const Test = db.model('Test', testSchema); + const doc = new Test({ arr: [[1, 2, 3], [2, 3, 4]] }); + await doc.save(); + const query = { arr: { $elemMatch: { $not: { $elemMatch: { $eq: '1' } } } } }; + const res = await Test.find(query); + assert(res); + assert(res[0].arr); + }); }); function _geojsonPoint(coordinates) { diff --git a/test/model.test.js b/test/model.test.js index 0a0903715c..a115660661 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -5657,6 +5657,39 @@ describe('Model', function() { }); + it('bulkWrite skips defaults based on global setDefaultsOnInsert (gh-13823)', async function() { + const m = new mongoose.Mongoose(); + m.set('setDefaultsOnInsert', false); + await m.connect(start.uri); + + const Test = m.model('Test', Schema({ + name: String, + age: Number, + status: { + type: Number, + enum: [1, 2], + default: 1 + } + })); + await Test.bulkWrite([{ + updateOne: { + filter: { + name: 'test1' + }, + update: { + $set: { + age: 19 + } + }, + upsert: true + } + }]); + const doc = await Test.findOne({ name: 'test1' }).lean(); + assert.ok(!doc.status); + + await m.disconnect(); + }); + it('bulkWrite upsert works when update casts to empty (gh-8698)', async function() { const userSchema = new Schema({ name: String diff --git a/test/schema.test.js b/test/schema.test.js index 495a79393a..a0e015ea9d 100644 --- a/test/schema.test.js +++ b/test/schema.test.js @@ -2873,6 +2873,43 @@ describe('schema', function() { assert(batch.message); }); + it('supports numbers with Schema.discriminator() (gh-13788)', async() => { + const baseClassSchema = new Schema({ + type: { type: Number, required: true } + }, { discriminatorKey: 'type' }); + + class BaseClass { + whoAmI() { + return 'I am base'; + } + } + BaseClass.type = 1; + + baseClassSchema.loadClass(BaseClass); + + class NumberTyped extends BaseClass { + whoAmI() { + return 'I am NumberTyped'; + } + } + NumberTyped.type = 2; + + class StringTyped extends BaseClass { + whoAmI() { + return 'I am StringTyped'; + } + } + StringTyped.type = '3'; + + baseClassSchema.discriminator(2, new Schema({}).loadClass(NumberTyped)); + baseClassSchema.discriminator('3', new Schema({}).loadClass(StringTyped)); + const Test = db.model('Test', { item: baseClassSchema }); + let doc = await Test.create({ item: { type: 2 } }); + assert.equal(doc.item.whoAmI(), 'I am NumberTyped'); + doc = await Test.create({ item: { type: '3' } }); + assert.equal(doc.item.whoAmI(), 'I am StringTyped'); + }); + it('can use on as a schema property (gh-11580)', async() => { const testSchema = new mongoose.Schema({ on: String diff --git a/types/index.d.ts b/types/index.d.ts index 1bd4b0a708..f38324417a 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -230,13 +230,13 @@ declare module 'mongoose' { ObtainDocumentType>, ResolveSchemaOptions >, - THydratedDocumentType = HydratedDocument + THydratedDocumentType = HydratedDocument, TVirtuals & TInstanceMethods> > extends events.EventEmitter { /** * Create a new schema */ - constructor(definition?: SchemaDefinition, EnforcedDocType> | DocType, options?: SchemaOptions | ResolveSchemaOptions); + constructor(definition?: SchemaDefinition, EnforcedDocType> | DocType, options?: SchemaOptions, TInstanceMethods, TQueryHelpers, TStaticMethods, TVirtuals, THydratedDocumentType> | ResolveSchemaOptions); /** Adds key path / schema type pairs to this schema. */ add(obj: SchemaDefinition> | Schema, prefix?: string): this;