mongodb 6.5.0-dev.20240406.sha.62ea94b → 6.5.0-dev.20240409.sha.30cac05

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.
@@ -37,7 +37,6 @@ export type WriteProtocolMessageType = OpQueryRequest | OpMsgRequest;
37
37
  export interface OpQueryOptions extends CommandOptions {
38
38
  socketTimeoutMS?: number;
39
39
  session?: ClientSession;
40
- documentsReturnedIn?: string;
41
40
  numberToSkip?: number;
42
41
  numberToReturn?: number;
43
42
  returnFieldSelector?: Document;
@@ -53,9 +52,6 @@ export interface OpQueryOptions extends CommandOptions {
53
52
  exhaustAllowed?: boolean;
54
53
  }
55
54
 
56
- /**************************************************************
57
- * QUERY
58
- **************************************************************/
59
55
  /** @internal */
60
56
  export class OpQueryRequest {
61
57
  ns: string;
@@ -284,16 +280,11 @@ export interface MessageHeader {
284
280
  }
285
281
 
286
282
  /** @internal */
287
- export interface OpResponseOptions extends BSONSerializeOptions {
288
- documentsReturnedIn?: string | null;
289
- }
290
-
291
- /** @internal */
292
- export class OpQueryResponse {
283
+ export class OpReply {
293
284
  parsed: boolean;
294
285
  raw: Buffer;
295
286
  data: Buffer;
296
- opts: OpResponseOptions;
287
+ opts: BSONSerializeOptions;
297
288
  length: number;
298
289
  requestId: number;
299
290
  responseTo: number;
@@ -303,7 +294,6 @@ export class OpQueryResponse {
303
294
  cursorId?: Long;
304
295
  startingFrom?: number;
305
296
  numberReturned?: number;
306
- documents: (Document | Buffer)[] = new Array(0);
307
297
  cursorNotFound?: boolean;
308
298
  queryFailure?: boolean;
309
299
  shardConfigStale?: boolean;
@@ -313,7 +303,8 @@ export class OpQueryResponse {
313
303
  promoteValues: boolean;
314
304
  promoteBuffers: boolean;
315
305
  bsonRegExp?: boolean;
316
- index?: number;
306
+ index = 0;
307
+ sections: Uint8Array[] = [];
317
308
 
318
309
  /** moreToCome is an OP_MSG only concept */
319
310
  moreToCome = false;
@@ -322,7 +313,7 @@ export class OpQueryResponse {
322
313
  message: Buffer,
323
314
  msgHeader: MessageHeader,
324
315
  msgBody: Buffer,
325
- opts?: OpResponseOptions
316
+ opts?: BSONSerializeOptions
326
317
  ) {
327
318
  this.parsed = false;
328
319
  this.raw = message;
@@ -356,29 +347,9 @@ export class OpQueryResponse {
356
347
  return this.parsed;
357
348
  }
358
349
 
359
- parse(options: OpResponseOptions): void {
350
+ parse(): Uint8Array {
360
351
  // Don't parse again if not needed
361
- if (this.parsed) return;
362
- options = options ?? {};
363
-
364
- // Allow the return of raw documents instead of parsing
365
- const raw = options.raw || false;
366
- const documentsReturnedIn = options.documentsReturnedIn || null;
367
- const useBigInt64 = options.useBigInt64 ?? this.opts.useBigInt64;
368
- const promoteLongs = options.promoteLongs ?? this.opts.promoteLongs;
369
- const promoteValues = options.promoteValues ?? this.opts.promoteValues;
370
- const promoteBuffers = options.promoteBuffers ?? this.opts.promoteBuffers;
371
- const bsonRegExp = options.bsonRegExp ?? this.opts.bsonRegExp;
372
- let bsonSize;
373
-
374
- // Set up the options
375
- const _options: BSONSerializeOptions = {
376
- useBigInt64,
377
- promoteLongs,
378
- promoteValues,
379
- promoteBuffers,
380
- bsonRegExp
381
- };
352
+ if (this.parsed) return this.sections[0];
382
353
 
383
354
  // Position within OP_REPLY at which documents start
384
355
  // (See https://www.mongodb.com/docs/manual/reference/mongodb-wire-protocol/#wire-op-reply)
@@ -390,8 +361,11 @@ export class OpQueryResponse {
390
361
  this.startingFrom = this.data.readInt32LE(12);
391
362
  this.numberReturned = this.data.readInt32LE(16);
392
363
 
393
- // Preallocate document array
394
- this.documents = new Array(this.numberReturned);
364
+ if (this.numberReturned < 0 || this.numberReturned > 2 ** 32 - 1) {
365
+ throw new RangeError(
366
+ `OP_REPLY numberReturned is an invalid array length ${this.numberReturned}`
367
+ );
368
+ }
395
369
 
396
370
  this.cursorNotFound = (this.responseFlags & CURSOR_NOT_FOUND) !== 0;
397
371
  this.queryFailure = (this.responseFlags & QUERY_FAILURE) !== 0;
@@ -400,67 +374,26 @@ export class OpQueryResponse {
400
374
 
401
375
  // Parse Body
402
376
  for (let i = 0; i < this.numberReturned; i++) {
403
- bsonSize =
377
+ const bsonSize =
404
378
  this.data[this.index] |
405
379
  (this.data[this.index + 1] << 8) |
406
380
  (this.data[this.index + 2] << 16) |
407
381
  (this.data[this.index + 3] << 24);
408
382
 
409
- // If we have raw results specified slice the return document
410
- if (raw) {
411
- this.documents[i] = this.data.slice(this.index, this.index + bsonSize);
412
- } else {
413
- this.documents[i] = BSON.deserialize(
414
- this.data.slice(this.index, this.index + bsonSize),
415
- _options
416
- );
417
- }
383
+ const section = this.data.subarray(this.index, this.index + bsonSize);
384
+ this.sections.push(section);
418
385
 
419
386
  // Adjust the index
420
387
  this.index = this.index + bsonSize;
421
388
  }
422
389
 
423
- if (this.documents.length === 1 && documentsReturnedIn != null && raw) {
424
- const fieldsAsRaw: Document = {};
425
- fieldsAsRaw[documentsReturnedIn] = true;
426
- _options.fieldsAsRaw = fieldsAsRaw;
427
-
428
- const doc = BSON.deserialize(this.documents[0] as Buffer, _options);
429
- this.documents = [doc];
430
- }
431
-
432
390
  // Set parsed
433
391
  this.parsed = true;
392
+
393
+ return this.sections[0];
434
394
  }
435
395
  }
436
396
 
437
- // Implementation of OP_MSG spec:
438
- // https://github.com/mongodb/specifications/blob/master/source/message/OP_MSG.rst
439
- //
440
- // struct Section {
441
- // uint8 payloadType;
442
- // union payload {
443
- // document document; // payloadType == 0
444
- // struct sequence { // payloadType == 1
445
- // int32 size;
446
- // cstring identifier;
447
- // document* documents;
448
- // };
449
- // };
450
- // };
451
-
452
- // struct OP_MSG {
453
- // struct MsgHeader {
454
- // int32 messageLength;
455
- // int32 requestID;
456
- // int32 responseTo;
457
- // int32 opCode = 2013;
458
- // };
459
- // uint32 flagBits;
460
- // Section+ sections;
461
- // [uint32 checksum;]
462
- // };
463
-
464
397
  // Msg Flags
465
398
  const OPTS_CHECKSUM_PRESENT = 1;
466
399
  const OPTS_MORE_TO_COME = 2;
@@ -587,7 +520,7 @@ export class OpMsgResponse {
587
520
  parsed: boolean;
588
521
  raw: Buffer;
589
522
  data: Buffer;
590
- opts: OpResponseOptions;
523
+ opts: BSONSerializeOptions;
591
524
  length: number;
592
525
  requestId: number;
593
526
  responseTo: number;
@@ -603,14 +536,14 @@ export class OpMsgResponse {
603
536
  promoteValues: boolean;
604
537
  promoteBuffers: boolean;
605
538
  bsonRegExp: boolean;
606
- documents: (Document | Buffer)[];
607
- index?: number;
539
+ index = 0;
540
+ sections: Uint8Array[] = [];
608
541
 
609
542
  constructor(
610
543
  message: Buffer,
611
544
  msgHeader: MessageHeader,
612
545
  msgBody: Buffer,
613
- opts?: OpResponseOptions
546
+ opts?: BSONSerializeOptions
614
547
  ) {
615
548
  this.parsed = false;
616
549
  this.raw = message;
@@ -642,47 +575,26 @@ export class OpMsgResponse {
642
575
  this.promoteBuffers =
643
576
  typeof this.opts.promoteBuffers === 'boolean' ? this.opts.promoteBuffers : false;
644
577
  this.bsonRegExp = typeof this.opts.bsonRegExp === 'boolean' ? this.opts.bsonRegExp : false;
645
-
646
- this.documents = [];
647
578
  }
648
579
 
649
580
  isParsed(): boolean {
650
581
  return this.parsed;
651
582
  }
652
583
 
653
- parse(options: OpResponseOptions): void {
584
+ parse(): Uint8Array {
654
585
  // Don't parse again if not needed
655
- if (this.parsed) return;
656
- options = options ?? {};
586
+ if (this.parsed) return this.sections[0];
657
587
 
658
588
  this.index = 4;
659
- // Allow the return of raw documents instead of parsing
660
- const raw = options.raw || false;
661
- const documentsReturnedIn = options.documentsReturnedIn || null;
662
- const useBigInt64 = options.useBigInt64 ?? this.opts.useBigInt64;
663
- const promoteLongs = options.promoteLongs ?? this.opts.promoteLongs;
664
- const promoteValues = options.promoteValues ?? this.opts.promoteValues;
665
- const promoteBuffers = options.promoteBuffers ?? this.opts.promoteBuffers;
666
- const bsonRegExp = options.bsonRegExp ?? this.opts.bsonRegExp;
667
- const validation = this.parseBsonSerializationOptions(options);
668
-
669
- // Set up the options
670
- const bsonOptions: BSONSerializeOptions = {
671
- useBigInt64,
672
- promoteLongs,
673
- promoteValues,
674
- promoteBuffers,
675
- bsonRegExp,
676
- validation
677
- // Due to the strictness of the BSON libraries validation option we need this cast
678
- } as BSONSerializeOptions & { validation: { utf8: { writeErrors: boolean } } };
679
589
 
680
590
  while (this.index < this.data.length) {
681
591
  const payloadType = this.data.readUInt8(this.index++);
682
592
  if (payloadType === 0) {
683
593
  const bsonSize = this.data.readUInt32LE(this.index);
684
- const bin = this.data.slice(this.index, this.index + bsonSize);
685
- this.documents.push(raw ? bin : BSON.deserialize(bin, bsonOptions));
594
+ const bin = this.data.subarray(this.index, this.index + bsonSize);
595
+
596
+ this.sections.push(bin);
597
+
686
598
  this.index += bsonSize;
687
599
  } else if (payloadType === 1) {
688
600
  // It was decided that no driver makes use of payload type 1
@@ -692,25 +604,9 @@ export class OpMsgResponse {
692
604
  }
693
605
  }
694
606
 
695
- if (this.documents.length === 1 && documentsReturnedIn != null && raw) {
696
- const fieldsAsRaw: Document = {};
697
- fieldsAsRaw[documentsReturnedIn] = true;
698
- bsonOptions.fieldsAsRaw = fieldsAsRaw;
699
- const doc = BSON.deserialize(this.documents[0] as Buffer, bsonOptions);
700
- this.documents = [doc];
701
- }
702
-
703
607
  this.parsed = true;
704
- }
705
-
706
- parseBsonSerializationOptions({ enableUtf8Validation }: BSONSerializeOptions): {
707
- utf8: { writeErrors: false } | false;
708
- } {
709
- if (enableUtf8Validation === false) {
710
- return { utf8: false };
711
- }
712
608
 
713
- return { utf8: { writeErrors: false } };
609
+ return this.sections[0];
714
610
  }
715
611
  }
716
612
 
@@ -103,7 +103,7 @@ export async function performInitialHandshake(
103
103
  const handshakeDoc = await prepareHandshakeDocument(authContext);
104
104
 
105
105
  // @ts-expect-error: TODO(NODE-5141): The options need to be filtered properly, Connection options differ from Command options
106
- const handshakeOptions: CommandOptions = { ...options };
106
+ const handshakeOptions: CommandOptions = { ...options, raw: false };
107
107
  if (typeof options.connectTimeoutMS === 'number') {
108
108
  // The handshake technically is a monitoring check, so its socket timeout should be connectTimeoutMS
109
109
  handshakeOptions.socketTimeoutMS = options.connectTimeoutMS;
@@ -54,7 +54,7 @@ import {
54
54
  OpMsgRequest,
55
55
  type OpMsgResponse,
56
56
  OpQueryRequest,
57
- type OpQueryResponse,
57
+ type OpReply,
58
58
  type WriteProtocolMessageType
59
59
  } from './commands';
60
60
  import type { Stream } from './connect';
@@ -62,6 +62,7 @@ import type { ClientMetadata } from './handshake/client_metadata';
62
62
  import { StreamDescription, type StreamDescriptionOptions } from './stream_description';
63
63
  import { type CompressorName, decompressResponse } from './wire_protocol/compression';
64
64
  import { onData } from './wire_protocol/on_data';
65
+ import { MongoDBResponse, type MongoDBResponseConstructor } from './wire_protocol/responses';
65
66
  import { getReadPreference, isSharded } from './wire_protocol/shared';
66
67
 
67
68
  /** @internal */
@@ -412,7 +413,11 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
412
413
  return message;
413
414
  }
414
415
 
415
- private async *sendWire(message: WriteProtocolMessageType, options: CommandOptions) {
416
+ private async *sendWire(
417
+ message: WriteProtocolMessageType,
418
+ options: CommandOptions,
419
+ responseType?: MongoDBResponseConstructor
420
+ ): AsyncGenerator<MongoDBResponse> {
416
421
  this.throwIfAborted();
417
422
 
418
423
  if (typeof options.socketTimeoutMS === 'number') {
@@ -428,7 +433,7 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
428
433
  });
429
434
 
430
435
  if (options.noResponse) {
431
- yield { ok: 1 };
436
+ yield MongoDBResponse.empty;
432
437
  return;
433
438
  }
434
439
 
@@ -436,21 +441,9 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
436
441
 
437
442
  for await (const response of this.readMany()) {
438
443
  this.socket.setTimeout(0);
439
- response.parse(options);
440
-
441
- const [document] = response.documents;
444
+ const bson = response.parse();
442
445
 
443
- if (!Buffer.isBuffer(document)) {
444
- const { session } = options;
445
- if (session) {
446
- updateSessionFromResponse(session, document);
447
- }
448
-
449
- if (document.$clusterTime) {
450
- this.clusterTime = document.$clusterTime;
451
- this.emit(Connection.CLUSTER_TIME_RECEIVED, document.$clusterTime);
452
- }
453
- }
446
+ const document = new (responseType ?? MongoDBResponse)(bson, 0, false);
454
447
 
455
448
  yield document;
456
449
  this.throwIfAborted();
@@ -469,7 +462,8 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
469
462
  private async *sendCommand(
470
463
  ns: MongoDBNamespace,
471
464
  command: Document,
472
- options: CommandOptions = {}
465
+ options: CommandOptions,
466
+ responseType?: MongoDBResponseConstructor
473
467
  ) {
474
468
  const message = this.prepareCommand(ns.db, command, options);
475
469
 
@@ -485,19 +479,41 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
485
479
  );
486
480
  }
487
481
 
488
- let document;
482
+ // If `documentsReturnedIn` not set or raw is not enabled, use input bson options
483
+ // Otherwise, support raw flag. Raw only works for cursors that hardcode firstBatch/nextBatch fields
484
+ const bsonOptions =
485
+ options.documentsReturnedIn == null || !options.raw
486
+ ? options
487
+ : {
488
+ ...options,
489
+ raw: false,
490
+ fieldsAsRaw: { [options.documentsReturnedIn]: true }
491
+ };
492
+
493
+ /** MongoDBResponse instance or subclass */
494
+ let document: MongoDBResponse | undefined = undefined;
495
+ /** Cached result of a toObject call */
496
+ let object: Document | undefined = undefined;
489
497
  try {
490
498
  this.throwIfAborted();
491
- for await (document of this.sendWire(message, options)) {
492
- if (!Buffer.isBuffer(document) && document.writeConcernError) {
493
- throw new MongoWriteConcernError(document.writeConcernError, document);
499
+ for await (document of this.sendWire(message, options, responseType)) {
500
+ object = undefined;
501
+ if (options.session != null) {
502
+ updateSessionFromResponse(options.session, document);
503
+ }
504
+
505
+ if (document.$clusterTime) {
506
+ this.clusterTime = document.$clusterTime;
507
+ this.emit(Connection.CLUSTER_TIME_RECEIVED, document.$clusterTime);
494
508
  }
495
509
 
496
- if (
497
- !Buffer.isBuffer(document) &&
498
- (document.ok === 0 || document.$err || document.errmsg || document.code)
499
- ) {
500
- throw new MongoServerError(document);
510
+ if (document.has('writeConcernError')) {
511
+ object ??= document.toObject(bsonOptions);
512
+ throw new MongoWriteConcernError(object.writeConcernError, object);
513
+ }
514
+
515
+ if (document.isError) {
516
+ throw new MongoServerError((object ??= document.toObject(bsonOptions)));
501
517
  }
502
518
 
503
519
  if (this.shouldEmitAndLogCommand) {
@@ -509,14 +525,19 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
509
525
  new CommandSucceededEvent(
510
526
  this,
511
527
  message,
512
- options.noResponse ? undefined : document,
528
+ options.noResponse ? undefined : (object ??= document.toObject(bsonOptions)),
513
529
  started,
514
530
  this.description.serverConnectionId
515
531
  )
516
532
  );
517
533
  }
518
534
 
519
- yield document;
535
+ if (responseType == null) {
536
+ yield (object ??= document.toObject(bsonOptions));
537
+ } else {
538
+ yield document;
539
+ }
540
+
520
541
  this.throwIfAborted();
521
542
  }
522
543
  } catch (error) {
@@ -530,7 +551,7 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
530
551
  new CommandSucceededEvent(
531
552
  this,
532
553
  message,
533
- options.noResponse ? undefined : document,
554
+ options.noResponse ? undefined : (object ??= document?.toObject(bsonOptions)),
534
555
  started,
535
556
  this.description.serverConnectionId
536
557
  )
@@ -555,13 +576,27 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
555
576
  }
556
577
  }
557
578
 
579
+ public async command<T extends MongoDBResponseConstructor>(
580
+ ns: MongoDBNamespace,
581
+ command: Document,
582
+ options: CommandOptions | undefined,
583
+ responseType: T | undefined
584
+ ): Promise<typeof responseType extends undefined ? Document : InstanceType<T>>;
585
+
586
+ public async command(
587
+ ns: MongoDBNamespace,
588
+ command: Document,
589
+ options?: CommandOptions
590
+ ): Promise<Document>;
591
+
558
592
  public async command(
559
593
  ns: MongoDBNamespace,
560
594
  command: Document,
561
- options: CommandOptions = {}
595
+ options: CommandOptions = {},
596
+ responseType?: MongoDBResponseConstructor
562
597
  ): Promise<Document> {
563
598
  this.throwIfAborted();
564
- for await (const document of this.sendCommand(ns, command, options)) {
599
+ for await (const document of this.sendCommand(ns, command, options, responseType)) {
565
600
  return document;
566
601
  }
567
602
  throw new MongoUnexpectedServerResponseError('Unable to get response from server');
@@ -622,7 +657,7 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
622
657
  *
623
658
  * Note that `for-await` loops call `return` automatically when the loop is exited.
624
659
  */
625
- private async *readMany(): AsyncGenerator<OpMsgResponse | OpQueryResponse> {
660
+ private async *readMany(): AsyncGenerator<OpMsgResponse | OpReply> {
626
661
  try {
627
662
  this.dataEvents = onData(this.messageStream);
628
663
  for await (const message of this.dataEvents) {
@@ -687,11 +722,24 @@ export class CryptoConnection extends Connection {
687
722
  this.autoEncrypter = options.autoEncrypter;
688
723
  }
689
724
 
690
- /** @internal @override */
691
- override async command(
725
+ public override async command<T extends MongoDBResponseConstructor>(
726
+ ns: MongoDBNamespace,
727
+ command: Document,
728
+ options: CommandOptions | undefined,
729
+ responseType: T
730
+ ): Promise<InstanceType<T>>;
731
+
732
+ public override async command(
733
+ ns: MongoDBNamespace,
734
+ command: Document,
735
+ options?: CommandOptions
736
+ ): Promise<Document>;
737
+
738
+ override async command<T extends MongoDBResponseConstructor>(
692
739
  ns: MongoDBNamespace,
693
740
  cmd: Document,
694
- options: CommandOptions
741
+ options?: CommandOptions,
742
+ responseType?: T | undefined
695
743
  ): Promise<Document> {
696
744
  const { autoEncrypter } = this;
697
745
  if (!autoEncrypter) {
@@ -705,7 +753,7 @@ export class CryptoConnection extends Connection {
705
753
  const serverWireVersion = maxWireVersion(this);
706
754
  if (serverWireVersion === 0) {
707
755
  // This means the initial handshake hasn't happened yet
708
- return await super.command(ns, cmd, options);
756
+ return await super.command<T>(ns, cmd, options, responseType);
709
757
  }
710
758
 
711
759
  if (serverWireVersion < 8) {
@@ -739,7 +787,7 @@ export class CryptoConnection extends Connection {
739
787
  }
740
788
  }
741
789
 
742
- const response = await super.command(ns, encrypted, options);
790
+ const response = await super.command<T>(ns, encrypted, options, responseType);
743
791
 
744
792
  return await autoEncrypter.decrypt(response, options);
745
793
  }
@@ -8,7 +8,7 @@ import {
8
8
  type MessageHeader,
9
9
  OpCompressedRequest,
10
10
  OpMsgResponse,
11
- OpQueryResponse,
11
+ OpReply,
12
12
  type WriteProtocolMessageType
13
13
  } from '../commands';
14
14
  import { OP_COMPRESSED, OP_MSG } from './constants';
@@ -163,9 +163,7 @@ export async function compressCommand(
163
163
  *
164
164
  * This method does not parse the response's BSON.
165
165
  */
166
- export async function decompressResponse(
167
- message: Buffer
168
- ): Promise<OpMsgResponse | OpQueryResponse> {
166
+ export async function decompressResponse(message: Buffer): Promise<OpMsgResponse | OpReply> {
169
167
  const messageHeader: MessageHeader = {
170
168
  length: message.readInt32LE(0),
171
169
  requestId: message.readInt32LE(4),
@@ -174,7 +172,7 @@ export async function decompressResponse(
174
172
  };
175
173
 
176
174
  if (messageHeader.opCode !== OP_COMPRESSED) {
177
- const ResponseType = messageHeader.opCode === OP_MSG ? OpMsgResponse : OpQueryResponse;
175
+ const ResponseType = messageHeader.opCode === OP_MSG ? OpMsgResponse : OpReply;
178
176
  const messageBody = message.subarray(MESSAGE_HEADER_SIZE);
179
177
  return new ResponseType(message, messageHeader, messageBody);
180
178
  }
@@ -189,7 +187,7 @@ export async function decompressResponse(
189
187
  const compressedBuffer = message.slice(MESSAGE_HEADER_SIZE + 9);
190
188
 
191
189
  // recalculate based on wrapped opcode
192
- const ResponseType = header.opCode === OP_MSG ? OpMsgResponse : OpQueryResponse;
190
+ const ResponseType = header.opCode === OP_MSG ? OpMsgResponse : OpReply;
193
191
  const messageBody = await decompress(compressorID, compressedBuffer);
194
192
  if (messageBody.length !== header.length) {
195
193
  throw new MongoDecompressionError('Message body and message header must be the same length');
@@ -23,6 +23,7 @@ const enum BSONElementOffset {
23
23
  length = 4
24
24
  }
25
25
 
26
+ /** @internal */
26
27
  export type JSTypeOf = {
27
28
  [BSONType.null]: null;
28
29
  [BSONType.undefined]: null;
@@ -0,0 +1,109 @@
1
+ import { type BSONSerializeOptions, BSONType, type Document, type Timestamp } from '../../bson';
2
+ import { type ClusterTime } from '../../sdam/common';
3
+ import { OnDemandDocument } from './on_demand/document';
4
+
5
+ /** @internal */
6
+ export type MongoDBResponseConstructor = {
7
+ new (bson: Uint8Array, offset?: number, isArray?: boolean): MongoDBResponse;
8
+ };
9
+
10
+ /** @internal */
11
+ export class MongoDBResponse extends OnDemandDocument {
12
+ // {ok:1}
13
+ static empty = new MongoDBResponse(new Uint8Array([13, 0, 0, 0, 16, 111, 107, 0, 1, 0, 0, 0, 0]));
14
+
15
+ /** Indicates this document is a server error */
16
+ public get isError() {
17
+ let isError = this.ok === 0;
18
+ isError ||= this.has('errmsg');
19
+ isError ||= this.has('code');
20
+ isError ||= this.has('$err'); // The '$err' field is used in OP_REPLY responses
21
+ return isError;
22
+ }
23
+
24
+ /**
25
+ * Drivers can safely assume that the `recoveryToken` field is always a BSON document but drivers MUST NOT modify the
26
+ * contents of the document.
27
+ */
28
+ get recoveryToken(): Document | null {
29
+ return (
30
+ this.get('recoveryToken', BSONType.object)?.toObject({
31
+ promoteValues: false,
32
+ promoteLongs: false,
33
+ promoteBuffers: false
34
+ }) ?? null
35
+ );
36
+ }
37
+
38
+ /**
39
+ * The server creates a cursor in response to a snapshot find/aggregate command and reports atClusterTime within the cursor field in the response.
40
+ * For the distinct command the server adds a top-level atClusterTime field to the response.
41
+ * The atClusterTime field represents the timestamp of the read and is guaranteed to be majority committed.
42
+ */
43
+ public get atClusterTime(): Timestamp | null {
44
+ return (
45
+ this.get('cursor', BSONType.object)?.get('atClusterTime', BSONType.timestamp) ??
46
+ this.get('atClusterTime', BSONType.timestamp)
47
+ );
48
+ }
49
+
50
+ public get operationTime(): Timestamp | null {
51
+ return this.get('operationTime', BSONType.timestamp);
52
+ }
53
+
54
+ public get ok(): 0 | 1 {
55
+ return this.getNumber('ok') ? 1 : 0;
56
+ }
57
+
58
+ public get $err(): string | null {
59
+ return this.get('$err', BSONType.string);
60
+ }
61
+
62
+ public get errmsg(): string | null {
63
+ return this.get('errmsg', BSONType.string);
64
+ }
65
+
66
+ public get code(): number | null {
67
+ return this.getNumber('code');
68
+ }
69
+
70
+ private clusterTime?: ClusterTime | null;
71
+ public get $clusterTime(): ClusterTime | null {
72
+ if (!('clusterTime' in this)) {
73
+ const clusterTimeDoc = this.get('$clusterTime', BSONType.object);
74
+ if (clusterTimeDoc == null) {
75
+ this.clusterTime = null;
76
+ return null;
77
+ }
78
+ const clusterTime = clusterTimeDoc.get('clusterTime', BSONType.timestamp, true);
79
+ const signature = clusterTimeDoc.get('signature', BSONType.object)?.toObject();
80
+ // @ts-expect-error: `signature` is incorrectly typed. It is public API.
81
+ this.clusterTime = { clusterTime, signature };
82
+ }
83
+ return this.clusterTime ?? null;
84
+ }
85
+
86
+ public override toObject(options: BSONSerializeOptions = {}): Record<string, any> {
87
+ const exactBSONOptions = {
88
+ useBigInt64: options.useBigInt64,
89
+ promoteLongs: options.promoteLongs,
90
+ promoteValues: options.promoteValues,
91
+ promoteBuffers: options.promoteBuffers,
92
+ bsonRegExp: options.bsonRegExp,
93
+ raw: options.raw ?? false,
94
+ fieldsAsRaw: options.fieldsAsRaw ?? {},
95
+ validation: this.parseBsonSerializationOptions(options)
96
+ };
97
+ return super.toObject(exactBSONOptions);
98
+ }
99
+
100
+ private parseBsonSerializationOptions({ enableUtf8Validation }: BSONSerializeOptions): {
101
+ utf8: { writeErrors: false } | false;
102
+ } {
103
+ if (enableUtf8Validation === false) {
104
+ return { utf8: false };
105
+ }
106
+
107
+ return { utf8: { writeErrors: false } };
108
+ }
109
+ }