mongodb 6.7.0 → 6.8.0-dev.20240629.sha.d85f827a

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.
Files changed (105) hide show
  1. package/README.md +29 -1
  2. package/lib/bson.js.map +1 -1
  3. package/lib/client-side-encryption/auto_encrypter.js +8 -61
  4. package/lib/client-side-encryption/auto_encrypter.js.map +1 -1
  5. package/lib/client-side-encryption/client_encryption.js +5 -5
  6. package/lib/client-side-encryption/client_encryption.js.map +1 -1
  7. package/lib/client-side-encryption/providers/index.js.map +1 -1
  8. package/lib/client-side-encryption/state_machine.js +15 -11
  9. package/lib/client-side-encryption/state_machine.js.map +1 -1
  10. package/lib/cmap/connection.js +22 -20
  11. package/lib/cmap/connection.js.map +1 -1
  12. package/lib/cmap/wire_protocol/constants.js +2 -2
  13. package/lib/cmap/wire_protocol/on_demand/document.js +8 -5
  14. package/lib/cmap/wire_protocol/on_demand/document.js.map +1 -1
  15. package/lib/cmap/wire_protocol/responses.js +116 -40
  16. package/lib/cmap/wire_protocol/responses.js.map +1 -1
  17. package/lib/collection.js +13 -2
  18. package/lib/collection.js.map +1 -1
  19. package/lib/constants.js +9 -1
  20. package/lib/constants.js.map +1 -1
  21. package/lib/cursor/abstract_cursor.js +231 -285
  22. package/lib/cursor/abstract_cursor.js.map +1 -1
  23. package/lib/cursor/aggregation_cursor.js +11 -19
  24. package/lib/cursor/aggregation_cursor.js.map +1 -1
  25. package/lib/cursor/change_stream_cursor.js +12 -14
  26. package/lib/cursor/change_stream_cursor.js.map +1 -1
  27. package/lib/cursor/find_cursor.js +64 -84
  28. package/lib/cursor/find_cursor.js.map +1 -1
  29. package/lib/cursor/list_collections_cursor.js +0 -1
  30. package/lib/cursor/list_collections_cursor.js.map +1 -1
  31. package/lib/cursor/list_indexes_cursor.js +0 -1
  32. package/lib/cursor/list_indexes_cursor.js.map +1 -1
  33. package/lib/cursor/run_command_cursor.js +4 -6
  34. package/lib/cursor/run_command_cursor.js.map +1 -1
  35. package/lib/error.js +10 -23
  36. package/lib/error.js.map +1 -1
  37. package/lib/index.js.map +1 -1
  38. package/lib/operations/aggregate.js +2 -2
  39. package/lib/operations/aggregate.js.map +1 -1
  40. package/lib/operations/bulk_write.js +1 -2
  41. package/lib/operations/bulk_write.js.map +1 -1
  42. package/lib/operations/command.js +2 -3
  43. package/lib/operations/command.js.map +1 -1
  44. package/lib/operations/execute_operation.js.map +1 -1
  45. package/lib/operations/find.js +2 -1
  46. package/lib/operations/find.js.map +1 -1
  47. package/lib/operations/get_more.js +1 -1
  48. package/lib/operations/get_more.js.map +1 -1
  49. package/lib/operations/indexes.js +2 -1
  50. package/lib/operations/indexes.js.map +1 -1
  51. package/lib/operations/list_collections.js +2 -1
  52. package/lib/operations/list_collections.js.map +1 -1
  53. package/lib/operations/run_command.js +1 -1
  54. package/lib/operations/run_command.js.map +1 -1
  55. package/lib/operations/update.js +2 -1
  56. package/lib/operations/update.js.map +1 -1
  57. package/lib/sdam/server.js +7 -2
  58. package/lib/sdam/server.js.map +1 -1
  59. package/lib/sessions.js +1 -1
  60. package/lib/sessions.js.map +1 -1
  61. package/lib/utils.js +45 -1
  62. package/lib/utils.js.map +1 -1
  63. package/lib/write_concern.js +17 -1
  64. package/lib/write_concern.js.map +1 -1
  65. package/mongodb.d.ts +188 -221
  66. package/package.json +3 -3
  67. package/src/bson.ts +1 -0
  68. package/src/client-side-encryption/auto_encrypter.ts +10 -149
  69. package/src/client-side-encryption/client_encryption.ts +33 -19
  70. package/src/client-side-encryption/providers/index.ts +118 -92
  71. package/src/client-side-encryption/state_machine.ts +22 -18
  72. package/src/cmap/connection.ts +46 -50
  73. package/src/cmap/wire_protocol/constants.ts +2 -2
  74. package/src/cmap/wire_protocol/on_demand/document.ts +13 -6
  75. package/src/cmap/wire_protocol/responses.ts +140 -45
  76. package/src/collection.ts +25 -5
  77. package/src/constants.ts +9 -0
  78. package/src/cursor/abstract_cursor.ts +280 -373
  79. package/src/cursor/aggregation_cursor.ts +24 -33
  80. package/src/cursor/change_stream_cursor.ts +31 -48
  81. package/src/cursor/find_cursor.ts +77 -92
  82. package/src/cursor/list_collections_cursor.ts +3 -4
  83. package/src/cursor/list_indexes_cursor.ts +3 -4
  84. package/src/cursor/run_command_cursor.ts +13 -19
  85. package/src/error.ts +20 -30
  86. package/src/index.ts +19 -10
  87. package/src/operations/aggregate.ts +12 -5
  88. package/src/operations/bulk_write.ts +1 -2
  89. package/src/operations/command.ts +17 -3
  90. package/src/operations/delete.ts +2 -2
  91. package/src/operations/execute_operation.ts +0 -13
  92. package/src/operations/find.ts +7 -3
  93. package/src/operations/find_and_modify.ts +1 -1
  94. package/src/operations/get_more.ts +6 -10
  95. package/src/operations/indexes.ts +7 -3
  96. package/src/operations/list_collections.ts +8 -3
  97. package/src/operations/run_command.ts +16 -6
  98. package/src/operations/update.ts +2 -1
  99. package/src/sdam/server.ts +7 -2
  100. package/src/sessions.ts +1 -1
  101. package/src/utils.ts +52 -2
  102. package/src/write_concern.ts +18 -0
  103. package/lib/operations/count_documents.js +0 -31
  104. package/lib/operations/count_documents.js.map +0 -1
  105. package/src/operations/count_documents.ts +0 -46
