mongodb 6.1.0 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/db.ts CHANGED
@@ -6,7 +6,7 @@ import * as CONSTANTS from './constants';
6
6
  import { AggregationCursor } from './cursor/aggregation_cursor';
7
7
  import { ListCollectionsCursor } from './cursor/list_collections_cursor';
8
8
  import { RunCommandCursor, type RunCursorCommandOptions } from './cursor/run_command_cursor';
9
- import { MongoAPIError, MongoInvalidArgumentError } from './error';
9
+ import { MongoInvalidArgumentError } from './error';
10
10
  import type { MongoClient, PkFactory } from './mongo_client';
11
11
  import type { TODO_NODE_3286 } from './mongo_types';
12
12
  import type { AggregateOptions } from './operations/aggregate';
@@ -134,11 +134,13 @@ export class Db {
134
134
  public static SYSTEM_JS_COLLECTION = CONSTANTS.SYSTEM_JS_COLLECTION;
135
135
 
136
136
  /**
137
- * Creates a new Db instance
137
+ * Creates a new Db instance.
138
+ *
139
+ * Db name cannot contain a dot, the server may apply more restrictions when an operation is run.
138
140
  *
139
141
  * @param client - The MongoClient for the database.
140
142
  * @param databaseName - The name of the database this instance represents.
141
- * @param options - Optional settings for Db construction
143
+ * @param options - Optional settings for Db construction.
142
144
  */
143
145
  constructor(client: MongoClient, databaseName: string, options?: DbOptions) {
144
146
  options = options ?? {};
@@ -146,8 +148,10 @@ export class Db {
146
148
  // Filter the options
147
149
  options = filterOptions(options, DB_OPTIONS_ALLOW_LIST);
148
150
 
149
- // Ensure we have a valid db name
150
- validateDatabaseName(databaseName);
151
+ // Ensure there are no dots in database name
152
+ if (typeof databaseName === 'string' && databaseName.includes('.')) {
153
+ throw new MongoInvalidArgumentError(`Database names cannot contain the character '.'`);
154
+ }
151
155
 
152
156
  // Internal state of the db object
153
157
  this.s = {
@@ -218,6 +222,8 @@ export class Db {
218
222
  * Create a new collection on a server with the specified options. Use this to create capped collections.
219
223
  * More information about command options available at https://www.mongodb.com/docs/manual/reference/command/create/
220
224
  *
225
+ * Collection namespace validation is performed server-side.
226
+ *
221
227
  * @param name - The name of the collection to create
222
228
  * @param options - Optional settings for the command
223
229
  */
@@ -294,6 +300,8 @@ export class Db {
294
300
  /**
295
301
  * Returns a reference to a MongoDB Collection. If it does not exist it will be created implicitly.
296
302
  *
303
+ * Collection namespace validation is performed server-side.
304
+ *
297
305
  * @param name - the collection name we wish to access.
298
306
  * @returns return the new Collection instance
299
307
  */
@@ -519,19 +527,3 @@ export class Db {
519
527
  return new RunCommandCursor(this, command, options);
520
528
  }
521
529
  }
522
-
523
- // TODO(NODE-3484): Refactor into MongoDBNamespace
524
- // Validate the database name
525
- function validateDatabaseName(databaseName: string) {
526
- if (typeof databaseName !== 'string')
527
- throw new MongoInvalidArgumentError('Database name must be a string');
528
- if (databaseName.length === 0)
529
- throw new MongoInvalidArgumentError('Database name cannot be the empty string');
530
- if (databaseName === '$external') return;
531
-
532
- const invalidChars = [' ', '.', '$', '/', '\\'];
533
- for (let i = 0; i < invalidChars.length; i++) {
534
- if (databaseName.indexOf(invalidChars[i]) !== -1)
535
- throw new MongoAPIError(`database names cannot contain the character '${invalidChars[i]}'`);
536
- }
537
- }
package/src/index.ts CHANGED
@@ -524,6 +524,7 @@ export type {
524
524
  HostAddress,
525
525
  List,
526
526
  MongoDBCollectionNamespace,
527
- MongoDBNamespace
527
+ MongoDBNamespace,
528
+ TimeoutController
528
529
  } from './utils';
529
530
  export type { W, WriteConcernOptions, WriteConcernSettings } from './write_concern';
@@ -2,7 +2,7 @@ import type { Document } from '../bson';
2
2
  import { Collection } from '../collection';
3
3
  import type { Server } from '../sdam/server';
4
4
  import type { ClientSession } from '../sessions';
5
- import { checkCollectionName, MongoDBNamespace } from '../utils';
5
+ import { MongoDBNamespace } from '../utils';
6
6
  import { CommandOperation, type CommandOperationOptions } from './command';
7
7
  import { Aspect, defineAspects } from './operation';
8
8
 
@@ -21,7 +21,6 @@ export class RenameOperation extends CommandOperation<Document> {
21
21
  public newName: string,
22
22
  public override options: RenameOptions
23
23
  ) {
24
- checkCollectionName(newName);
25
24
  super(collection, options);
26
25
  this.ns = new MongoDBNamespace('admin', '$cmd');
27
26
  }
@@ -132,10 +132,13 @@ export class TopologyClosedEvent {
132
132
  export class ServerHeartbeatStartedEvent {
133
133
  /** The connection id for the command */
134
134
  connectionId: string;
135
+ /** Is true when using the streaming protocol. */
136
+ awaited: boolean;
135
137
 
136
138
  /** @internal */
137
- constructor(connectionId: string) {
139
+ constructor(connectionId: string, awaited: boolean) {
138
140
  this.connectionId = connectionId;
141
+ this.awaited = awaited;
139
142
  }
140
143
  }
141
144
 
@@ -151,12 +154,15 @@ export class ServerHeartbeatSucceededEvent {
151
154
  duration: number;
152
155
  /** The command reply */
153
156
  reply: Document;
157
+ /** Is true when using the streaming protocol. */
158
+ awaited: boolean;
154
159
 
155
160
  /** @internal */
156
- constructor(connectionId: string, duration: number, reply: Document | null) {
161
+ constructor(connectionId: string, duration: number, reply: Document | null, awaited: boolean) {
157
162
  this.connectionId = connectionId;
158
163
  this.duration = duration;
159
164
  this.reply = reply ?? {};
165
+ this.awaited = awaited;
160
166
  }
161
167
  }
162
168
 
@@ -172,11 +178,14 @@ export class ServerHeartbeatFailedEvent {
172
178
  duration: number;
173
179
  /** The command failure */
174
180
  failure: Error;
181
+ /** Is true when using the streaming protocol. */
182
+ awaited: boolean;
175
183
 
176
184
  /** @internal */
177
- constructor(connectionId: string, duration: number, failure: Error) {
185
+ constructor(connectionId: string, duration: number, failure: Error, awaited: boolean) {
178
186
  this.connectionId = connectionId;
179
187
  this.duration = duration;
180
188
  this.failure = failure;
189
+ this.awaited = awaited;
181
190
  }
182
191
  }
@@ -209,7 +209,12 @@ function resetMonitorState(monitor: Monitor) {
209
209
 
210
210
  function checkServer(monitor: Monitor, callback: Callback<Document | null>) {
211
211
  let start = now();
212
- monitor.emit(Server.SERVER_HEARTBEAT_STARTED, new ServerHeartbeatStartedEvent(monitor.address));
212
+ const topologyVersion = monitor[kServer].description.topologyVersion;
213
+ const isAwaitable = topologyVersion != null;
214
+ monitor.emit(
215
+ Server.SERVER_HEARTBEAT_STARTED,
216
+ new ServerHeartbeatStartedEvent(monitor.address, isAwaitable)
217
+ );
213
218
 
214
219
  function failureHandler(err: Error) {
215
220
  monitor[kConnection]?.destroy({ force: true });
@@ -217,7 +222,12 @@ function checkServer(monitor: Monitor, callback: Callback<Document | null>) {
217
222
 
218
223
  monitor.emit(
219
224
  Server.SERVER_HEARTBEAT_FAILED,
220
- new ServerHeartbeatFailedEvent(monitor.address, calculateDurationInMs(start), err)
225
+ new ServerHeartbeatFailedEvent(
226
+ monitor.address,
227
+ calculateDurationInMs(start),
228
+ err,
229
+ isAwaitable
230
+ )
221
231
  );
222
232
 
223
233
  const error = !(err instanceof MongoError)
@@ -237,8 +247,6 @@ function checkServer(monitor: Monitor, callback: Callback<Document | null>) {
237
247
  const { serverApi, helloOk } = connection;
238
248
  const connectTimeoutMS = monitor.options.connectTimeoutMS;
239
249
  const maxAwaitTimeMS = monitor.options.heartbeatFrequencyMS;
240
- const topologyVersion = monitor[kServer].description.topologyVersion;
241
- const isAwaitable = topologyVersion != null;
242
250
 
243
251
  const cmd = {
244
252
  [serverApi?.version || helloOk ? 'hello' : LEGACY_HELLO_COMMAND]: 1,
@@ -278,17 +286,18 @@ function checkServer(monitor: Monitor, callback: Callback<Document | null>) {
278
286
  const duration =
279
287
  isAwaitable && rttPinger ? rttPinger.roundTripTime : calculateDurationInMs(start);
280
288
 
289
+ const awaited = isAwaitable && hello.topologyVersion != null;
281
290
  monitor.emit(
282
291
  Server.SERVER_HEARTBEAT_SUCCEEDED,
283
- new ServerHeartbeatSucceededEvent(monitor.address, duration, hello)
292
+ new ServerHeartbeatSucceededEvent(monitor.address, duration, hello, awaited)
284
293
  );
285
294
 
286
295
  // if we are using the streaming protocol then we immediately issue another `started`
287
296
  // event, otherwise the "check" is complete and return to the main monitor loop
288
- if (isAwaitable && hello.topologyVersion) {
297
+ if (awaited) {
289
298
  monitor.emit(
290
299
  Server.SERVER_HEARTBEAT_STARTED,
291
- new ServerHeartbeatStartedEvent(monitor.address)
300
+ new ServerHeartbeatStartedEvent(monitor.address, true)
292
301
  );
293
302
  start = now();
294
303
  } else {
@@ -324,7 +333,12 @@ function checkServer(monitor: Monitor, callback: Callback<Document | null>) {
324
333
  monitor[kConnection] = conn;
325
334
  monitor.emit(
326
335
  Server.SERVER_HEARTBEAT_SUCCEEDED,
327
- new ServerHeartbeatSucceededEvent(monitor.address, calculateDurationInMs(start), conn.hello)
336
+ new ServerHeartbeatSucceededEvent(
337
+ monitor.address,
338
+ calculateDurationInMs(start),
339
+ conn.hello,
340
+ false
341
+ )
328
342
  );
329
343
 
330
344
  callback(undefined, conn.hello);
@@ -1,4 +1,3 @@
1
- import { clearTimeout, setTimeout } from 'timers';
2
1
  import { promisify } from 'util';
3
2
 
4
3
  import type { BSONSerializeOptions, Document } from '../bson';
@@ -43,7 +42,8 @@ import {
43
42
  List,
44
43
  makeStateMachine,
45
44
  ns,
46
- shuffle
45
+ shuffle,
46
+ TimeoutController
47
47
  } from '../utils';
48
48
  import {
49
49
  _advanceClusterTime,
@@ -94,8 +94,8 @@ export interface ServerSelectionRequest {
94
94
  serverSelector: ServerSelector;
95
95
  transaction?: Transaction;
96
96
  callback: ServerSelectionCallback;
97
- timer?: NodeJS.Timeout;
98
97
  [kCancelled]?: boolean;
98
+ timeoutController: TimeoutController;
99
99
  }
100
100
 
101
101
  /** @internal */
@@ -556,22 +556,20 @@ export class Topology extends TypedEventEmitter<TopologyEvents> {
556
556
  const waitQueueMember: ServerSelectionRequest = {
557
557
  serverSelector,
558
558
  transaction,
559
- callback
559
+ callback,
560
+ timeoutController: new TimeoutController(options.serverSelectionTimeoutMS)
560
561
  };
561
562
 
562
- const serverSelectionTimeoutMS = options.serverSelectionTimeoutMS;
563
- if (serverSelectionTimeoutMS) {
564
- waitQueueMember.timer = setTimeout(() => {
565
- waitQueueMember[kCancelled] = true;
566
- waitQueueMember.timer = undefined;
567
- const timeoutError = new MongoServerSelectionError(
568
- `Server selection timed out after ${serverSelectionTimeoutMS} ms`,
569
- this.description
570
- );
563
+ waitQueueMember.timeoutController.signal.addEventListener('abort', () => {
564
+ waitQueueMember[kCancelled] = true;
565
+ waitQueueMember.timeoutController.clear();
566
+ const timeoutError = new MongoServerSelectionError(
567
+ `Server selection timed out after ${options.serverSelectionTimeoutMS} ms`,
568
+ this.description
569
+ );
571
570
 
572
- waitQueueMember.callback(timeoutError);
573
- }, serverSelectionTimeoutMS);
574
- }
571
+ waitQueueMember.callback(timeoutError);
572
+ });
575
573
 
576
574
  this[kWaitQueue].push(waitQueueMember);
577
575
  processWaitQueue(this);
@@ -842,9 +840,7 @@ function drainWaitQueue(queue: List<ServerSelectionRequest>, err?: MongoDriverEr
842
840
  continue;
843
841
  }
844
842
 
845
- if (waitQueueMember.timer) {
846
- clearTimeout(waitQueueMember.timer);
847
- }
843
+ waitQueueMember.timeoutController.clear();
848
844
 
849
845
  if (!waitQueueMember[kCancelled]) {
850
846
  waitQueueMember.callback(err);
@@ -878,9 +874,7 @@ function processWaitQueue(topology: Topology) {
878
874
  ? serverSelector(topology.description, serverDescriptions)
879
875
  : serverDescriptions;
880
876
  } catch (e) {
881
- if (waitQueueMember.timer) {
882
- clearTimeout(waitQueueMember.timer);
883
- }
877
+ waitQueueMember.timeoutController.clear();
884
878
 
885
879
  waitQueueMember.callback(e);
886
880
  continue;
@@ -917,9 +911,7 @@ function processWaitQueue(topology: Topology) {
917
911
  transaction.pinServer(selectedServer);
918
912
  }
919
913
 
920
- if (waitQueueMember.timer) {
921
- clearTimeout(waitQueueMember.timer);
922
- }
914
+ waitQueueMember.timeoutController.clear();
923
915
 
924
916
  waitQueueMember.callback(undefined, selectedServer);
925
917
  }
package/src/utils.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as crypto from 'crypto';
2
2
  import type { SrvRecord } from 'dns';
3
3
  import * as http from 'http';
4
+ import { clearTimeout, setTimeout } from 'timers';
4
5
  import * as url from 'url';
5
6
  import { URL } from 'url';
6
7
 
@@ -78,39 +79,6 @@ export function hostMatchesWildcards(host: string, wildcards: string[]): boolean
78
79
  return false;
79
80
  }
80
81
 
81
- /**
82
- * Throws if collectionName is not a valid mongodb collection namespace.
83
- * @internal
84
- */
85
- export function checkCollectionName(collectionName: string): void {
86
- if ('string' !== typeof collectionName) {
87
- throw new MongoInvalidArgumentError('Collection name must be a String');
88
- }
89
-
90
- if (!collectionName || collectionName.indexOf('..') !== -1) {
91
- throw new MongoInvalidArgumentError('Collection names cannot be empty');
92
- }
93
-
94
- if (
95
- collectionName.indexOf('$') !== -1 &&
96
- collectionName.match(/((^\$cmd)|(oplog\.\$main))/) == null
97
- ) {
98
- // TODO(NODE-3483): Use MongoNamespace static method
99
- throw new MongoInvalidArgumentError("Collection names must not contain '$'");
100
- }
101
-
102
- if (collectionName.match(/^\.|\.$/) != null) {
103
- // TODO(NODE-3483): Use MongoNamespace static method
104
- throw new MongoInvalidArgumentError("Collection names must not start or end with '.'");
105
- }
106
-
107
- // Validate that we are not passing 0x00 in the collection name
108
- if (collectionName.indexOf('\x00') !== -1) {
109
- // TODO(NODE-3483): Use MongoNamespace static method
110
- throw new MongoInvalidArgumentError('Collection names cannot contain a null character');
111
- }
112
- }
113
-
114
82
  /**
115
83
  * Ensure Hint field is in a shape we expect:
116
84
  * - object of index names mapping to 1 or -1
@@ -386,11 +354,6 @@ export function maybeCallback<T>(
386
354
  return;
387
355
  }
388
356
 
389
- /** @internal */
390
- export function databaseNamespace(ns: string): string {
391
- return ns.split('.')[0];
392
- }
393
-
394
357
  /**
395
358
  * Synchronously Generate a UUIDv4
396
359
  * @internal
@@ -1292,3 +1255,30 @@ export async function request(
1292
1255
  req.end();
1293
1256
  });
1294
1257
  }
1258
+
1259
+ /**
1260
+ * A custom AbortController that aborts after a specified timeout.
1261
+ *
1262
+ * If `timeout` is undefined or \<=0, the abort controller never aborts.
1263
+ *
1264
+ * This class provides two benefits over the built-in AbortSignal.timeout() method.
1265
+ * - This class provides a mechanism for cancelling the timeout
1266
+ * - This class supports infinite timeouts by interpreting a timeout of 0 as infinite. This is
1267
+ * consistent with existing timeout options in the Node driver (serverSelectionTimeoutMS, for example).
1268
+ * @internal
1269
+ */
1270
+ export class TimeoutController extends AbortController {
1271
+ constructor(
1272
+ timeout = 0,
1273
+ private timeoutId = timeout > 0 ? setTimeout(() => this.abort(), timeout) : null
1274
+ ) {
1275
+ super();
1276
+ }
1277
+
1278
+ clear() {
1279
+ if (this.timeoutId != null) {
1280
+ clearTimeout(this.timeoutId);
1281
+ }
1282
+ this.timeoutId = null;
1283
+ }
1284
+ }