Skip to content

Commit 965b21a

Browse files
authored
feat(NODE-6773): add support for $lookup with automatic encryption (#4427)
1 parent 488c407 commit 965b21a

22 files changed

+693
-86
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ test/lambda/env.json
9999

100100
# files generated by tooling in drivers-evergreen-tools
101101
secrets-export.sh
102+
secrets-export.fish
102103
mo-expansion.sh
103104
mo-expansion.yml
104105
expansions.sh

etc/bash_to_fish.mjs

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { createReadStream, promises as fs } from 'node:fs';
2+
import path from 'node:path';
3+
import readline from 'node:readline/promises';
4+
5+
/**
6+
* Takes an "exports" only bash script file
7+
* and converts it to fish syntax.
8+
* Will crash on any line that isn't:
9+
* - a comment
10+
* - an empty line
11+
* - a bash 'set' call
12+
* - export VAR=VAL
13+
*/
14+
15+
const fileName = process.argv[2];
16+
const outFileName = path.basename(fileName, '.sh') + '.fish';
17+
const input = createReadStream(process.argv[2]);
18+
const lines = readline.createInterface({ input });
19+
const output = await fs.open(outFileName, 'w');
20+
21+
for await (let line of lines) {
22+
line = line.trim();
23+
24+
if (!line.startsWith('export ')) {
25+
if (line.startsWith('#')) continue;
26+
if (line === '') continue;
27+
if (line.startsWith('set')) continue;
28+
throw new Error('Cannot translate: ' + line);
29+
}
30+
31+
const varVal = line.slice('export '.length);
32+
const variable = varVal.slice(0, varVal.indexOf('='));
33+
const value = varVal.slice(varVal.indexOf('=') + 1);
34+
await output.appendFile(`set -x ${variable} ${value}\n`);
35+
}
36+
37+
output.close();
38+
input.close();
39+
lines.close();

package-lock.json

+4-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@
9696
"js-yaml": "^4.1.0",
9797
"mocha": "^10.8.2",
9898
"mocha-sinon": "^2.1.2",
99-
"mongodb-client-encryption": "^6.2.0",
99+
"mongodb-client-encryption": "^6.3.0",
100100
"mongodb-legacy": "^6.1.3",
101101
"nyc": "^15.1.0",
102102
"prettier": "^3.4.2",

src/client-side-encryption/auto_encrypter.ts

+1
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ export class AutoEncrypter {
239239
this._kmsProviders = options.kmsProviders || {};
240240

241241
const mongoCryptOptions: MongoCryptOptions = {
242+
enableMultipleCollinfo: true,
242243
cryptoCallbacks
243244
};
244245
if (options.schemaMap) {

src/client-side-encryption/state_machine.ts

+31-22
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { getSocks, type SocksLib } from '../deps';
1616
import { MongoOperationTimeoutError } from '../error';
1717
import { type MongoClient, type MongoClientOptions } from '../mongo_client';
1818
import { type Abortable } from '../mongo_types';
19+
import { type CollectionInfo } from '../operations/list_collections';
1920
import { Timeout, type TimeoutContext, TimeoutError } from '../timeout';
2021
import {
2122
addAbortListener,
@@ -205,11 +206,19 @@ export class StateMachine {
205206
const mongocryptdManager = executor._mongocryptdManager;
206207
let result: Uint8Array | null = null;
207208

208-
while (context.state !== MONGOCRYPT_CTX_DONE && context.state !== MONGOCRYPT_CTX_ERROR) {
209+
// Typescript treats getters just like properties: Once you've tested it for equality
210+
// it cannot change. Which is exactly the opposite of what we use state and status for.
211+
// Every call to at least `addMongoOperationResponse` and `finalize` can change the state.
212+
// These wrappers let us write code more naturally and not add compiler exceptions
213+
// to conditions checks inside the state machine.
214+
const getStatus = () => context.status;
215+
const getState = () => context.state;
216+
217+
while (getState() !== MONGOCRYPT_CTX_DONE && getState() !== MONGOCRYPT_CTX_ERROR) {
209218
options.signal?.throwIfAborted();
210-
debug(`[context#${context.id}] ${stateToString.get(context.state) || context.state}`);
219+
debug(`[context#${context.id}] ${stateToString.get(getState()) || getState()}`);
211220

212-
switch (context.state) {
221+
switch (getState()) {
213222
case MONGOCRYPT_CTX_NEED_MONGO_COLLINFO: {
214223
const filter = deserialize(context.nextMongoOperation());
215224
if (!metaDataClient) {
@@ -218,22 +227,28 @@ export class StateMachine {
218227
);
219228
}
220229

221-
const collInfo = await this.fetchCollectionInfo(
230+
const collInfoCursor = this.fetchCollectionInfo(
222231
metaDataClient,
223232
context.ns,
224233
filter,
225234
options
226235
);
227-
if (collInfo) {
228-
context.addMongoOperationResponse(collInfo);
236+
237+
for await (const collInfo of collInfoCursor) {
238+
context.addMongoOperationResponse(serialize(collInfo));
239+
if (getState() === MONGOCRYPT_CTX_ERROR) break;
229240
}
230241

242+
if (getState() === MONGOCRYPT_CTX_ERROR) break;
243+
231244
context.finishMongoOperation();
232245
break;
233246
}
234247

235248
case MONGOCRYPT_CTX_NEED_MONGO_MARKINGS: {
236249
const command = context.nextMongoOperation();
250+
if (getState() === MONGOCRYPT_CTX_ERROR) break;
251+
237252
if (!mongocryptdClient) {
238253
throw new MongoCryptError(
239254
'unreachable state machine state: entered MONGOCRYPT_CTX_NEED_MONGO_MARKINGS but mongocryptdClient is undefined'
@@ -283,22 +298,21 @@ export class StateMachine {
283298

284299
case MONGOCRYPT_CTX_READY: {
285300
const finalizedContext = context.finalize();
286-
// @ts-expect-error finalize can change the state, check for error
287-
if (context.state === MONGOCRYPT_CTX_ERROR) {
288-
const message = context.status.message || 'Finalization error';
301+
if (getState() === MONGOCRYPT_CTX_ERROR) {
302+
const message = getStatus().message || 'Finalization error';
289303
throw new MongoCryptError(message);
290304
}
291305
result = finalizedContext;
292306
break;
293307
}
294308

295309
default:
296-
throw new MongoCryptError(`Unknown state: ${context.state}`);
310+
throw new MongoCryptError(`Unknown state: ${getState()}`);
297311
}
298312
}
299313

300-
if (context.state === MONGOCRYPT_CTX_ERROR || result == null) {
301-
const message = context.status.message;
314+
if (getState() === MONGOCRYPT_CTX_ERROR || result == null) {
315+
const message = getStatus().message;
302316
if (!message) {
303317
debug(
304318
`unidentifiable error in MongoCrypt - received an error status from \`libmongocrypt\` but received no error message.`
@@ -527,29 +541,24 @@ export class StateMachine {
527541
* @param filter - A filter for the listCollections command
528542
* @param callback - Invoked with the info of the requested collection, or with an error
529543
*/
530-
async fetchCollectionInfo(
544+
fetchCollectionInfo(
531545
client: MongoClient,
532546
ns: string,
533547
filter: Document,
534548
options?: { timeoutContext?: TimeoutContext } & Abortable
535-
): Promise<Uint8Array | null> {
549+
): AsyncIterable<CollectionInfo> {
536550
const { db } = MongoDBCollectionNamespace.fromString(ns);
537551

538552
const cursor = client.db(db).listCollections(filter, {
539553
promoteLongs: false,
540554
promoteValues: false,
541555
timeoutContext:
542556
options?.timeoutContext && new CursorTimeoutContext(options?.timeoutContext, Symbol()),
543-
signal: options?.signal
557+
signal: options?.signal,
558+
nameOnly: false
544559
});
545560

546-
// There is always exactly zero or one matching documents, so this should always exhaust the cursor
547-
// in a single batch. We call `toArray()` just to be safe and ensure that the cursor is always
548-
// exhausted and closed.
549-
const collections = await cursor.toArray();
550-
551-
const info = collections.length > 0 ? serialize(collections[0]) : null;
552-
return info;
561+
return cursor;
553562
}
554563

555564
/**

0 commit comments

Comments
 (0)