mongodb 6.7.0-dev.20240613.sha.c1af6adc → 6.7.0-dev.20240614.sha.3ed6a2ad

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 (93) hide show
  1. package/lib/bson.js.map +1 -1
  2. package/lib/client-side-encryption/auto_encrypter.js +8 -61
  3. package/lib/client-side-encryption/auto_encrypter.js.map +1 -1
  4. package/lib/client-side-encryption/client_encryption.js +5 -5
  5. package/lib/client-side-encryption/client_encryption.js.map +1 -1
  6. package/lib/client-side-encryption/state_machine.js +15 -11
  7. package/lib/client-side-encryption/state_machine.js.map +1 -1
  8. package/lib/cmap/connection.js +22 -20
  9. package/lib/cmap/connection.js.map +1 -1
  10. package/lib/cmap/wire_protocol/on_demand/document.js +8 -5
  11. package/lib/cmap/wire_protocol/on_demand/document.js.map +1 -1
  12. package/lib/cmap/wire_protocol/responses.js +116 -40
  13. package/lib/cmap/wire_protocol/responses.js.map +1 -1
  14. package/lib/constants.js +9 -1
  15. package/lib/constants.js.map +1 -1
  16. package/lib/cursor/abstract_cursor.js +24 -60
  17. package/lib/cursor/abstract_cursor.js.map +1 -1
  18. package/lib/cursor/aggregation_cursor.js +2 -3
  19. package/lib/cursor/aggregation_cursor.js.map +1 -1
  20. package/lib/cursor/change_stream_cursor.js +6 -8
  21. package/lib/cursor/change_stream_cursor.js.map +1 -1
  22. package/lib/cursor/find_cursor.js +5 -17
  23. package/lib/cursor/find_cursor.js.map +1 -1
  24. package/lib/cursor/list_collections_cursor.js +0 -1
  25. package/lib/cursor/list_collections_cursor.js.map +1 -1
  26. package/lib/cursor/list_indexes_cursor.js +0 -1
  27. package/lib/cursor/list_indexes_cursor.js.map +1 -1
  28. package/lib/cursor/run_command_cursor.js +4 -6
  29. package/lib/cursor/run_command_cursor.js.map +1 -1
  30. package/lib/error.js +6 -21
  31. package/lib/error.js.map +1 -1
  32. package/lib/operations/aggregate.js +2 -2
  33. package/lib/operations/aggregate.js.map +1 -1
  34. package/lib/operations/bulk_write.js +1 -2
  35. package/lib/operations/bulk_write.js.map +1 -1
  36. package/lib/operations/command.js +2 -3
  37. package/lib/operations/command.js.map +1 -1
  38. package/lib/operations/count_documents.js +1 -7
  39. package/lib/operations/count_documents.js.map +1 -1
  40. package/lib/operations/execute_operation.js.map +1 -1
  41. package/lib/operations/find.js +2 -1
  42. package/lib/operations/find.js.map +1 -1
  43. package/lib/operations/get_more.js +1 -1
  44. package/lib/operations/get_more.js.map +1 -1
  45. package/lib/operations/indexes.js +2 -1
  46. package/lib/operations/indexes.js.map +1 -1
  47. package/lib/operations/list_collections.js +2 -1
  48. package/lib/operations/list_collections.js.map +1 -1
  49. package/lib/operations/run_command.js +1 -1
  50. package/lib/operations/run_command.js.map +1 -1
  51. package/lib/operations/update.js +2 -1
  52. package/lib/operations/update.js.map +1 -1
  53. package/lib/sdam/server.js +7 -2
  54. package/lib/sdam/server.js.map +1 -1
  55. package/lib/utils.js +45 -1
  56. package/lib/utils.js.map +1 -1
  57. package/lib/write_concern.js +17 -1
  58. package/lib/write_concern.js.map +1 -1
  59. package/mongodb.d.ts +17 -8
  60. package/package.json +1 -1
  61. package/src/bson.ts +1 -0
  62. package/src/client-side-encryption/auto_encrypter.ts +9 -70
  63. package/src/client-side-encryption/client_encryption.ts +6 -6
  64. package/src/client-side-encryption/state_machine.ts +18 -16
  65. package/src/cmap/connection.ts +46 -50
  66. package/src/cmap/wire_protocol/on_demand/document.ts +13 -6
  67. package/src/cmap/wire_protocol/responses.ts +140 -45
  68. package/src/constants.ts +9 -0
  69. package/src/cursor/abstract_cursor.ts +51 -71
  70. package/src/cursor/aggregation_cursor.ts +13 -12
  71. package/src/cursor/change_stream_cursor.ts +20 -34
  72. package/src/cursor/find_cursor.ts +17 -25
  73. package/src/cursor/list_collections_cursor.ts +3 -4
  74. package/src/cursor/list_indexes_cursor.ts +3 -4
  75. package/src/cursor/run_command_cursor.ts +13 -19
  76. package/src/error.ts +16 -28
  77. package/src/index.ts +6 -7
  78. package/src/operations/aggregate.ts +12 -5
  79. package/src/operations/bulk_write.ts +1 -2
  80. package/src/operations/command.ts +17 -3
  81. package/src/operations/count_documents.ts +7 -11
  82. package/src/operations/delete.ts +2 -2
  83. package/src/operations/execute_operation.ts +0 -13
  84. package/src/operations/find.ts +7 -3
  85. package/src/operations/find_and_modify.ts +1 -1
  86. package/src/operations/get_more.ts +6 -10
  87. package/src/operations/indexes.ts +7 -3
  88. package/src/operations/list_collections.ts +8 -3
  89. package/src/operations/run_command.ts +16 -6
  90. package/src/operations/update.ts +2 -1
  91. package/src/sdam/server.ts +7 -2
  92. package/src/utils.ts +52 -2
  93. package/src/write_concern.ts +18 -0