@@ -1,14 +1,15 @@
1
1
  import { type Readable, Transform, type TransformCallback } from 'stream';
2
2
  import { clearTimeout, setTimeout } from 'timers';
3
3
 
4
- import type { BSONSerializeOptions, Document, ObjectId } from '../bson';
5
- import type { AutoEncrypter } from '../client-side-encryption/auto_encrypter';
4
+ import { type BSONSerializeOptions, deserialize, type Document, type ObjectId } from '../bson';
5
+ import { type AutoEncrypter } from '../client-side-encryption/auto_encrypter';
6
6
  import {
7
7
  CLOSE,
8
8
  CLUSTER_TIME_RECEIVED,
9
9
  COMMAND_FAILED,
10
10
  COMMAND_STARTED,
11
11
  COMMAND_SUCCEEDED,
12
+ kDecorateResult,
12
13
  PINNED,
13
14
  UNPINNED
14
15
  } from '../constants';
@@ -19,8 +20,7 @@ import {
19
20
  MongoNetworkTimeoutError,
20
21
  MongoParseError,
21
22
  MongoServerError,
22
- MongoUnexpectedServerResponseError,
23
- MongoWriteConcernError
23
+ MongoUnexpectedServerResponseError
24
24
  } from '../error';
25
25
  import type { ServerApi, SupportedNodeConnectionOptions } from '../mongo_client';
26
26
  import { type MongoClientAuthProviders } from '../mongo_client_auth_providers';
