Skip to content

Commit 3ed6a2a

Browse files
feat(NODE-6136): parse cursor responses on demand (#4112)
Co-authored-by: Bailey Pearson <[email protected]>
1 parent 0c687a5 commit 3ed6a2a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+767
-721
lines changed

Diff for: src/bson.ts

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export {
2727
UUID
2828
} from 'bson';
2929

30+
/** @internal */
3031
export type BSONElement = BSON.OnDemand['BSONElement'];
3132

3233
export function parseToElementsToArray(bytes: Uint8Array, offset?: number): BSONElement[] {

Diff for: src/client-side-encryption/auto_encrypter.ts

+9-70
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66

77
import { deserialize, type Document, serialize } from '../bson';
88
import { type CommandOptions, type ProxyOptions } from '../cmap/connection';
9+
import { kDecorateResult } from '../constants';
910
import { getMongoDBClientEncryption } from '../deps';
1011
import { MongoRuntimeError } from '../error';
1112
import { MongoClient, type MongoClientOptions } from '../mongo_client';
@@ -212,15 +213,6 @@ export const AutoEncryptionLoggerLevel = Object.freeze({
212213
export type AutoEncryptionLoggerLevel =
213214
(typeof AutoEncryptionLoggerLevel)[keyof typeof AutoEncryptionLoggerLevel];
214215

215-
// Typescript errors if we index objects with `Symbol.for(...)`, so
216-
// to avoid TS errors we pull them out into variables. Then we can type
217-
// the objects (and class) that we expect to see them on and prevent TS
218-
// errors.
219-
/** @internal */
220-
const kDecorateResult = Symbol.for('@@mdb.decorateDecryptionResult');
221-
/** @internal */
222-
const kDecoratedKeys = Symbol.for('@@mdb.decryptedKeys');
223-
224216
/**
225217
* @internal An internal class to be used by the driver for auto encryption
226218
* **NOTE**: Not meant to be instantiated directly, this is for internal use only.
@@ -467,16 +459,18 @@ export class AutoEncrypter {
467459
proxyOptions: this._proxyOptions,
468460
tlsOptions: this._tlsOptions
469461
});
470-
return await stateMachine.execute<Document>(this, context);
462+
463+
return deserialize(await stateMachine.execute(this, context), {
464+
promoteValues: false,
465+
promoteLongs: false
466+
});
471467
}
472468

473469
/**
474470
* Decrypt a command response
475471
*/
476-
async decrypt(response: Uint8Array | Document, options: CommandOptions = {}): Promise<Document> {
477-
const buffer = Buffer.isBuffer(response) ? response : serialize(response, options);
478-
479-
const context = this._mongocrypt.makeDecryptionContext(buffer);
472+
async decrypt(response: Uint8Array, options: CommandOptions = {}): Promise<Uint8Array> {
473+
const context = this._mongocrypt.makeDecryptionContext(response);
480474

481475
context.id = this._contextCounter++;
482476

@@ -486,12 +480,7 @@ export class AutoEncrypter {
486480
tlsOptions: this._tlsOptions
487481
});
488482

489-
const decorateResult = this[kDecorateResult];
490-
const result = await stateMachine.execute<Document>(this, context);
491-
if (decorateResult) {
492-
decorateDecryptionResult(result, response);
493-
}
494-
return result;
483+
return await stateMachine.execute(this, context);
495484
}
496485

497486
/**
@@ -518,53 +507,3 @@ export class AutoEncrypter {
518507
return AutoEncrypter.getMongoCrypt().libmongocryptVersion;
519508
}
520509
}
521-
522-
/**
523-
* Recurse through the (identically-shaped) `decrypted` and `original`
524-
* objects and attach a `decryptedKeys` property on each sub-object that
525-
* contained encrypted fields. Because we only call this on BSON responses,
526-
* we do not need to worry about circular references.
527-
*
528-
* @internal
529-
*/
530-
function decorateDecryptionResult(
531-
decrypted: Document & { [kDecoratedKeys]?: Array<string> },
532-
original: Document,
533-
isTopLevelDecorateCall = true
534-
): void {
535-
if (isTopLevelDecorateCall) {
536-
// The original value could have been either a JS object or a BSON buffer
537-
if (Buffer.isBuffer(original)) {
538-
original = deserialize(original);
539-
}
540-
if (Buffer.isBuffer(decrypted)) {
541-
throw new MongoRuntimeError('Expected result of decryption to be deserialized BSON object');
542-
}
543-
}
544-
545-
if (!decrypted || typeof decrypted !== 'object') return;
546-
for (const k of Object.keys(decrypted)) {
547-
const originalValue = original[k];
548-
549-
// An object was decrypted by libmongocrypt if and only if it was
550-
// a BSON Binary object with subtype 6.
551-
if (originalValue && originalValue._bsontype === 'Binary' && originalValue.sub_type === 6) {
552-
if (!decrypted[kDecoratedKeys]) {
553-
Object.defineProperty(decrypted, kDecoratedKeys, {
554-
value: [],
555-
configurable: true,
556-
enumerable: false,
557-
writable: false
558-
});
559-
}
560-
// this is defined in the preceding if-statement
561-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
562-
decrypted[kDecoratedKeys]!.push(k);
563-
// Do not recurse into this decrypted value. It could be a sub-document/array,
564-
// in which case there is no original value associated with its subfields.
565-
continue;
566-
}
567-
568-
decorateDecryptionResult(decrypted[k], originalValue, false);
569-
}
570-
}

Diff for: src/client-side-encryption/client_encryption.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type {
55
MongoCryptOptions
66
} from 'mongodb-client-encryption';
77

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';
99
import { type AnyBulkWriteOperation, type BulkWriteResult } from '../bulk/common';
1010
import { type ProxyOptions } from '../cmap/connection';
1111
import { type Collection } from '../collection';
@@ -202,7 +202,7 @@ export class ClientEncryption {
202202
tlsOptions: this._tlsOptions
203203
});
204204

205-
const dataKey = await stateMachine.execute<DataKey>(this, context);
205+
const dataKey = deserialize(await stateMachine.execute(this, context)) as DataKey;
206206

207207
const { db: dbName, collection: collectionName } = MongoDBCollectionNamespace.fromString(
208208
this._keyVaultNamespace
@@ -259,7 +259,7 @@ export class ClientEncryption {
259259
tlsOptions: this._tlsOptions
260260
});
261261

262-
const { v: dataKeys } = await stateMachine.execute<{ v: DataKey[] }>(this, context);
262+
const { v: dataKeys } = deserialize(await stateMachine.execute(this, context));
263263
if (dataKeys.length === 0) {
264264
return {};
265265
}
@@ -640,7 +640,7 @@ export class ClientEncryption {
640640
tlsOptions: this._tlsOptions
641641
});
642642

643-
const { v } = await stateMachine.execute<{ v: T }>(this, context);
643+
const { v } = deserialize(await stateMachine.execute(this, context));
644644

645645
return v;
646646
}
@@ -719,8 +719,8 @@ export class ClientEncryption {
719719
});
720720
const context = this._mongoCrypt.makeExplicitEncryptionContext(valueBuffer, contextOptions);
721721

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;
724724
}
725725
}
726726