@@ -5,7 +5,7 @@ import type {
5
5
  MongoCryptOptions
6
6
  } from 'mongodb-client-encryption';
7
7
 
8
- import { type Binary, type Document, type Long, serialize, type UUID } from '../bson';
8
+ import { type Binary, deserialize, type Document, type Long, serialize, type UUID } from '../bson';
9
9
  import { type AnyBulkWriteOperation, type BulkWriteResult } from '../bulk/common';
10
10
  import { type ProxyOptions } from '../cmap/connection';
11
11
  import { type Collection } from '../collection';
@@ -202,7 +202,7 @@ export class ClientEncryption {
202
202
  tlsOptions: this._tlsOptions
203
203
  });
204
204
 
205
- const dataKey = await stateMachine.execute<DataKey>(this, context);
205
+ const dataKey = deserialize(await stateMachine.execute(this, context)) as DataKey;
206
206
 
207
207
  const { db: dbName, collection: collectionName } = MongoDBCollectionNamespace.fromString(
208
208
  this._keyVaultNamespace
@@ -259,7 +259,7 @@ export class ClientEncryption {
259
259
  tlsOptions: this._tlsOptions
260
260
  });
261
261
 
262
- const { v: dataKeys } = await stateMachine.execute<{ v: DataKey[] }>(this, context);
262
+ const { v: dataKeys } = deserialize(await stateMachine.execute(this, context));
263
263
  if (dataKeys.length === 0) {
264
264
  return {};
265
265
  }
@@ -640,7 +640,7 @@ export class ClientEncryption {
640
640
  tlsOptions: this._tlsOptions
641
641
  });
642
642
 
643
- const { v } = await stateMachine.execute<{ v: T }>(this, context);
643
+ const { v } = deserialize(await stateMachine.execute(this, context));
644
644
 
645
645
  return v;
646
646
  }
@@ -719,8 +719,8 @@ export class ClientEncryption {
719
719
  });
720
720
  const context = this._mongoCrypt.makeExplicitEncryptionContext(valueBuffer, contextOptions);
721
721
 
