Skip to content

Commit aac7629

Browse files
refactor(NODE-7060): move QE collection creation/deletion out of operations classes (#4596)
1 parent c536534 commit aac7629

File tree

5 files changed

+152
-130
lines changed

5 files changed

+152
-130
lines changed

src/collection.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import {
3737
type DeleteResult
3838
} from './operations/delete';
3939
import { DistinctOperation, type DistinctOptions } from './operations/distinct';
40-
import { DropCollectionOperation, type DropCollectionOptions } from './operations/drop';
40+
import { type DropCollectionOptions } from './operations/drop';
4141
import {
4242
EstimatedDocumentCountOperation,
4343
type EstimatedDocumentCountOptions
@@ -523,10 +523,7 @@ export class Collection<TSchema extends Document = Document> {
523523
* @param options - Optional settings for the command
524524
*/
525525
async drop(options?: DropCollectionOptions): Promise<boolean> {
526-
return await executeOperation(
527-
this.client,
528-
new DropCollectionOperation(this.s.db, this.collectionName, options)
529-
);
526+
return await this.s.db.dropCollection(this.collectionName, options);
530527
}
531528

532529
/**

src/db.ts

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,10 @@ import { MongoInvalidArgumentError } from './error';
1010
import type { MongoClient, PkFactory } from './mongo_client';
1111
import type { Abortable, TODO_NODE_3286 } from './mongo_types';
1212
import type { AggregateOptions } from './operations/aggregate';
13+
import { type CreateCollectionOptions, createCollections } from './operations/create_collection';
1314
import {
14-
CreateCollectionOperation,
15-
type CreateCollectionOptions
16-
} from './operations/create_collection';
17-
import {
18-
DropCollectionOperation,
1915
type DropCollectionOptions,
16+
dropCollections,
2017
DropDatabaseOperation,
2118
type DropDatabaseOptions
2219
} from './operations/drop';
@@ -241,10 +238,8 @@ export class Db {
241238
name: string,
242239
options?: CreateCollectionOptions
243240
): Promise<Collection<TSchema>> {
244-
return await executeOperation(
245-
this.client,
246-
new CreateCollectionOperation(this, name, resolveOptions(this, options)) as TODO_NODE_3286
247-
);
241+
options = resolveOptions(this, options);
242+
return await createCollections<TSchema>(this, name, options);
248243
}
249244

250245
/**
@@ -410,10 +405,8 @@ export class Db {
410405
* @param options - Optional settings for the command
411406
*/
412407
async dropCollection(name: string, options?: DropCollectionOptions): Promise<boolean> {
413-
return await executeOperation(
414-
this.client,
415-
new DropCollectionOperation(this, name, resolveOptions(this, options))
416-
);
408+
options = resolveOptions(this, options);
409+
return await dropCollections(this, name, options);
417410
}
418411

419412
/**

src/operations/create_collection.ts

Lines changed: 81 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import { MongoCompatibilityError } from '../error';
99
import type { PkFactory } from '../mongo_client';
1010
import type { Server } from '../sdam/server';
1111
import type { ClientSession } from '../sessions';
12-
import { type TimeoutContext } from '../timeout';
12+
import { TimeoutContext } from '../timeout';
1313
import { CommandOperation, type CommandOperationOptions } from './command';
14+
import { executeOperation } from './execute_operation';
1415
import { CreateIndexesOperation } from './indexes';
1516
import { Aspect, defineAspects } from './operation';
1617

@@ -135,79 +136,95 @@ export class CreateCollectionOperation extends CommandOperation<Collection> {
135136
const name = this.name;
136137
const options = this.options;
137138

138-
const encryptedFields: Document | undefined =
139-
options.encryptedFields ??
140-
db.client.s.options.autoEncryption?.encryptedFieldsMap?.[`${db.databaseName}.${name}`];
141-
142-
if (encryptedFields) {
143-
// Creating a QE collection required min server of 7.0.0
144-
// TODO(NODE-5353): Get wire version information from connection.
145-
if (
146-
!server.loadBalanced &&
147-
server.description.maxWireVersion < MIN_SUPPORTED_QE_WIRE_VERSION
148-
) {
149-
throw new MongoCompatibilityError(
150-
`${INVALID_QE_VERSION} The minimum server version required is ${MIN_SUPPORTED_QE_SERVER_VERSION}`
151-
);
152-
}
153-
// Create auxilliary collections for queryable encryption support.
154-
const escCollection = encryptedFields.escCollection ?? `enxcol_.${name}.esc`;
155-
const ecocCollection = encryptedFields.ecocCollection ?? `enxcol_.${name}.ecoc`;
156-
157-
for (const collectionName of [escCollection, ecocCollection]) {
158-
const createOp = new CreateCollectionOperation(db, collectionName, {
159-
clusteredIndex: {
160-
key: { _id: 1 },
161-
unique: true
162-
}
163-
});
164-
await createOp.executeWithoutEncryptedFieldsCheck(server, session, timeoutContext);
139+
const cmd: Document = { create: name };
140+
for (const [option, value] of Object.entries(options)) {
141+
if (value != null && typeof value !== 'function' && !ILLEGAL_COMMAND_FIELDS.has(option)) {
142+
cmd[option] = value;
165143
}
144+
}
166145

167-
if (!options.encryptedFields) {
168-
this.options = { ...this.options, encryptedFields };
146+
// otherwise just execute the command
147+
await super.executeCommand(server, session, cmd, timeoutContext);
148+
return new Collection(db, name, options);
149+
}
150+
}
151+
152+
export async function createCollections<TSchema extends Document>(
153+
db: Db,
154+
name: string,
155+
options: CreateCollectionOptions
156+
): Promise<Collection<TSchema>> {
157+
const timeoutContext = TimeoutContext.create({
158+
session: options.session,
159+
serverSelectionTimeoutMS: db.client.s.options.serverSelectionTimeoutMS,
160+
waitQueueTimeoutMS: db.client.s.options.waitQueueTimeoutMS,
161+
timeoutMS: options.timeoutMS
162+
});
163+
164+
const encryptedFields: Document | undefined =
165+
options.encryptedFields ??
166+
db.client.s.options.autoEncryption?.encryptedFieldsMap?.[`${db.databaseName}.${name}`];
167+
168+
if (encryptedFields) {
169+
class CreateSupportingFLEv2CollectionOperation extends CreateCollectionOperation {
170+
override execute(
171+
server: Server,
172+
session: ClientSession | undefined,
173+
timeoutContext: TimeoutContext
174+
): Promise<Collection> {
175+
// Creating a QE collection required min server of 7.0.0
176+
// TODO(NODE-5353): Get wire version information from connection.
177+
if (
178+
!server.loadBalanced &&
179+
server.description.maxWireVersion < MIN_SUPPORTED_QE_WIRE_VERSION
180+
) {
181+
throw new MongoCompatibilityError(
182+
`${INVALID_QE_VERSION} The minimum server version required is ${MIN_SUPPORTED_QE_SERVER_VERSION}`
183+
);
184+
}
185+
186+
return super.execute(server, session, timeoutContext);
169187
}
170188
}
171189

172-
const coll = await this.executeWithoutEncryptedFieldsCheck(server, session, timeoutContext);
173-
174-
if (encryptedFields) {
175-
// Create the required index for queryable encryption support.
176-
const createIndexOp = CreateIndexesOperation.fromIndexSpecification(
177-
db,
178-
name,
179-
{ __safeContent__: 1 },
180-
{}
181-
);
182-
await createIndexOp.execute(server, session, timeoutContext);
190+
// Create auxilliary collections for queryable encryption support.
191+
const escCollection = encryptedFields.escCollection ?? `enxcol_.${name}.esc`;
192+
const ecocCollection = encryptedFields.ecocCollection ?? `enxcol_.${name}.ecoc`;
193+
194+
for (const collectionName of [escCollection, ecocCollection]) {
195+
const createOp = new CreateSupportingFLEv2CollectionOperation(db, collectionName, {
196+
clusteredIndex: {
197+
key: { _id: 1 },
198+
unique: true
199+
},
200+
session: options.session
201+
});
202+
await executeOperation(db.client, createOp, timeoutContext);
183203
}
184204

185-
return coll;
205+
if (!options.encryptedFields) {
206+
options = { ...options, encryptedFields };
207+
}
186208
}
187209

188-
private async executeWithoutEncryptedFieldsCheck(
189-
server: Server,
190-
session: ClientSession | undefined,
191-
timeoutContext: TimeoutContext
192-
): Promise<Collection> {
193-
const db = this.db;
194-
const name = this.name;
195-
const options = this.options;
196-
197-
const cmd: Document = { create: name };
198-
for (const n in options) {
199-
if (
200-
(options as any)[n] != null &&
201-
typeof (options as any)[n] !== 'function' &&
202-
!ILLEGAL_COMMAND_FIELDS.has(n)
203-
) {
204-
cmd[n] = (options as any)[n];
205-
}
206-
}
207-
// otherwise just execute the command
208-
await super.executeCommand(server, session, cmd, timeoutContext);
209-
return new Collection(db, name, options);
210+
const coll = await executeOperation(
211+
db.client,
212+
new CreateCollectionOperation(db, name, options),
213+
timeoutContext
214+
);
215+
216+
if (encryptedFields) {
217+
// Create the required index for queryable encryption support.
218+
const createIndexOp = CreateIndexesOperation.fromIndexSpecification(
219+
db,
220+
name,
221+
{ __safeContent__: 1 },
222+
{ session: options.session }
223+
);
224+
await executeOperation(db.client, createIndexOp, timeoutContext);
210225
}
226+
227+
return coll as unknown as Collection<TSchema>;
211228
}
212229

213230
defineAspects(CreateCollectionOperation, [Aspect.WRITE_OPERATION]);

src/operations/drop.ts

Lines changed: 61 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import type { Document } from '../bson';
2+
import { CursorTimeoutContext } from '../cursor/abstract_cursor';
23
import type { Db } from '../db';
34
import { MONGODB_ERROR_CODES, MongoServerError } from '../error';
45
import type { Server } from '../sdam/server';
56
import type { ClientSession } from '../sessions';
6-
import { type TimeoutContext } from '../timeout';
7+
import { TimeoutContext } from '../timeout';
78
import { CommandOperation, type CommandOperationOptions } from './command';
9+
import { executeOperation } from './execute_operation';
810
import { Aspect, defineAspects } from './operation';
911

1012
/** @public */
@@ -16,12 +18,10 @@ export interface DropCollectionOptions extends CommandOperationOptions {
1618
/** @internal */
1719
export class DropCollectionOperation extends CommandOperation<boolean> {
1820
override options: DropCollectionOptions;
19-
db: Db;
2021
name: string;
2122

2223
constructor(db: Db, name: string, options: DropCollectionOptions = {}) {
2324
super(db, options);
24-
this.db = db;
2525
this.options = options;
2626
this.name = name;
2727
}
@@ -35,56 +35,70 @@ export class DropCollectionOperation extends CommandOperation<boolean> {
3535
session: ClientSession | undefined,
3636
timeoutContext: TimeoutContext
3737
): Promise<boolean> {
38-
const db = this.db;
39-
const options = this.options;
40-
const name = this.name;
41-
42-
const encryptedFieldsMap = db.client.s.options.autoEncryption?.encryptedFieldsMap;
43-
let encryptedFields: Document | undefined =
44-
options.encryptedFields ?? encryptedFieldsMap?.[`${db.databaseName}.${name}`];
45-
46-
if (!encryptedFields && encryptedFieldsMap) {
47-
// If the MongoClient was configured with an encryptedFieldsMap,
48-
// and no encryptedFields config was available in it or explicitly
49-
// passed as an argument, the spec tells us to look one up using
50-
// listCollections().
51-
const listCollectionsResult = await db
52-
.listCollections({ name }, { nameOnly: false })
53-
.toArray();
54-
encryptedFields = listCollectionsResult?.[0]?.options?.encryptedFields;
55-
}
38+
await super.executeCommand(server, session, { drop: this.name }, timeoutContext);
39+
return true;
40+
}
41+
}
42+
43+
export async function dropCollections(
44+
db: Db,
45+
name: string,
46+
options: DropCollectionOptions
47+
): Promise<boolean> {
48+
const timeoutContext = TimeoutContext.create({
49+
session: options.session,
50+
serverSelectionTimeoutMS: db.client.s.options.serverSelectionTimeoutMS,
51+
waitQueueTimeoutMS: db.client.s.options.waitQueueTimeoutMS,
52+
timeoutMS: options.timeoutMS
53+
});
54+
55+
const encryptedFieldsMap = db.client.s.options.autoEncryption?.encryptedFieldsMap;
56+
let encryptedFields: Document | undefined =
57+
options.encryptedFields ?? encryptedFieldsMap?.[`${db.databaseName}.${name}`];
58+
59+
if (!encryptedFields && encryptedFieldsMap) {
60+
// If the MongoClient was configured with an encryptedFieldsMap,
61+
// and no encryptedFields config was available in it or explicitly
62+
// passed as an argument, the spec tells us to look one up using
63+
// listCollections().
64+
const listCollectionsResult = await db
65+
.listCollections(
66+
{ name },
67+
{
68+
nameOnly: false,
69+
session: options.session,
70+
timeoutContext: new CursorTimeoutContext(timeoutContext, Symbol())
71+
}
72+
)
73+
.toArray();
74+
encryptedFields = listCollectionsResult?.[0]?.options?.encryptedFields;
75+
}
5676

57-
if (encryptedFields) {
58-
const escCollection = encryptedFields.escCollection || `enxcol_.${name}.esc`;
59-
const ecocCollection = encryptedFields.ecocCollection || `enxcol_.${name}.ecoc`;
60-
61-
for (const collectionName of [escCollection, ecocCollection]) {
62-
// Drop auxilliary collections, ignoring potential NamespaceNotFound errors.
63-
const dropOp = new DropCollectionOperation(db, collectionName);
64-
try {
65-
await dropOp.executeWithoutEncryptedFieldsCheck(server, session, timeoutContext);
66-
} catch (err) {
67-
if (
68-
!(err instanceof MongoServerError) ||
69-
err.code !== MONGODB_ERROR_CODES.NamespaceNotFound
70-
) {
71-
throw err;
72-
}
77+
if (encryptedFields) {
78+
const escCollection = encryptedFields.escCollection || `enxcol_.${name}.esc`;
79+
const ecocCollection = encryptedFields.ecocCollection || `enxcol_.${name}.ecoc`;
80+
81+
for (const collectionName of [escCollection, ecocCollection]) {
82+
// Drop auxilliary collections, ignoring potential NamespaceNotFound errors.
83+
const dropOp = new DropCollectionOperation(db, collectionName, options);
84+
try {
85+
await executeOperation(db.client, dropOp, timeoutContext);
86+
} catch (err) {
87+
if (
88+
!(err instanceof MongoServerError) ||
89+
err.code !== MONGODB_ERROR_CODES.NamespaceNotFound
90+
) {
91+
throw err;
7392
}
7493
}
7594
}
76-
77-
return await this.executeWithoutEncryptedFieldsCheck(server, session, timeoutContext);
7895
}
7996

80-
private async executeWithoutEncryptedFieldsCheck(
81-
server: Server,
82-
session: ClientSession | undefined,
83-
timeoutContext: TimeoutContext
84-
): Promise<boolean> {
85-
await super.executeCommand(server, session, { drop: this.name }, timeoutContext);
86-
return true;
87-
}
97+
return await executeOperation(
98+
db.client,
99+
new DropCollectionOperation(db, name, options),
100+
timeoutContext
101+
);
88102
}
89103

90104
/** @public */

test/integration/collection-management/collection_db_management.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ describe('Collection Management and Db Management', function () {
66
let client: MongoClient;
77
let db: Db;
88

9-
beforeEach(function () {
9+
beforeEach(async function () {
1010
client = this.configuration.newClient();
1111
db = client.db();
12+
await db.dropDatabase();
1213
});
1314

1415
afterEach(async function () {

0 commit comments

Comments
 (0)