Skip to content

Commit 6d3947b

Browse files
authored
feat(NODE-4202): add FLE 2 behavior for create/drop collection (#3218)
1 parent c54df3f commit 6d3947b

File tree

8 files changed

+3493
-32
lines changed

8 files changed

+3493
-32
lines changed

src/encrypter.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,10 @@ export class Encrypter {
7272
if (internalClient == null) {
7373
const clonedOptions: MongoClientOptions = {};
7474

75-
for (const key of Object.keys(options)) {
75+
for (const key of [
76+
...Object.getOwnPropertyNames(options),
77+
...Object.getOwnPropertySymbols(options)
78+
] as string[]) {
7679
if (['autoEncryption', 'minPoolSize', 'servers', 'caseTranslate', 'dbName'].includes(key))
7780
continue;
7881
Reflect.set(clonedOptions, key, Reflect.get(options, key));

src/operations/create_collection.ts

Lines changed: 71 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { Server } from '../sdam/server';
66
import type { ClientSession } from '../sessions';
77
import type { Callback } from '../utils';
88
import { CommandOperation, CommandOperationOptions } from './command';
9+
import { CreateIndexOperation } from './indexes';
910
import { Aspect, defineAspects } from './operation';
1011

1112
const ILLEGAL_COMMAND_FIELDS = new Set([
@@ -75,6 +76,8 @@ export interface CreateCollectionOptions extends CommandOperationOptions {
7576
timeseries?: TimeSeriesCollectionOptions;
7677
/** The number of seconds after which a document in a timeseries collection expires. */
7778
expireAfterSeconds?: number;
79+
/** @experimental */
80+
encryptedFields?: Document;
7881
}
7982

8083
/** @internal */
@@ -96,31 +99,79 @@ export class CreateCollectionOperation extends CommandOperation<Collection> {
9699
session: ClientSession | undefined,
97100
callback: Callback<Collection>
98101
): void {
99-
const db = this.db;
100-
const name = this.name;
101-
const options = this.options;
102+
(async () => {
103+
const db = this.db;
104+
const name = this.name;
105+
const options = this.options;
102106

103-
const done: Callback = err => {
104-
if (err) {
105-
return callback(err);
107+
const encryptedFields: Document | undefined =
108+
options.encryptedFields ??
109+
db.s.client.options.autoEncryption?.encryptedFieldsMap?.[`${db.databaseName}.${name}`];
110+
111+
if (encryptedFields) {
112+
// Create auxilliary collections for FLE2 support.
113+
const escCollection = encryptedFields.escCollection ?? `enxcol_.${name}.esc`;
114+
const eccCollection = encryptedFields.eccCollection ?? `enxcol_.${name}.ecc`;
115+
const ecocCollection = encryptedFields.ecocCollection ?? `enxcol_.${name}.ecoc`;
116+
117+
for (const collectionName of [escCollection, eccCollection, ecocCollection]) {
118+
const createOp = new CreateCollectionOperation(db, collectionName);
119+
await createOp.executeWithoutEncryptedFieldsCheck(server, session);
120+
}
121+
122+
if (!options.encryptedFields) {
123+
this.options = { ...this.options, encryptedFields };
124+
}
125+
}
126+
127+
const coll = await this.executeWithoutEncryptedFieldsCheck(server, session);
128+
129+
if (encryptedFields) {
130+
// Create the required index for FLE2 support.
131+
const createIndexOp = new CreateIndexOperation(db, name, { __safeContent__: 1 }, {});
132+
await new Promise<void>((resolve, reject) => {
133+
createIndexOp.execute(server, session, err => (err ? reject(err) : resolve()));
134+
});
106135
}
107136

108-
callback(undefined, new Collection(db, name, options));
109-
};
110-
111-
const cmd: Document = { create: name };
112-
for (const n in options) {
113-
if (
114-
(options as any)[n] != null &&
115-
typeof (options as any)[n] !== 'function' &&
116-
!ILLEGAL_COMMAND_FIELDS.has(n)
117-
) {
118-
cmd[n] = (options as any)[n];
137+
return coll;
138+
})().then(
139+
coll => callback(undefined, coll),
140+
err => callback(err)
141+
);
142+
}
143+
144+
private executeWithoutEncryptedFieldsCheck(
145+
server: Server,
146+
session: ClientSession | undefined
147+
): Promise<Collection> {
148+
return new Promise<Collection>((resolve, reject) => {
149+
const db = this.db;
150+
const name = this.name;
151+
const options = this.options;
152+
153+
const done: Callback = err => {
154+
if (err) {
155+
return reject(err);
156+
}
157+
158+
resolve(new Collection(db, name, options));
159+
};
160+
161+
const cmd: Document = { create: name };
162+
for (const n in options) {
163+
if (
164+
(options as any)[n] != null &&
165+
typeof (options as any)[n] !== 'function' &&
166+
!ILLEGAL_COMMAND_FIELDS.has(n)
167+
) {
168+
cmd[n] = (options as any)[n];
169+
}
119170
}
120-
}
121171

122-
// otherwise just execute the command
123-
super.executeCommand(server, session, cmd, done);
172+
// otherwise just execute the command
173+
super.executeCommand(server, session, cmd, done);
174+
});
124175
}
125176
}
126177

src/operations/drop.ts

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
1+
import type { Document } from '../bson';
12
import type { Db } from '../db';
3+
import { MONGODB_ERROR_CODES, MongoServerError } from '../error';
24
import type { Server } from '../sdam/server';
35
import type { ClientSession } from '../sessions';
46
import type { Callback } from '../utils';
57
import { CommandOperation, CommandOperationOptions } from './command';
68
import { Aspect, defineAspects } from './operation';
79

810
/** @public */
9-
export type DropCollectionOptions = CommandOperationOptions;
11+
export interface DropCollectionOptions extends CommandOperationOptions {
12+
/** @experimental */
13+
encryptedFields?: Document;
14+
}
1015

1116
/** @internal */
1217
export class DropCollectionOperation extends CommandOperation<boolean> {
1318
override options: DropCollectionOptions;
19+
db: Db;
1420
name: string;
1521

16-
constructor(db: Db, name: string, options: DropCollectionOptions) {
22+
constructor(db: Db, name: string, options: DropCollectionOptions = {}) {
1723
super(db, options);
24+
this.db = db;
1825
this.options = options;
1926
this.name = name;
2027
}
@@ -24,10 +31,85 @@ export class DropCollectionOperation extends CommandOperation<boolean> {
2431
session: ClientSession | undefined,
2532
callback: Callback<boolean>
2633
): void {
27-
super.executeCommand(server, session, { drop: this.name }, (err, result) => {
28-
if (err) return callback(err);
29-
if (result.ok) return callback(undefined, true);
30-
callback(undefined, false);
34+
(async () => {
35+
const db = this.db;
36+
const options = this.options;
37+
const name = this.name;
38+
39+
const encryptedFieldsMap = db.s.client.options.autoEncryption?.encryptedFieldsMap;
40+
let encryptedFields: Document | undefined =
41+
options.encryptedFields ?? encryptedFieldsMap?.[`${db.databaseName}.${name}`];
42+
43+
if (!encryptedFields && encryptedFieldsMap) {
44+
// If the MongoClient was configued with an encryptedFieldsMap,
45+
// and no encryptedFields config was available in it or explicitly
46+
// passed as an argument, the spec tells us to look one up using
47+
// listCollections().
48+
const listCollectionsResult = await db
49+
.listCollections({ name }, { nameOnly: false })
50+
.toArray();
51+
encryptedFields = listCollectionsResult?.[0]?.options?.encryptedFields;
52+
}
53+
54+
let result;
55+
let errorForMainOperation;
56+
try {
57+
result = await this.executeWithoutEncryptedFieldsCheck(server, session);
58+
} catch (err) {
59+
if (
60+
!encryptedFields ||
61+
!(err instanceof MongoServerError) ||
62+
err.code !== MONGODB_ERROR_CODES.NamespaceNotFound
63+
) {
64+
throw err;
65+
}
66+
// Save a possible NamespaceNotFound error for later
67+
// in the encryptedFields case, so that the auxilliary
68+
// collections will still be dropped.
69+
errorForMainOperation = err;
70+
}
71+
72+
if (encryptedFields) {
73+
const escCollection = encryptedFields.escCollection || `enxcol_.${name}.esc`;
74+
const eccCollection = encryptedFields.eccCollection || `enxcol_.${name}.ecc`;
75+
const ecocCollection = encryptedFields.ecocCollection || `enxcol_.${name}.ecoc`;
76+
77+
for (const collectionName of [escCollection, eccCollection, ecocCollection]) {
78+
// Drop auxilliary collections, ignoring potential NamespaceNotFound errors.
79+
const dropOp = new DropCollectionOperation(db, collectionName);
80+
try {
81+
await dropOp.executeWithoutEncryptedFieldsCheck(server, session);
82+
} catch (err) {
83+
if (
84+
!(err instanceof MongoServerError) ||
85+
err.code !== MONGODB_ERROR_CODES.NamespaceNotFound
86+
) {
87+
throw err;
88+
}
89+
}
90+
}
91+
92+
if (errorForMainOperation) {
93+
throw errorForMainOperation;
94+
}
95+
}
96+
97+
return result;
98+
})().then(
99+
result => callback(undefined, result),
100+
err => callback(err)
101+
);
102+
}
103+
104+
private executeWithoutEncryptedFieldsCheck(
105+
server: Server,
106+
session: ClientSession | undefined
107+
): Promise<boolean> {
108+
return new Promise<boolean>((resolve, reject) => {
109+
super.executeCommand(server, session, { drop: this.name }, (err, result) => {
110+
if (err) return reject(err);
111+
resolve(!!result.ok);
112+
});
31113
});
32114
}
33115
}

0 commit comments

Comments
 (0)