@@ -33,6 +33,7 @@ import {
33
33
  BufferPool,
34
34
  calculateDurationInMs,
35
35
  type Callback,
36
+ decorateDecryptionResult,
36
37
  HostAddress,
37
38
  maxWireVersion,
38
39
  type MongoDBNamespace,
@@ -63,7 +64,7 @@ import { StreamDescription, type StreamDescriptionOptions } from './stream_descr
63
64
  import { type CompressorName, decompressResponse } from './wire_protocol/compression';
64
65
  import { onData } from './wire_protocol/on_data';
65
66
  import {
66
- isErrorResponse,
67
+ CursorResponse,
67
68
  MongoDBResponse,
68
69
  type MongoDBResponseConstructor
69
70
  } from './wire_protocol/responses';
@@ -448,12 +449,7 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
448
449
  this.socket.setTimeout(0);
449
450
  const bson = response.parse();
450
451
 
451
- const document =
452
- responseType == null
453
- ? new MongoDBResponse(bson)
454
- : isErrorResponse(bson)
455
- ? new MongoDBResponse(bson)
456
- : new responseType(bson);
452
+ const document = (responseType ?? MongoDBResponse).make(bson);
457
453
 
458
454
  yield document;
459
455
  this.throwIfAborted();
@@ -517,12 +513,7 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
517
513
  this.emit(Connection.CLUSTER_TIME_RECEIVED, document.$clusterTime);
518
514
  }
519
515
 
520
- if (document.has('writeConcernError')) {
521
- object ??= document.toObject(bsonOptions);
522
- throw new MongoWriteConcernError(object.writeConcernError, object);
523
- }
524
-
525
- if (document.isError) {
516
+ if (document.ok === 0) {
526
517
  throw new MongoServerError((object ??= document.toObject(bsonOptions)));
527
518
  }
528
519
 
@@ -552,40 +543,25 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
552
543
  }
553
544
  } catch (error) {
554
545
  if (this.shouldEmitAndLogCommand) {
555
- if (error.name === 'MongoWriteConcernError') {
556
- this.emitAndLogCommand(
557
- this.monitorCommands,
558
- Connection.COMMAND_SUCCEEDED,
559
- message.databaseName,
560
- this.established,
561
- new CommandSucceededEvent(
562
- this,
563
- message,
564
- options.noResponse ? undefined : (object ??= document?.toObject(bsonOptions)),
565
- started,
566
- this.description.serverConnectionId
567
- )
568
- );
569
- } else {
570
- this.emitAndLogCommand(
571
- this.monitorCommands,
572
- Connection.COMMAND_FAILED,
573
- message.databaseName,
574
- this.established,
575
- new CommandFailedEvent(
576
- this,
577
- message,
578
- error,
579
- started,
580
- this.description.serverConnectionId
581
- )
582
- );
583
- }
546
+ this.emitAndLogCommand(
547
+ this.monitorCommands,
548
+ Connection.COMMAND_FAILED,
549
+ message.databaseName,
550
+ this.established,
551
+ new CommandFailedEvent(this, message, error, started, this.description.serverConnectionId)
552
+ );
584
553
  }
585
554
  throw error;
586
555
  }
587
556
  }
588
557
 
