diff --git a/.npmignore b/.npmignore index 70115377db2..efbbf2be524 100644 --- a/.npmignore +++ b/.npmignore @@ -40,4 +40,6 @@ renovate.json webpack.config.js webpack.base.config.js -.nyc-output \ No newline at end of file +.nyc-output + +*.tgz diff --git a/CHANGELOG.md b/CHANGELOG.md index d54e3a7223e..ced766bcd51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +6.7.4 / 2022-11-28 +================== + * fix: allow setting global strictQuery after Schema creation #12717 #12703 [lpizzinidev](https://github.com/lpizzinidev) + * fix(cursor): make eachAsync() avoid modifying batch when mixing parallel and batchSize #12716 + * fix(types): infer virtuals in query results #12727 #12702 #12684 + * fix(types): correctly infer ReadonlyArray types in schema definitions #12720 + * fix(types): avoid typeof Query with generics for TypeScript 4.6 support #12712 #12688 + * chore: avoid bundling .tgz files when publishing #12725 [hasezoey](https://github.com/hasezoey) + +6.7.3 / 2022-11-22 +================== + * fix(document): handle setting array to itself after saving and pushing a new value #12672 #12656 + * fix(types): update replaceWith pipeline stage #12715 [coyotte508](https://github.com/coyotte508) + * fix(types): remove incorrect modelName type definition #12682 #12669 [lpizzinidev](https://github.com/lpizzinidev) + * fix(schema): fix setupTimestamps for browser.umd #12683 [raphael-papazikas](https://github.com/raphael-papazikas) + * docs: correct justOne description #12686 #12599 [tianguangcn](https://github.com/tianguangcn) + * docs: make links more consistent #12690 #12645 [hasezoey](https://github.com/hasezoey) + * docs(document): explain that $isNew is false in post('save') hooks #12685 #11990 + * docs: fixed line causing a "used before defined" linting error #12707 [sgpinkus](https://github.com/sgpinkus) + 6.7.2 / 2022-11-07 ================== * fix(discriminator): skip copying base schema plugins if `applyPlugins == false` #12613 #12604 [lpizzinidev](https://github.com/lpizzinidev) diff --git a/lib/cast.js b/lib/cast.js index 1cb88406de4..ea0bccdca3f 100644 --- a/lib/cast.js +++ b/lib/cast.js @@ -264,7 +264,7 @@ module.exports = function cast(schema, obj, options, context) { } const strict = 'strict' in options ? options.strict : schema.options.strict; - const strictQuery = getStrictQuery(options, schema._userProvidedOptions, schema.options); + const strictQuery = getStrictQuery(options, schema._userProvidedOptions, schema.options, context); if (options.upsert && strict) { if (strict === 'throw') { throw new StrictModeError(path); @@ -374,7 +374,7 @@ function _cast(val, numbertype, context) { } } -function getStrictQuery(queryOptions, schemaUserProvidedOptions, schemaOptions) { +function getStrictQuery(queryOptions, schemaUserProvidedOptions, schemaOptions, context) { if ('strictQuery' in queryOptions) { return queryOptions.strictQuery; } @@ -387,5 +387,17 @@ function getStrictQuery(queryOptions, schemaUserProvidedOptions, schemaOptions) if ('strict' in schemaUserProvidedOptions) { return schemaUserProvidedOptions.strict; } + const mongooseOptions = context.mongooseCollection && + context.mongooseCollection.conn && + context.mongooseCollection.conn.base && + context.mongooseCollection.conn.base.options; + if (mongooseOptions) { + if ('strictQuery' in mongooseOptions) { + return mongooseOptions.strictQuery; + } + if ('strict' in mongooseOptions) { + return mongooseOptions.strict; + } + } return schemaOptions.strictQuery; } diff --git a/lib/helpers/common.js b/lib/helpers/common.js index c1433ce4748..fb3dbdd098a 100644 --- a/lib/helpers/common.js +++ b/lib/helpers/common.js @@ -7,6 +7,8 @@ const Binary = require('../driver').get().Binary; const isBsonType = require('./isBsonType'); const isMongooseObject = require('./isMongooseObject'); +const MongooseError = require('../error'); +const util = require('util'); exports.flatten = flatten; exports.modifiedPaths = modifiedPaths; @@ -67,7 +69,25 @@ function flatten(update, path, options, schema) { * ignore */ -function modifiedPaths(update, path, result) { +function modifiedPaths(update, path, result, recursion = null) { + if (update == null || typeof update !== 'object') { + return; + } + + if (recursion == null) { + recursion = { + raw: { update, path }, + trace: new WeakSet() + }; + } + + if (recursion.trace.has(update)) { + throw new MongooseError(`a circular reference in the update value, updateValue: +${util.inspect(recursion.raw.update, { showHidden: false, depth: 1 })} +updatePath: '${recursion.raw.path}'`); + } + recursion.trace.add(update); + const keys = Object.keys(update || {}); const numKeys = keys.length; result = result || {}; @@ -83,7 +103,7 @@ function modifiedPaths(update, path, result) { val = val.toObject({ transform: false, virtuals: false }); } if (shouldFlatten(val)) { - modifiedPaths(val, path + key, result); + modifiedPaths(val, path + key, result, recursion); } } @@ -96,11 +116,11 @@ function modifiedPaths(update, path, result) { function shouldFlatten(val) { return val && - typeof val === 'object' && - !(val instanceof Date) && - !isBsonType(val, 'ObjectID') && - (!Array.isArray(val) || val.length !== 0) && - !(val instanceof Buffer) && - !isBsonType(val, 'Decimal128') && - !(val instanceof Binary); + typeof val === 'object' && + !(val instanceof Date) && + !isBsonType(val, 'ObjectID') && + (!Array.isArray(val) || val.length !== 0) && + !(val instanceof Buffer) && + !isBsonType(val, 'Decimal128') && + !(val instanceof Binary); } diff --git a/lib/helpers/cursor/eachAsync.js b/lib/helpers/cursor/eachAsync.js index 735fd2d8156..e3f6f8a24f4 100644 --- a/lib/helpers/cursor/eachAsync.js +++ b/lib/helpers/cursor/eachAsync.js @@ -33,7 +33,7 @@ module.exports = function eachAsync(next, fn, options, callback) { const aggregatedErrors = []; const enqueue = asyncQueue(); - let drained = false; + let aborted = false; return promiseOrCallback(callback, cb => { if (signal != null) { @@ -42,7 +42,7 @@ module.exports = function eachAsync(next, fn, options, callback) { } signal.addEventListener('abort', () => { - drained = true; + aborted = true; return cb(null); }, { once: true }); } @@ -63,90 +63,104 @@ module.exports = function eachAsync(next, fn, options, callback) { function iterate(finalCallback) { let handleResultsInProgress = 0; let currentDocumentIndex = 0; - let documentsBatch = []; let error = null; for (let i = 0; i < parallel; ++i) { - enqueue(fetch); + enqueue(createFetch()); } - function fetch(done) { - if (drained || error) { - return done(); - } + function createFetch() { + let documentsBatch = []; + let drained = false; + + return fetch; - next(function(err, doc) { - if (drained || error != null) { + function fetch(done) { + if (drained || aborted) { + return done(); + } else if (error) { return done(); } - if (err != null) { - if (continueOnError) { - aggregatedErrors.push(err); - } else { - error = err; - finalCallback(err); + + next(function(err, doc) { + if (error != null) { return done(); } - } - if (doc == null) { - drained = true; - if (handleResultsInProgress <= 0) { - const finalErr = continueOnError ? - createEachAsyncMultiError(aggregatedErrors) : - error; - - finalCallback(finalErr); - } else if (batchSize && documentsBatch.length) { - handleNextResult(documentsBatch, currentDocumentIndex++, handleNextResultCallBack); + if (err != null) { + if (err.name === 'MongoCursorExhaustedError') { + // We may end up calling `next()` multiple times on an exhausted + // cursor, which leads to an error. In case cursor is exhausted, + // just treat it as if the cursor returned no document, which is + // how a cursor indicates it is exhausted. + doc = null; + } else if (continueOnError) { + aggregatedErrors.push(err); + } else { + error = err; + finalCallback(err); + return done(); + } + } + if (doc == null) { + drained = true; + if (handleResultsInProgress <= 0) { + const finalErr = continueOnError ? + createEachAsyncMultiError(aggregatedErrors) : + error; + + finalCallback(finalErr); + } else if (batchSize && documentsBatch.length) { + handleNextResult(documentsBatch, currentDocumentIndex++, handleNextResultCallBack); + } + return done(); } - return done(); - } - ++handleResultsInProgress; + ++handleResultsInProgress; - // Kick off the subsequent `next()` before handling the result, but - // make sure we know that we still have a result to handle re: #8422 - immediate(() => done()); + // Kick off the subsequent `next()` before handling the result, but + // make sure we know that we still have a result to handle re: #8422 + immediate(() => done()); - if (batchSize) { - documentsBatch.push(doc); - } + if (batchSize) { + documentsBatch.push(doc); + } - // If the current documents size is less than the provided patch size don't process the documents yet - if (batchSize && documentsBatch.length !== batchSize) { - immediate(() => enqueue(fetch)); - return; - } + // If the current documents size is less than the provided batch size don't process the documents yet + if (batchSize && documentsBatch.length !== batchSize) { + immediate(() => enqueue(fetch)); + return; + } - const docsToProcess = batchSize ? documentsBatch : doc; + const docsToProcess = batchSize ? documentsBatch : doc; - function handleNextResultCallBack(err) { - if (batchSize) { - handleResultsInProgress -= documentsBatch.length; - documentsBatch = []; - } else { - --handleResultsInProgress; - } - if (err != null) { - if (continueOnError) { - aggregatedErrors.push(err); + function handleNextResultCallBack(err) { + if (batchSize) { + handleResultsInProgress -= documentsBatch.length; + documentsBatch = []; } else { - error = err; - return finalCallback(err); + --handleResultsInProgress; + } + if (err != null) { + if (continueOnError) { + aggregatedErrors.push(err); + } else { + error = err; + return finalCallback(err); + } + } + if ((drained || aborted) && handleResultsInProgress <= 0) { + const finalErr = continueOnError ? + createEachAsyncMultiError(aggregatedErrors) : + error; + return finalCallback(finalErr); } - } - if (drained && handleResultsInProgress <= 0) { - const finalErr = continueOnError ? - createEachAsyncMultiError(aggregatedErrors) : - error; - return finalCallback(finalErr); - } - immediate(() => enqueue(fetch)); - } + immediate(() => enqueue(fetch)); + } - handleNextResult(docsToProcess, currentDocumentIndex++, handleNextResultCallBack); - }); + handleNextResult(docsToProcess, currentDocumentIndex++, handleNextResultCallBack); + }); + } } } diff --git a/package.json b/package.json index 24ec752128d..c94d877feae 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "6.7.2", + "version": "6.7.4", "author": "Guillermo Rauch ", "keywords": [ "mongodb", diff --git a/test/errors.validation.test.js b/test/errors.validation.test.js index b881d4bf80a..d4081997372 100644 --- a/test/errors.validation.test.js +++ b/test/errors.validation.test.js @@ -234,6 +234,8 @@ describe('ValidationError', function() { describe('when user code defines a r/o Error#toJSON', function() { it('should not fail', function(done) { + this.timeout(10000); + const err = []; const child = require('child_process') .fork('./test/isolated/project-has-error.toJSON.js', ['--no-warnings'], { silent: true }); diff --git a/test/helpers/common.test.js b/test/helpers/common.test.js new file mode 100644 index 00000000000..93dd982ae6b --- /dev/null +++ b/test/helpers/common.test.js @@ -0,0 +1,17 @@ +'use strict'; + +const assert = require('assert'); +const modifiedPaths = require('../../lib/helpers/common').modifiedPaths; + +describe('modifiedPaths, bad update value which has circular reference field', () => { + it('update value can be null', function() { + modifiedPaths(null, 'path', null); + }); + + it('values with obvious error on circular reference', function() { + const objA = {}; + objA.a = objA; + + assert.throws(() => modifiedPaths(objA, 'path', null), /circular reference/); + }); +}); diff --git a/test/helpers/cursor.eachAsync.test.js b/test/helpers/cursor.eachAsync.test.js index b42cab85da0..57411ccfd5d 100644 --- a/test/helpers/cursor.eachAsync.test.js +++ b/test/helpers/cursor.eachAsync.test.js @@ -189,6 +189,30 @@ describe('eachAsync()', function() { assert.equal(numCalled, 1); }); + it('avoids mutating document batch with parallel (gh-12652)', async() => { + const max = 100; + let numCalled = 0; + function next(cb) { + setImmediate(() => { + if (++numCalled > max) { + return cb(null, null); + } + cb(null, { num: numCalled }); + }); + } + + let numDocsProcessed = 0; + async function fn(batch) { + numDocsProcessed += batch.length; + const length = batch.length; + await new Promise(resolve => setTimeout(resolve, 50)); + assert.equal(batch.length, length); + } + + await eachAsync(next, fn, { parallel: 7, batchSize: 10 }); + assert.equal(numDocsProcessed, max); + }); + it('using AbortSignal (gh-12173)', async function() { if (typeof AbortController === 'undefined') { return this.skip(); diff --git a/test/query.test.js b/test/query.test.js index b56a5e6c3de..a5309ebba40 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -4263,4 +4263,47 @@ describe('Query', function() { ); }, { message: 'Can\'t modify discriminator key "animals.kind" on discriminator model' }); }); + + it('global strictQuery should work if applied after schema creation (gh-12703)', async() => { + const m = new mongoose.Mongoose(); + + await m.connect(start.uri); + + const schema = new mongoose.Schema({ title: String }); + + const Test = m.model('test', schema); + + m.set('strictQuery', false); + + await Test.create({ + title: 'chimichanga' + }); + await Test.create({ + title: 'burrito bowl' + }); + await Test.create({ + title: 'taco supreme' + }); + + const cond = { + $or: [ + { + title: { + $regex: 'urri', + $options: 'i' + } + }, + { + name: { + $regex: 'urri', + $options: 'i' + } + } + ] + }; + + const found = await Test.find(cond); + assert.strictEqual(found.length, 1); + assert.strictEqual(found[0].title, 'burrito bowl'); + }); }); diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index 7bd00a8d4b8..a531247e183 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -938,3 +938,22 @@ function gh12590() { }); } + +function gh12611() { + const reusableFields = { + description: { type: String, required: true }, + skills: { type: [Schema.Types.ObjectId], ref: 'Skill', default: [] } + } as const; + + const firstSchema = new Schema({ + ...reusableFields, + anotherField: String + }); + + type Props = InferSchemaType; + expectType<{ + description: string; + skills: Types.ObjectId[]; + anotherField?: string; + }>({} as Props); +} diff --git a/test/types/virtuals.test.ts b/test/types/virtuals.test.ts index a5b07f54419..0e12483d79e 100644 --- a/test/types/virtuals.test.ts +++ b/test/types/virtuals.test.ts @@ -87,7 +87,7 @@ function gh11543() { expectType(personSchema.virtuals); } -function autoTypedVirtuals() { +async function autoTypedVirtuals() { type AutoTypedSchemaType = InferSchemaType; type VirtualsType = { domain: string }; type InferredDocType = FlatRecord>; @@ -119,4 +119,7 @@ function autoTypedVirtuals() { expectType(doc.domain); expectType>({} as InferredDocType); + + const doc2 = await TestModel.findOne().orFail(); + expectType(doc2.domain); } diff --git a/types/index.d.ts b/types/index.d.ts index 974366e0d26..4c843d940fa 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -78,7 +78,13 @@ declare module 'mongoose' { schema?: TSchema, collection?: string, options?: CompileModelOptions - ): Model, ObtainSchemaGeneric, ObtainSchemaGeneric, {}, TSchema> & ObtainSchemaGeneric; + ): Model< + InferSchemaType, + ObtainSchemaGeneric, + ObtainSchemaGeneric, + ObtainSchemaGeneric, + TSchema + > & ObtainSchemaGeneric; export function model(name: string, schema?: Schema | Schema, collection?: string, options?: CompileModelOptions): Model; diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts index e8c345fc707..c4dc62f7502 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -161,29 +161,30 @@ type PathEnumOrString['enum']> = T extends ( type ResolvePathType = {}, TypeKey extends TypeKeyBaseType = DefaultTypeKey> = PathValueType extends Schema ? InferSchemaType : PathValueType extends (infer Item)[] ? IfEquals> : ObtainDocumentPathType[]> : - PathValueType extends StringSchemaDefinition ? PathEnumOrString : - IfEquals extends true ? PathEnumOrString : - IfEquals extends true ? PathEnumOrString : - PathValueType extends NumberSchemaDefinition ? Options['enum'] extends ReadonlyArray ? Options['enum'][number] : number : - IfEquals extends true ? number : - PathValueType extends DateSchemaDefinition ? Date : - IfEquals extends true ? Date : - PathValueType extends typeof Buffer | 'buffer' | 'Buffer' | typeof Schema.Types.Buffer ? Buffer : - PathValueType extends BooleanSchemaDefinition ? boolean : - IfEquals extends true ? boolean : - PathValueType extends ObjectIdSchemaDefinition ? Types.ObjectId : - IfEquals extends true ? Types.ObjectId : - IfEquals extends true ? Types.ObjectId : - PathValueType extends 'decimal128' | 'Decimal128' | typeof Schema.Types.Decimal128 ? Types.Decimal128 : - IfEquals extends true ? Types.Decimal128 : - IfEquals extends true ? Types.Decimal128 : - PathValueType extends 'uuid' | 'UUID' | typeof Schema.Types.UUID ? Buffer : - IfEquals extends true ? Buffer : - PathValueType extends MapConstructor ? Map> : - PathValueType extends ArrayConstructor ? any[] : - PathValueType extends typeof Schema.Types.Mixed ? any: - IfEquals extends true ? any: - IfEquals extends true ? any: - PathValueType extends typeof SchemaType ? PathValueType['prototype'] : - PathValueType extends Record ? ObtainDocumentType : - unknown; + PathValueType extends ReadonlyArray ? IfEquals> : ObtainDocumentPathType[]> : + PathValueType extends StringSchemaDefinition ? PathEnumOrString : + IfEquals extends true ? PathEnumOrString : + IfEquals extends true ? PathEnumOrString : + PathValueType extends NumberSchemaDefinition ? Options['enum'] extends ReadonlyArray ? Options['enum'][number] : number : + IfEquals extends true ? number : + PathValueType extends DateSchemaDefinition ? Date : + IfEquals extends true ? Date : + PathValueType extends typeof Buffer | 'buffer' | 'Buffer' | typeof Schema.Types.Buffer ? Buffer : + PathValueType extends BooleanSchemaDefinition ? boolean : + IfEquals extends true ? boolean : + PathValueType extends ObjectIdSchemaDefinition ? Types.ObjectId : + IfEquals extends true ? Types.ObjectId : + IfEquals extends true ? Types.ObjectId : + PathValueType extends 'decimal128' | 'Decimal128' | typeof Schema.Types.Decimal128 ? Types.Decimal128 : + IfEquals extends true ? Types.Decimal128 : + IfEquals extends true ? Types.Decimal128 : + PathValueType extends 'uuid' | 'UUID' | typeof Schema.Types.UUID ? Buffer : + IfEquals extends true ? Buffer : + PathValueType extends MapConstructor ? Map> : + PathValueType extends ArrayConstructor ? any[] : + PathValueType extends typeof Schema.Types.Mixed ? any: + IfEquals extends true ? any: + IfEquals extends true ? any: + PathValueType extends typeof SchemaType ? PathValueType['prototype'] : + PathValueType extends Record ? ObtainDocumentType : + unknown; diff --git a/types/pipelinestage.d.ts b/types/pipelinestage.d.ts index f58e4b61f27..37d228ff1b7 100644 --- a/types/pipelinestage.d.ts +++ b/types/pipelinestage.d.ts @@ -213,7 +213,7 @@ declare module 'mongoose' { export interface ReplaceWith { /** [`$replaceWith` reference](https://docs.mongodb.com/manual/reference/operator/aggregation/replaceWith/) */ - $replaceWith: ObjectExpressionOperator | { [field: string]: Expression }; + $replaceWith: ObjectExpressionOperator | { [field: string]: Expression } | `$${string}`; } export interface Sample { diff --git a/types/query.d.ts b/types/query.d.ts index 3a3a7a6b36f..5dec26efd7a 100644 --- a/types/query.d.ts +++ b/types/query.d.ts @@ -616,7 +616,7 @@ declare module 'mongoose' { then: Promise['then']; /** Converts this query to a customized, reusable query constructor with all arguments and options retained. */ - toConstructor(): typeof Query; + toConstructor(): RetType; /** Declare and/or execute this query as an update() operation. */ update(filter?: FilterQuery, update?: UpdateQuery | UpdateWithAggregationPipeline, options?: QueryOptions | null, callback?: Callback): QueryWithHelpers;