Diff for: src/client-side-encryption/state_machine.ts

+18-16
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,19 @@ export type CSFLEKMSTlsOptions = {
114114
[key: string]: ClientEncryptionTlsOptions | undefined;
115115
};
116116

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+
117130
/**
118131
* @internal
119132
*
@@ -156,16 +169,13 @@ export class StateMachine {
156169
/**
157170
* Executes the state machine according to the specification
158171
*/
159-
async execute<T extends Document>(
160-
executor: StateMachineExecutable,
161-
context: MongoCryptContext
162-
): Promise<T> {
172+
async execute(executor: StateMachineExecutable, context: MongoCryptContext): Promise<Uint8Array> {
163173
const keyVaultNamespace = executor._keyVaultNamespace;
164174
const keyVaultClient = executor._keyVaultClient;
165175
const metaDataClient = executor._metaDataClient;
166176
const mongocryptdClient = executor._mongocryptdClient;
167177
const mongocryptdManager = executor._mongocryptdManager;
168-
let result: T | null = null;
178+
let result: Uint8Array | null = null;
169179

170180
while (context.state !== MONGOCRYPT_CTX_DONE && context.state !== MONGOCRYPT_CTX_ERROR) {
171181
debug(`[context#${context.id}] ${stateToString.get(context.state) || context.state}`);
@@ -213,16 +223,8 @@ export class StateMachine {
213223
const keys = await this.fetchKeys(keyVaultClient, keyVaultNamespace, filter);
214224

215225
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: [] });
226228
}
227229
for await (const key of keys) {
228230
context.addMongoOperationResponse(serialize(key));
@@ -254,7 +256,7 @@ export class StateMachine {
254256
const message = context.status.message || 'Finalization error';
255257
throw new MongoCryptError(message);
256258
}
257-
result = deserialize(finalizedContext, this.options) as T;
259+
result = finalizedContext;
258260
break;
259261
}
260262

0 commit comments

Comments
 (0)