Skip to content

Commit b502e8c

Browse files
committed
Merge branch 'master' into 8.11
2 parents c41f32c + f1c6ad2 commit b502e8c

File tree

10 files changed

+181
-75
lines changed

10 files changed

+181
-75
lines changed

lib/connection.js

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -818,32 +818,41 @@ Connection.prototype.dropCollection = async function dropCollection(collection)
818818
/**
819819
* Waits for connection to be established, so the connection has a `client`
820820
*
821+
* @param {Boolean} [noTimeout=false] if set, don't put a timeout on the operation. Used internally so `mongoose.model()` doesn't leave open handles.
821822
* @return Promise
822823
* @api private
823824
*/
824825

825-
Connection.prototype._waitForConnect = async function _waitForConnect() {
826+
Connection.prototype._waitForConnect = async function _waitForConnect(noTimeout) {
826827
if ((this.readyState === STATES.connecting || this.readyState === STATES.disconnected) && this._shouldBufferCommands()) {
827828
const bufferTimeoutMS = this._getBufferTimeoutMS();
828829
let timeout = null;
829830
let timedOut = false;
830831
// The element that this function pushes onto `_queue`, stored to make it easy to remove later
831832
const queueElement = {};
832-
await Promise.race([
833-
new Promise(resolve => {
834-
queueElement.fn = resolve;
835-
this._queue.push(queueElement);
836-
}),
837-
new Promise(resolve => {
838-
timeout = setTimeout(
839-
() => {
840-
timedOut = true;
841-
resolve();
842-
},
843-
bufferTimeoutMS
844-
);
845-
})
846-
]);
833+
834+
// Mongoose executes all elements in `_queue` when initial connection succeeds in `onOpen()`.
835+
const waitForConnectPromise = new Promise(resolve => {
836+
queueElement.fn = resolve;
837+
this._queue.push(queueElement);
838+
});
839+
840+
if (noTimeout) {
841+
await waitForConnectPromise;
842+
} else {
843+
await Promise.race([
844+
waitForConnectPromise,
845+
new Promise(resolve => {
846+
timeout = setTimeout(
847+
() => {
848+
timedOut = true;
849+
resolve();
850+
},
851+
bufferTimeoutMS
852+
);
853+
})
854+
]);
855+
}
847856

848857
if (timedOut) {
849858
const index = this._queue.indexOf(queueElement);

lib/document.js

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -741,15 +741,10 @@ function init(self, obj, doc, opts, prefix) {
741741
let schemaType;
742742
let path;
743743
let i;
744-
let index = 0;
745744
const strict = self.$__.strictMode;
746745
const docSchema = self.$__schema;
747746

748-
while (index < len) {
749-
_init(index++);
750-
}
751-
752-
function _init(index) {
747+
for (let index = 0; index < len; ++index) {
753748
i = keys[index];
754749
// avoid prototype pollution
755750
if (i === '__proto__' || i === 'constructor') {

lib/model.js

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1103,16 +1103,28 @@ Model.init = function init() {
11031103
return results;
11041104
};
11051105
const _createCollection = async() => {
1106-
await conn._waitForConnect();
1107-
const autoCreate = utils.getOption(
1106+
let autoCreate = utils.getOption(
11081107
'autoCreate',
11091108
this.schema.options,
1110-
conn.config,
1111-
conn.base.options
1109+
conn.config
1110+
// No base.options here because we don't want to take the base value if the connection hasn't
1111+
// set it yet
11121112
);
1113+
if (autoCreate == null) {
1114+
// `autoCreate` may later be set when the connection is opened, so wait for connect before checking
1115+
await conn._waitForConnect(true);
1116+
autoCreate = utils.getOption(
1117+
'autoCreate',
1118+
this.schema.options,
1119+
conn.config,
1120+
conn.base.options
1121+
);
1122+
}
1123+
11131124
if (!autoCreate) {
11141125
return;
11151126
}
1127+
11161128
return await this.createCollection();
11171129
};
11181130

lib/query.js

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,25 @@ const queryOptionMethods = new Set([
6565
'wtimeout'
6666
]);
6767

68+
// Map from operation name to the name of the function that executes the actual operation against MongoDB.
69+
// Called a thunk for legacy reasons, "thunk" means function that takes exactly 1 param, a callback.
70+
// Currently `_countDocuments()`, etc. are async functions that take no params.
71+
const opToThunk = new Map([
72+
['countDocuments', '_countDocuments'],
73+
['distinct', '__distinct'],
74+
['estimatedDocumentCount', '_estimatedDocumentCount'],
75+
['find', '_find'],
76+
['findOne', '_findOne'],
77+
['findOneAndReplace', '_findOneAndReplace'],
78+
['findOneAndUpdate', '_findOneAndUpdate'],
79+
['replaceOne', '_replaceOne'],
80+
['updateMany', '_updateMany'],
81+
['updateOne', '_updateOne'],
82+
['deleteMany', '_deleteMany'],
83+
['deleteOne', '_deleteOne'],
84+
['findOneAndDelete', '_findOneAndDelete']
85+
]);
86+
6887
/**
6988
* Query constructor used for building queries. You do not need
7089
* to instantiate a `Query` directly. Instead use Model functions like
@@ -2337,18 +2356,17 @@ Query.prototype._find = async function _find() {
23372356
}
23382357

23392358
const mongooseOptions = this._mongooseOptions;
2340-
const _this = this;
2341-
const userProvidedFields = _this._userProvidedFields || {};
2359+
const userProvidedFields = this._userProvidedFields || {};
23422360

23432361
applyGlobalMaxTimeMS(this.options, this.model.db.options, this.model.base.options);
23442362
applyGlobalDiskUse(this.options, this.model.db.options, this.model.base.options);
23452363

23462364
// Separate options to pass down to `completeMany()` in case we need to
23472365
// set a session on the document
2348-
const completeManyOptions = Object.assign({}, {
2366+
const completeManyOptions = {
23492367
session: this && this.options && this.options.session || null,
23502368
lean: mongooseOptions.lean || null
2351-
});
2369+
};
23522370

23532371
const options = this._optionsForExec();
23542372

@@ -2366,7 +2384,7 @@ Query.prototype._find = async function _find() {
23662384
}
23672385

23682386
if (!mongooseOptions.populate) {
2369-
const versionKey = _this.schema.options.versionKey;
2387+
const versionKey = this.schema.options.versionKey;
23702388
if (mongooseOptions.lean && mongooseOptions.lean.versionKey === false && versionKey) {
23712389
docs.forEach((doc) => {
23722390
if (versionKey in doc) {
@@ -2375,17 +2393,17 @@ Query.prototype._find = async function _find() {
23752393
});
23762394
}
23772395
return mongooseOptions.lean ?
2378-
_completeManyLean(_this.model.schema, docs, null, completeManyOptions) :
2379-
_this._completeMany(docs, fields, userProvidedFields, completeManyOptions);
2396+
_completeManyLean(this.model.schema, docs, null, completeManyOptions) :
2397+
this._completeMany(docs, fields, userProvidedFields, completeManyOptions);
23802398
}
23812399

2382-
const pop = helpers.preparePopulationOptionsMQ(_this, mongooseOptions);
2400+
const pop = helpers.preparePopulationOptionsMQ(this, mongooseOptions);
23832401

23842402
if (mongooseOptions.lean) {
2385-
return _this.model.populate(docs, pop);
2403+
return this.model.populate(docs, pop);
23862404
}
23872405

2388-
docs = await _this._completeMany(docs, fields, userProvidedFields, completeManyOptions);
2406+
docs = await this._completeMany(docs, fields, userProvidedFields, completeManyOptions);
23892407
await this.model.populate(docs, pop);
23902408

23912409
return docs;
@@ -4397,22 +4415,14 @@ Query.prototype.exec = async function exec(op) {
43974415
if (this.model == null) {
43984416
throw new MongooseError('Query must have an associated model before executing');
43994417
}
4400-
this._validateOp();
4401-
4402-
if (!this.op) {
4403-
return;
4404-
}
44054418

4406-
if (this.options && this.options.sort) {
4407-
const keys = Object.keys(this.options.sort);
4408-
if (keys.includes('')) {
4409-
throw new Error('Invalid field "" passed to sort()');
4410-
}
4419+
const thunk = opToThunk.get(this.op);
4420+
if (!thunk) {
4421+
throw new MongooseError('Query has invalid `op`: "' + this.op + '"');
44114422
}
44124423

4413-
let thunk = '_' + this.op;
4414-
if (this.op === 'distinct') {
4415-
thunk = '__distinct';
4424+
if (this.options && this.options.sort && typeof this.options.sort === 'object' && this.options.sort.hasOwnProperty('')) {
4425+
throw new Error('Invalid field "" passed to sort()');
44164426
}
44174427

44184428
if (this._executionStack != null) {

test/connection.test.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1637,6 +1637,19 @@ describe('connections:', function() {
16371637
assert.ok(!res.map(c => c.name).includes('gh12940_Conn'));
16381638
});
16391639

1640+
it('does not wait for buffering if autoCreate: false (gh-15241)', async function() {
1641+
const m = new mongoose.Mongoose();
1642+
m.set('bufferTimeoutMS', 100);
1643+
1644+
const schema = new Schema({ name: String }, {
1645+
autoCreate: false
1646+
});
1647+
const Model = m.model('gh15241_Conn', schema);
1648+
1649+
// Without gh-15241 changes, this would buffer and fail even though `autoCreate: false`
1650+
await Model.init();
1651+
});
1652+
16401653
it('should not create default connection with createInitialConnection = false (gh-12965)', function() {
16411654
const m = new mongoose.Mongoose({
16421655
createInitialConnection: false

test/types/middleware.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,60 @@ function gh13601() {
200200
expectAssignable<Document>(this);
201201
});
202202
}
203+
204+
function gh15242() {
205+
type PostPersisted = {
206+
title: string,
207+
postTime: Date
208+
};
209+
210+
type ValidatorThis = DocumentValidatorThis | QueryValidatorThis;
211+
type DocumentValidatorThis = HydratedDocument<PostPersisted>;
212+
type QueryValidatorThis = Query<PostRecord, PostRecord>;
213+
214+
const PostSchema = new Schema<PostPersisted>({
215+
title: { type: String, required: true },
216+
postTime: {
217+
type: Date,
218+
required: true,
219+
validate: {
220+
validator: async function(this: ValidatorThis, postTime: Date): Promise<boolean> {
221+
return true;
222+
}
223+
}
224+
}
225+
});
226+
227+
type PostRecord = HydratedDocument<PostPersisted>;
228+
const PostModel = model<PostPersisted>('Post', PostSchema);
229+
}
230+
231+
function gh15242WithVirtuals() {
232+
type PostPersisted = {
233+
title: string,
234+
postTime: Date
235+
};
236+
237+
type ValidatorThis = DocumentValidatorThis | QueryValidatorThis;
238+
type DocumentValidatorThis = HydratedDocument<PostPersisted, { myVirtual: number }>;
239+
type QueryValidatorThis = Query<PostRecord, PostRecord>;
240+
241+
const PostSchema = new Schema({
242+
title: { type: String, required: true },
243+
postTime: {
244+
type: Date,
245+
required: true,
246+
validate: {
247+
validator: async function(this: ValidatorThis, postTime: Date): Promise<boolean> {
248+
if (!(this instanceof Query)) {
249+
expectType<number>(this.myVirtual);
250+
}
251+
return true;
252+
}
253+
}
254+
}
255+
}, { virtuals: { myVirtual: { get() { return 42; } } } });
256+
257+
type PostRecord = HydratedDocument<PostPersisted, { myVirtual: number }>;
258+
const PostModel = model<PostPersisted>('Post', PostSchema);
259+
}

test/types/schema.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1733,3 +1733,8 @@ async function gh15236() {
17331733

17341734
schema.path<Schema.Types.Number>('myNum').min(0);
17351735
}
1736+
1737+
function gh15244() {
1738+
const schema = new Schema({});
1739+
schema.discriminator('Name', new Schema({}), { value: 'value' });
1740+
}

types/index.d.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -273,10 +273,10 @@ declare module 'mongoose' {
273273
/**
274274
* Create a new schema
275275
*/
276-
constructor(definition?: SchemaDefinition<SchemaDefinitionType<RawDocType>, RawDocType> | DocType, options?: SchemaOptions<FlatRecord<DocType>, TInstanceMethods, TQueryHelpers, TStaticMethods, TVirtuals, THydratedDocumentType> | ResolveSchemaOptions<TSchemaOptions>);
276+
constructor(definition?: SchemaDefinition<SchemaDefinitionType<RawDocType>, RawDocType, THydratedDocumentType> | DocType, options?: SchemaOptions<FlatRecord<DocType>, TInstanceMethods, TQueryHelpers, TStaticMethods, TVirtuals, THydratedDocumentType> | ResolveSchemaOptions<TSchemaOptions>);
277277

278278
/** Adds key path / schema type pairs to this schema. */
279-
add(obj: SchemaDefinition<SchemaDefinitionType<RawDocType>> | Schema, prefix?: string): this;
279+
add(obj: SchemaDefinition<SchemaDefinitionType<RawDocType>, RawDocType> | Schema, prefix?: string): this;
280280

281281
/**
282282
* Add an alias for `path`. This means getting or setting the `alias`
@@ -297,7 +297,7 @@ declare module 'mongoose' {
297297
/** Returns a copy of this schema */
298298
clone<T = this>(): T;
299299

300-
discriminator<DisSchema = Schema>(name: string | number, schema: DisSchema): this;
300+
discriminator<DisSchema = Schema>(name: string | number, schema: DisSchema, options?: DiscriminatorOptions): this;
301301

302302
/** Returns a new schema that has the picked `paths` from this schema. */
303303
pick<T = this>(paths: string[], options?: SchemaOptions): T;
@@ -541,21 +541,21 @@ declare module 'mongoose' {
541541
? DateSchemaDefinition
542542
: (Function | string);
543543

544-
export type SchemaDefinitionProperty<T = undefined, EnforcedDocType = any> = SchemaDefinitionWithBuiltInClass<T> |
545-
SchemaTypeOptions<T extends undefined ? any : T, EnforcedDocType> |
546-
typeof SchemaType |
547-
Schema<any, any, any> |
548-
Schema<any, any, any>[] |
549-
SchemaTypeOptions<T extends undefined ? any : Unpacked<T>, EnforcedDocType>[] |
550-
Function[] |
551-
SchemaDefinition<T, EnforcedDocType> |
552-
SchemaDefinition<Unpacked<T>, EnforcedDocType>[] |
553-
typeof Schema.Types.Mixed |
554-
MixedSchemaTypeOptions<EnforcedDocType>;
555-
556-
export type SchemaDefinition<T = undefined, EnforcedDocType = any> = T extends undefined
544+
export type SchemaDefinitionProperty<T = undefined, EnforcedDocType = any, THydratedDocumentType = HydratedDocument<EnforcedDocType>> = SchemaDefinitionWithBuiltInClass<T>
545+
| SchemaTypeOptions<T extends undefined ? any : T, EnforcedDocType, THydratedDocumentType>
546+
| typeof SchemaType
547+
| Schema<any, any, any>
548+
| Schema<any, any, any>[]
549+
| SchemaTypeOptions<T extends undefined ? any : Unpacked<T>, EnforcedDocType, THydratedDocumentType>[]
550+
| Function[]
551+
| SchemaDefinition<T, EnforcedDocType, THydratedDocumentType>
552+
| SchemaDefinition<Unpacked<T>, EnforcedDocType, THydratedDocumentType>[]
553+
| typeof Schema.Types.Mixed
554+
| MixedSchemaTypeOptions<EnforcedDocType>;
555+
556+
export type SchemaDefinition<T = undefined, EnforcedDocType = any, THydratedDocumentType = HydratedDocument<EnforcedDocType>> = T extends undefined
557557
? { [path: string]: SchemaDefinitionProperty; }
558-
: { [path in keyof T]?: SchemaDefinitionProperty<T[path], EnforcedDocType>; };
558+
: { [path in keyof T]?: SchemaDefinitionProperty<T[path], EnforcedDocType, THydratedDocumentType>; };
559559

560560
export type AnyArray<T> = T[] | ReadonlyArray<T>;
561561
export type ExtractMongooseArray<T> = T extends Types.Array<any> ? AnyArray<Unpacked<T>> : T;

types/schematypes.d.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ declare module 'mongoose' {
5656

5757
type DefaultType<T> = T extends Schema.Types.Mixed ? any : Partial<ExtractMongooseArray<T>>;
5858

59-
class SchemaTypeOptions<T, EnforcedDocType = any> {
59+
class SchemaTypeOptions<T, EnforcedDocType = any, THydratedDocumentType = HydratedDocument<EnforcedDocType>> {
6060
type?:
6161
T extends string ? StringSchemaDefinition :
6262
T extends number ? NumberSchemaDefinition :
@@ -65,19 +65,19 @@ declare module 'mongoose' {
6565
T extends Map<any, any> ? SchemaDefinition<typeof Map> :
6666
T extends Buffer ? SchemaDefinition<typeof Buffer> :
6767
T extends Types.ObjectId ? ObjectIdSchemaDefinition :
68-
T extends Types.ObjectId[] ? AnyArray<ObjectIdSchemaDefinition> | AnyArray<SchemaTypeOptions<ObjectId, EnforcedDocType>> :
69-
T extends object[] ? (AnyArray<Schema<any, any, any>> | AnyArray<SchemaDefinition<Unpacked<T>>> | AnyArray<SchemaTypeOptions<Unpacked<T>, EnforcedDocType>>) :
70-
T extends string[] ? AnyArray<StringSchemaDefinition> | AnyArray<SchemaTypeOptions<string, EnforcedDocType>> :
71-
T extends number[] ? AnyArray<NumberSchemaDefinition> | AnyArray<SchemaTypeOptions<number, EnforcedDocType>> :
72-
T extends boolean[] ? AnyArray<BooleanSchemaDefinition> | AnyArray<SchemaTypeOptions<boolean, EnforcedDocType>> :
73-
T extends Function[] ? AnyArray<Function | string> | AnyArray<SchemaTypeOptions<Unpacked<T>, EnforcedDocType>> :
68+
T extends Types.ObjectId[] ? AnyArray<ObjectIdSchemaDefinition> | AnyArray<SchemaTypeOptions<ObjectId, EnforcedDocType, THydratedDocumentType>> :
69+
T extends object[] ? (AnyArray<Schema<any, any, any>> | AnyArray<SchemaDefinition<Unpacked<T>>> | AnyArray<SchemaTypeOptions<Unpacked<T>, EnforcedDocType, THydratedDocumentType>>) :
70+
T extends string[] ? AnyArray<StringSchemaDefinition> | AnyArray<SchemaTypeOptions<string, EnforcedDocType, THydratedDocumentType>> :
71+
T extends number[] ? AnyArray<NumberSchemaDefinition> | AnyArray<SchemaTypeOptions<number, EnforcedDocType, THydratedDocumentType>> :
72+
T extends boolean[] ? AnyArray<BooleanSchemaDefinition> | AnyArray<SchemaTypeOptions<boolean, EnforcedDocType, THydratedDocumentType>> :
73+
T extends Function[] ? AnyArray<Function | string> | AnyArray<SchemaTypeOptions<Unpacked<T>, EnforcedDocType, THydratedDocumentType>> :
7474
T | typeof SchemaType | Schema<any, any, any> | SchemaDefinition<T> | Function | AnyArray<Function>;
7575

7676
/** Defines a virtual with the given name that gets/sets this path. */
7777
alias?: string | string[];
7878

7979
/** Function or object describing how to validate this schematype. See [validation docs](https://mongoosejs.com/docs/validation.html). */
80-
validate?: SchemaValidator<T, EnforcedDocType> | AnyArray<SchemaValidator<T, EnforcedDocType>>;
80+
validate?: SchemaValidator<T, EnforcedDocType, THydratedDocumentType> | AnyArray<SchemaValidator<T, EnforcedDocType, THydratedDocumentType>>;
8181

8282
/** Allows overriding casting logic for this individual path. If a string, the given string overwrites Mongoose's default cast error message. */
8383
cast?: string |

0 commit comments

Comments
 (0)