722
- const result = await stateMachine.execute<{ v: Binary }>(this, context);
723
- return result.v;
722
+ const { v } = deserialize(await stateMachine.execute(this, context));
723
+ return v;
724
724
  }
725
725
  }
726
726
 
@@ -114,6 +114,19 @@ export type CSFLEKMSTlsOptions = {
114
114
  [key: string]: ClientEncryptionTlsOptions | undefined;
115
115
  };
116
116
 
117
+ /**
118
+ * This is kind of a hack. For `rewrapManyDataKey`, we have tests that
119
+ * guarantee that when there are no matching keys, `rewrapManyDataKey` returns
120
+ * nothing. We also have tests for auto encryption that guarantee for `encrypt`
121
+ * we return an error when there are no matching keys. This error is generated in
122
+ * subsequent iterations of the state machine.
123
+ * Some apis (`encrypt`) throw if there are no filter matches and others (`rewrapManyDataKey`)
124
+ * do not. We set the result manually here, and let the state machine continue. `libmongocrypt`
125
+ * will inform us if we need to error by setting the state to `MONGOCRYPT_CTX_ERROR` but
126
+ * otherwise we'll return `{ v: [] }`.
127
+ */
128
+ let EMPTY_V;
129
+
117
130
  /**
118
131
  * @internal
119
132
  *
@@ -156,16 +169,13 @@ export class StateMachine {
156
169
  /**
157
170
  * Executes the state machine according to the specification
158
171
  */
159
- async execute<T extends Document>(
160
- executor: StateMachineExecutable,
161
- context: MongoCryptContext
162
- ): Promise<T> {
172
+ async execute(executor: StateMachineExecutable, context: MongoCryptContext): Promise<Uint8Array> {
163
173
  const keyVaultNamespace = executor._keyVaultNamespace;
164
174
  const keyVaultClient = executor._keyVaultClient;
165
175
  const metaDataClient = executor._metaDataClient;
166
176
  const mongocryptdClient = executor._mongocryptdClient;
167
177
  const mongocryptdManager = executor._mongocryptdManager;
168
- let result: T | null = null;
178
+ let result: Uint8Array | null = null;
169
179
 
170
180
  while (context.state !== MONGOCRYPT_CTX_DONE && context.state !== MONGOCRYPT_CTX_ERROR) {
171
181
  debug(`[context#${context.id}] ${stateToString.get(context.state) || context.state}`);
@@ -213,16 +223,8 @@ export class StateMachine {
213
223
  const keys = await this.fetchKeys(keyVaultClient, keyVaultNamespace, filter);
214
224
 
215
225
  if (keys.length === 0) {
216
- // This is kind of a hack. For `rewrapManyDataKey`, we have tests that
217
- // guarantee that when there are no matching keys, `rewrapManyDataKey` returns
218
- // nothing. We also have tests for auto encryption that guarantee for `encrypt`
219
- // we return an error when there are no matching keys. This error is generated in
220
- // subsequent iterations of the state machine.
221
- // Some apis (`encrypt`) throw if there are no filter matches and others (`rewrapManyDataKey`)
222
- // do not. We set the result manually here, and let the state machine continue. `libmongocrypt`
223
- // will inform us if we need to error by setting the state to `MONGOCRYPT_CTX_ERROR` but
224
- // otherwise we'll return `{ v: [] }`.
225
- result = { v: [] } as any as T;
226
+ // See docs on EMPTY_V
227
+ result = EMPTY_V ??= serialize({ v: [] });
226
228
  }
227
229
  for await (const key of keys) {
228
230
  context.addMongoOperationResponse(serialize(key));
@@ -254,7 +256,7 @@ export class StateMachine {
254
256
  const message = context.status.message || 'Finalization error';
255
257
  throw new MongoCryptError(message);
256
258
  }
257
- result = deserialize(finalizedContext, this.options) as T;
259
+ result = finalizedContext;
258
260
  break;
259
261
  }
260
262
 
@@ -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
  }
@@ -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/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');