558
+ public async command<T extends MongoDBResponseConstructor>(
559
+ ns: MongoDBNamespace,
560
+ command: Document,
561
+ options: CommandOptions | undefined,
562
+ responseType: T
563
+ ): Promise<InstanceType<T>>;
564
+
589
565
  public async command<T extends MongoDBResponseConstructor>(
590
566
  ns: MongoDBNamespace,
591
567
  command: Document,
@@ -749,7 +725,7 @@ export class CryptoConnection extends Connection {
749
725
  ns: MongoDBNamespace,
750
726
  cmd: Document,
751
727
  options?: CommandOptions,
752
- _responseType?: T | undefined
728
+ responseType?: T | undefined
753
729
  ): Promise<Document> {
754
730
  const { autoEncrypter } = this;
755
731
  if (!autoEncrypter) {
@@ -763,7 +739,7 @@ export class CryptoConnection extends Connection {
763
739
  const serverWireVersion = maxWireVersion(this);
764
740
  if (serverWireVersion === 0) {
765
741
  // This means the initial handshake hasn't happened yet
766
- return await super.command<T>(ns, cmd, options, undefined);
742
+ return await super.command<T>(ns, cmd, options, responseType);
767
743
  }
768
744
 
769
745
  if (serverWireVersion < 8) {
@@ -797,8 +773,28 @@ export class CryptoConnection extends Connection {
797
773
  }
798
774
  }
799
775
 
800
- const response = await super.command<T>(ns, encrypted, options, undefined);
776
+ const encryptedResponse = await super.command(
777
+ ns,
778
+ encrypted,
779
+ options,
780
+ // Eventually we want to require `responseType` which means we would satisfy `T` as the return type.
781
+ // In the meantime, we want encryptedResponse to always be _at least_ a MongoDBResponse if not a more specific subclass
782
+ // So that we can ensure we have access to the on-demand APIs for decorate response
783
+ responseType ?? MongoDBResponse
784
+ );
785
+
786
+ const result = await autoEncrypter.decrypt(encryptedResponse.toBytes(), options);
787
+
788
+ const decryptedResponse = responseType?.make(result) ?? deserialize(result, options);
789
+
790
+ if (autoEncrypter[kDecorateResult]) {
791
+ if (responseType == null) {
792
+ decorateDecryptionResult(decryptedResponse, encryptedResponse.toObject(), true);
793
+ } else if (decryptedResponse instanceof CursorResponse) {
794
+ decryptedResponse.encryptedResponse = encryptedResponse;
795
+ }
796
+ }
801
797
 
802
- return await autoEncrypter.decrypt(response, options);
798
+ return decryptedResponse;
803
799
  }
804
800
  }
@@ -1,7 +1,7 @@
1
1
  export const MIN_SUPPORTED_SERVER_VERSION = '3.6';
2
- export const MAX_SUPPORTED_SERVER_VERSION = '7.0';
2
+ export const MAX_SUPPORTED_SERVER_VERSION = '8.0';
3
3
  export const MIN_SUPPORTED_WIRE_VERSION = 6;
4
- export const MAX_SUPPORTED_WIRE_VERSION = 21;
4
+ export const MAX_SUPPORTED_WIRE_VERSION = 25;
5
5
  export const MIN_SUPPORTED_QE_WIRE_VERSION = 21;
6
6
  export const MIN_SUPPORTED_QE_SERVER_VERSION = '7.0';
7
7
  export const OP_REPLY = 1;
@@ -66,9 +66,11 @@ export class OnDemandDocument {
66
66
  /** The start of the document */
67
67
  private readonly offset = 0,
68
68
  /** If this is an embedded document, indicates if this was a BSON array */
69
- public readonly isArray = false
69
+ public readonly isArray = false,
70
+ /** If elements was already calculated */
71
+ elements?: BSONElement[]
70
72
  ) {
71
- this.elements = parseToElementsToArray(this.bson, offset);
73
+ this.elements = elements ?? parseToElementsToArray(this.bson, offset);
72
74
  }
73
75
 
74
76
  /** Only supports basic latin strings */
@@ -78,8 +80,13 @@ export class OnDemandDocument {
78
80
 
79
81
  if (name.length !== nameLength) return false;
80
82
 
81
- for (let i = 0; i < name.length; i++) {
82
- if (this.bson[nameOffset + i] !== name.charCodeAt(i)) return false;
83
+ const nameEnd = nameOffset + nameLength;
84
+ for (
85
+ let byteIndex = nameOffset, charIndex = 0;
86
+ charIndex < name.length && byteIndex < nameEnd;
87
+ charIndex++, byteIndex++
88
+ ) {
89
+ if (this.bson[byteIndex] !== name.charCodeAt(charIndex)) return false;
83
90
  }
84
91
 
85
92
  return true;
@@ -125,7 +132,7 @@ export class OnDemandDocument {
125
132
  const element = this.elements[index];
126
133
 
127
134
  // skip this element if it has already been associated with a name
128
- if (!this.indexFound[index] && this.isElementName(name, element)) {
135
+ if (!(index in this.indexFound) && this.isElementName(name, element)) {
129
136
  const cachedElement = { element, value: undefined };
130
137
  this.cache[name] = cachedElement;
131
138
  this.indexFound[index] = true;
@@ -247,7 +254,7 @@ export class OnDemandDocument {
247
254
  public get<const T extends keyof JSTypeOf>(
248
255
  name: string | number,
249
256
  as: T,
250
- required?: false | undefined
257
+ required?: boolean | undefined
251
258
  ): JSTypeOf[T] | null;
252
259
 
253
260
  /** `required` will make `get` throw if name does not exist or is null/undefined */
@@ -1,15 +1,17 @@
1
1
  import {
2
+ type BSONElement,
2
3
  type BSONSerializeOptions,
3
4
  BSONType,
4
5
  type Document,
5
6
  Long,
6
7
  parseToElementsToArray,
8
+ pluckBSONSerializeOptions,
7
9
  type Timestamp
8
10
  } from '../../bson';
9
11
  import { MongoUnexpectedServerResponseError } from '../../error';
10
12
  import { type ClusterTime } from '../../sdam/common';
11
- import { type MongoDBNamespace, ns } from '../../utils';
12
- import { OnDemandDocument } from './on_demand/document';
13
+ import { decorateDecryptionResult, ns } from '../../utils';
14
+ import { type JSTypeOf, OnDemandDocument } from './on_demand/document';
13
15
 
14
16
  // eslint-disable-next-line no-restricted-syntax
15
17
  const enum BSONElementOffset {
@@ -30,8 +32,7 @@ const enum BSONElementOffset {
30
32
  *
31
33
  * @param bytes - BSON document returned from the server
32
34
  */
33
- export function isErrorResponse(bson: Uint8Array): boolean {
34
- const elements = parseToElementsToArray(bson, 0);
35
+ export function isErrorResponse(bson: Uint8Array, elements: BSONElement[]): boolean {
35
36
  for (let eIdx = 0; eIdx < elements.length; eIdx++) {
36
37
  const element = elements[eIdx];
37
38
 
@@ -60,26 +61,49 @@ export function isErrorResponse(bson: Uint8Array): boolean {
60
61
  /** @internal */
61
62
  export type MongoDBResponseConstructor = {
62
63
  new (bson: Uint8Array, offset?: number, isArray?: boolean): MongoDBResponse;
64
+ make(bson: Uint8Array): MongoDBResponse;
63
65
  };
64
66
 
65
67
  /** @internal */
66
68
  export class MongoDBResponse extends OnDemandDocument {
69
+ // Wrap error thrown from BSON
70
+ public override get<const T extends keyof JSTypeOf>(
71
+ name: string | number,
72
+ as: T,
73
+ required?: false | undefined
74
+ ): JSTypeOf[T] | null;
75
+ public override get<const T extends keyof JSTypeOf>(
76
+ name: string | number,
77
+ as: T,
78
+ required: true
79
+ ): JSTypeOf[T];
80
+ public override get<const T extends keyof JSTypeOf>(
81
+ name: string | number,
82
+ as: T,
83
+ required?: boolean | undefined
84
+ ): JSTypeOf[T] | null {
85
+ try {
86
+ return super.get(name, as, required);
87
+ } catch (cause) {
88
+ throw new MongoUnexpectedServerResponseError(cause.message, { cause });
89
+ }
90
+ }
91
+
67
92
  static is(value: unknown): value is MongoDBResponse {
68
93
  return value instanceof MongoDBResponse;
69
94
  }
70
95
 
96
+ static make(bson: Uint8Array) {
97
+ const elements = parseToElementsToArray(bson, 0);
98
+ const isError = isErrorResponse(bson, elements);
99
+ return isError
100
+ ? new MongoDBResponse(bson, 0, false, elements)
101
+ : new this(bson, 0, false, elements);
102
+ }
103
+
71
104
  // {ok:1}
72
105
  static empty = new MongoDBResponse(new Uint8Array([13, 0, 0, 0, 16, 111, 107, 0, 1, 0, 0, 0, 0]));
73
106
 
74
- /** Indicates this document is a server error */
75
- public get isError() {
76
- let isError = this.ok === 0;
77
- isError ||= this.has('errmsg');
78
- isError ||= this.has('code');
79
- isError ||= this.has('$err'); // The '$err' field is used in OP_REPLY responses
80
- return isError;
81
- }
82
-
83
107
  /**
84
108
  * Drivers can safely assume that the `recoveryToken` field is always a BSON document but drivers MUST NOT modify the
85
109
  * contents of the document.
@@ -110,6 +134,7 @@ export class MongoDBResponse extends OnDemandDocument {
110
134
  return this.get('operationTime', BSONType.timestamp);
111
135
  }
112
136
 
137
+ /** Normalizes whatever BSON value is "ok" to a JS number 1 or 0. */
113
138
  public get ok(): 0 | 1 {
114
139
  return this.getNumber('ok') ? 1 : 0;
115
140
  }
@@ -144,13 +169,7 @@ export class MongoDBResponse extends OnDemandDocument {
144
169
 
145
170
  public override toObject(options?: BSONSerializeOptions): Record<string, any> {
146
171
  const exactBSONOptions = {
147
- useBigInt64: options?.useBigInt64,
148
- promoteLongs: options?.promoteLongs,
149
- promoteValues: options?.promoteValues,
150
- promoteBuffers: options?.promoteBuffers,
151
- bsonRegExp: options?.bsonRegExp,
152
- raw: options?.raw ?? false,
153
- fieldsAsRaw: options?.fieldsAsRaw ?? {},
172
+ ...pluckBSONSerializeOptions(options ?? {}),
154
173
  validation: this.parseBsonSerializationOptions(options)
155
174
  };
156
175
  return super.toObject(exactBSONOptions);
@@ -169,69 +188,145 @@ export class MongoDBResponse extends OnDemandDocument {
169
188
 
170
189
  /** @internal */
171
190
  export class CursorResponse extends MongoDBResponse {
191
+ /**
192
+ * Devtools need to know which keys were encrypted before the driver automatically decrypted them.
193
+ * If decorating is enabled (`Symbol.for('@@mdb.decorateDecryptionResult')`), this field will be set,
194
+ * storing the original encrypted response from the server, so that we can build an object that has
195
+ * the list of BSON keys that were encrypted stored at a well known symbol: `Symbol.for('@@mdb.decryptedKeys')`.
196
+ */
197
+ encryptedResponse?: MongoDBResponse;
172
198
  /**
173
199
  * This supports a feature of the FindCursor.
174
200
  * It is an optimization to avoid an extra getMore when the limit has been reached
175
201
  */
176
- static emptyGetMore = { id: new Long(0), length: 0, shift: () => null };
202
+ static emptyGetMore: CursorResponse = {
203
+ id: new Long(0),
204
+ length: 0,
205
+ shift: () => null
206
+ } as unknown as CursorResponse;
177
207
 
178
208
  static override is(value: unknown): value is CursorResponse {
179
209
  return value instanceof CursorResponse || value === CursorResponse.emptyGetMore;
180
210
  }
181
211
 
182
- public id: Long;
183
- public ns: MongoDBNamespace | null = null;
184
- public batchSize = 0;
185
-
186
- private batch: OnDemandDocument;
212
+ private _batch: OnDemandDocument | null = null;
187
213
  private iterated = 0;
188
214
 
189
- constructor(bytes: Uint8Array, offset?: number, isArray?: boolean) {
190
- super(bytes, offset, isArray);
215
+ get cursor() {
216
+ return this.get('cursor', BSONType.object, true);
217
+ }
191
218
 
192
- const cursor = this.get('cursor', BSONType.object, true);
219
+ public get id(): Long {
220
+ try {
221
+ return Long.fromBigInt(this.cursor.get('id', BSONType.long, true));
222
+ } catch (cause) {
223
+ throw new MongoUnexpectedServerResponseError(cause.message, { cause });
224
+ }
225
+ }
193
226
 
194
- const id = cursor.get('id', BSONType.long, true);
195
- this.id = new Long(Number(id & 0xffff_ffffn), Number((id >> 32n) & 0xffff_ffffn));
227
+ public get ns() {
228
+ const namespace = this.cursor.get('ns', BSONType.string);
229
+ if (namespace != null) return ns(namespace);
230
+ return null;
231
+ }
232
+
233
+ public get length() {
234
+ return Math.max(this.batchSize - this.iterated, 0);
235
+ }
196
236
 
197
- const namespace = cursor.get('ns', BSONType.string);
198
- if (namespace != null) this.ns = ns(namespace);
237
+ private _encryptedBatch: OnDemandDocument | null = null;
238
+ get encryptedBatch() {
239
+ if (this.encryptedResponse == null) return null;
240
+ if (this._encryptedBatch != null) return this._encryptedBatch;
199
241
 
200
- if (cursor.has('firstBatch')) this.batch = cursor.get('firstBatch', BSONType.array, true);
201
- else if (cursor.has('nextBatch')) this.batch = cursor.get('nextBatch', BSONType.array, true);
242
+ const cursor = this.encryptedResponse?.get('cursor', BSONType.object);
243
+ if (cursor?.has('firstBatch'))
244
+ this._encryptedBatch = cursor.get('firstBatch', BSONType.array, true);
245
+ else if (cursor?.has('nextBatch'))
246
+ this._encryptedBatch = cursor.get('nextBatch', BSONType.array, true);
202
247
  else throw new MongoUnexpectedServerResponseError('Cursor document did not contain a batch');
203
248
 
204
- this.batchSize = this.batch.size();
249
+ return this._encryptedBatch;
205
250
  }
206
251
 
207
- get length() {
208
- return Math.max(this.batchSize - this.iterated, 0);
252
+ private get batch() {
253
+ if (this._batch != null) return this._batch;
254
+ const cursor = this.cursor;
255
+ if (cursor.has('firstBatch')) this._batch = cursor.get('firstBatch', BSONType.array, true);
256
+ else if (cursor.has('nextBatch')) this._batch = cursor.get('nextBatch', BSONType.array, true);
257
+ else throw new MongoUnexpectedServerResponseError('Cursor document did not contain a batch');
258
+ return this._batch;
259
+ }
260
+
261
+ public get batchSize() {
262
+ return this.batch?.size();
209
263
  }
210
264
 
211
- shift(options?: BSONSerializeOptions): any {
265
+ public get postBatchResumeToken() {
266
+ return (
267
+ this.cursor.get('postBatchResumeToken', BSONType.object)?.toObject({
268
+ promoteValues: false,
269
+ promoteLongs: false,
270
+ promoteBuffers: false
271
+ }) ?? null
272
+ );
273
+ }
274
+
275
+ public shift(options?: BSONSerializeOptions): any {
212
276
  if (this.iterated >= this.batchSize) {
213
277
  return null;
214
278
  }
215
279
 
216
280
  const result = this.batch.get(this.iterated, BSONType.object, true) ?? null;
281
+ const encryptedResult = this.encryptedBatch?.get(this.iterated, BSONType.object, true) ?? null;
282
+
217
283
  this.iterated += 1;
218
284
 
219
285
  if (options?.raw) {
220
286
  return result.toBytes();
221
287
  } else {
222
- return result.toObject(options);
288
+ const object = result.toObject(options);
289
+ if (encryptedResult) {
290
+ decorateDecryptionResult(object, encryptedResult.toObject(options), true);
291
+ }
292
+ return object;
223
293
  }
224
294
  }
225
295
 
226
- clear() {
296
+ public clear() {
227
297
  this.iterated = this.batchSize;
228
298
  }
299
+ }
300
+
301
+ /**
302
+ * Explain responses have nothing to do with cursor responses
303
+ * This class serves to temporarily avoid refactoring how cursors handle
304
+ * explain responses which is to detect that the response is not cursor-like and return the explain
305
+ * result as the "first and only" document in the "batch" and end the "cursor"
306
+ */
307
+ export class ExplainedCursorResponse extends CursorResponse {
308
+ isExplain = true;
309
+
310
+ override get id(): Long {
311
+ return Long.fromBigInt(0n);
312
+ }
313
+
314
+ override get batchSize() {
315
+ return 0;
316
+ }
317
+
318
+ override get ns() {
319
+ return null;
320
+ }
229
321
 
230
- pushMany() {
231
- throw new Error('pushMany Unsupported method');
322
+ _length = 1;
323
+ override get length(): number {
324
+ return this._length;
232
325
  }
233
326
 
234
- push() {
235
- throw new Error('push Unsupported method');
327
+ override shift(options?: BSONSerializeOptions | undefined) {
328
+ if (this._length === 0) return null;
329
+ this._length -= 1;
330
+ return this.toObject(options);
236
331
  }
237
332
  }
package/src/collection.ts CHANGED
@@ -25,7 +25,6 @@ import type {
25
25
  import type { AggregateOptions } from './operations/aggregate';
26
26
  import { BulkWriteOperation } from './operations/bulk_write';
27
27
  import { CountOperation, type CountOptions } from './operations/count';
28
- import { CountDocumentsOperation, type CountDocumentsOptions } from './operations/count_documents';
29
28
  import {
30
29
  DeleteManyOperation,
31
30
  DeleteOneOperation,
@@ -101,6 +100,14 @@ export interface ModifyResult<TSchema = Document> {
101
100
  ok: 0 | 1;
102
101
  }
103
102
 
103
+ /** @public */
104
+ export interface CountDocumentsOptions extends AggregateOptions {
105
+ /** The number of documents to skip. */
106
+ skip?: number;
107
+ /** The maximum amount of documents to consider. */
108
+ limit?: number;
109
+ }
110
+
104
111
  /** @public */
105
112
  export interface CollectionOptions extends BSONSerializeOptions, WriteConcernOptions {
106
113
  /** Specify a read concern for the collection. (only MongoDB 3.2 or higher supported) */
@@ -764,10 +771,23 @@ export class Collection<TSchema extends Document = Document> {
764
771
  filter: Filter<TSchema> = {},
765
772
  options: CountDocumentsOptions = {}
766
773
  ): Promise<number> {
767
- return await executeOperation(
768
- this.client,
769
- new CountDocumentsOperation(this as TODO_NODE_3286, filter, resolveOptions(this, options))
770
- );
774
+ const pipeline = [];
775
+ pipeline.push({ $match: filter });
776
+
777
+ if (typeof options.skip === 'number') {
778
+ pipeline.push({ $skip: options.skip });
779
+ }
780
+
781
+ if (typeof options.limit === 'number') {
782
+ pipeline.push({ $limit: options.limit });
783
+ }
784
+
785
+ pipeline.push({ $group: { _id: 1, n: { $sum: 1 } } });
786
+
787
+ const cursor = this.aggregate<{ n: number }>(pipeline, options);
788
+ const doc = await cursor.next();
789
+ await cursor.close();
790
+ return doc?.n ?? 0;
771
791
  }
772
792
 
773
793
  /**
package/src/constants.ts CHANGED
@@ -165,3 +165,12 @@ export const LEGACY_HELLO_COMMAND = 'ismaster';
165
165
  * The legacy hello command that was deprecated in MongoDB 5.0.
166
166
  */
167
167
  export const LEGACY_HELLO_COMMAND_CAMEL_CASE = 'isMaster';
168
+
169
+ // Typescript errors if we index objects with `Symbol.for(...)`, so
170
+ // to avoid TS errors we pull them out into variables. Then we can type
171
+ // the objects (and class) that we expect to see them on and prevent TS
172
+ // errors.
173
+ /** @internal */
174
+ export const kDecorateResult = Symbol.for('@@mdb.decorateDecryptionResult');
175
+ /** @internal */
176
+ export const kDecoratedKeys = Symbol.for('@@mdb.decryptedKeys');