diff --git a/src/collection.ts b/src/collection.ts index ef3e79f8c2..d1ae1d7fd9 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -37,7 +37,7 @@ import { type DeleteResult } from './operations/delete'; import { DistinctOperation, type DistinctOptions } from './operations/distinct'; -import { DropCollectionOperation, type DropCollectionOptions } from './operations/drop'; +import { type DropCollectionOptions } from './operations/drop'; import { EstimatedDocumentCountOperation, type EstimatedDocumentCountOptions @@ -523,10 +523,7 @@ export class Collection { * @param options - Optional settings for the command */ async drop(options?: DropCollectionOptions): Promise { - return await executeOperation( - this.client, - new DropCollectionOperation(this.s.db, this.collectionName, options) - ); + return await this.s.db.dropCollection(this.collectionName, options); } /** diff --git a/src/db.ts b/src/db.ts index c75293f44a..49b094bc10 100644 --- a/src/db.ts +++ b/src/db.ts @@ -10,13 +10,10 @@ import { MongoInvalidArgumentError } from './error'; import type { MongoClient, PkFactory } from './mongo_client'; import type { Abortable, TODO_NODE_3286 } from './mongo_types'; import type { AggregateOptions } from './operations/aggregate'; +import { type CreateCollectionOptions, createCollections } from './operations/create_collection'; import { - CreateCollectionOperation, - type CreateCollectionOptions -} from './operations/create_collection'; -import { - DropCollectionOperation, type DropCollectionOptions, + dropCollections, DropDatabaseOperation, type DropDatabaseOptions } from './operations/drop'; @@ -241,10 +238,8 @@ export class Db { name: string, options?: CreateCollectionOptions ): Promise> { - return await executeOperation( - this.client, - new CreateCollectionOperation(this, name, resolveOptions(this, options)) as TODO_NODE_3286 - ); + options = resolveOptions(this, options); + return await createCollections(this, name, options); } /** @@ -410,10 +405,8 @@ export class Db { * @param options - Optional settings for the command */ async dropCollection(name: string, options?: DropCollectionOptions): Promise { - return await executeOperation( - this.client, - new DropCollectionOperation(this, name, resolveOptions(this, options)) - ); + options = resolveOptions(this, options); + return await dropCollections(this, name, options); } /** diff --git a/src/operations/create_collection.ts b/src/operations/create_collection.ts index da278f88c1..3326d63c8e 100644 --- a/src/operations/create_collection.ts +++ b/src/operations/create_collection.ts @@ -9,8 +9,9 @@ import { MongoCompatibilityError } from '../error'; import type { PkFactory } from '../mongo_client'; import type { Server } from '../sdam/server'; import type { ClientSession } from '../sessions'; -import { type TimeoutContext } from '../timeout'; +import { TimeoutContext } from '../timeout'; import { CommandOperation, type CommandOperationOptions } from './command'; +import { executeOperation } from './execute_operation'; import { CreateIndexesOperation } from './indexes'; import { Aspect, defineAspects } from './operation'; @@ -135,79 +136,95 @@ export class CreateCollectionOperation extends CommandOperation { const name = this.name; const options = this.options; - const encryptedFields: Document | undefined = - options.encryptedFields ?? - db.client.s.options.autoEncryption?.encryptedFieldsMap?.[`${db.databaseName}.${name}`]; - - if (encryptedFields) { - // Creating a QE collection required min server of 7.0.0 - // TODO(NODE-5353): Get wire version information from connection. - if ( - !server.loadBalanced && - server.description.maxWireVersion < MIN_SUPPORTED_QE_WIRE_VERSION - ) { - throw new MongoCompatibilityError( - `${INVALID_QE_VERSION} The minimum server version required is ${MIN_SUPPORTED_QE_SERVER_VERSION}` - ); - } - // Create auxilliary collections for queryable encryption support. - const escCollection = encryptedFields.escCollection ?? `enxcol_.${name}.esc`; - const ecocCollection = encryptedFields.ecocCollection ?? `enxcol_.${name}.ecoc`; - - for (const collectionName of [escCollection, ecocCollection]) { - const createOp = new CreateCollectionOperation(db, collectionName, { - clusteredIndex: { - key: { _id: 1 }, - unique: true - } - }); - await createOp.executeWithoutEncryptedFieldsCheck(server, session, timeoutContext); + const cmd: Document = { create: name }; + for (const [option, value] of Object.entries(options)) { + if (value != null && typeof value !== 'function' && !ILLEGAL_COMMAND_FIELDS.has(option)) { + cmd[option] = value; } + } - if (!options.encryptedFields) { - this.options = { ...this.options, encryptedFields }; + // otherwise just execute the command + await super.executeCommand(server, session, cmd, timeoutContext); + return new Collection(db, name, options); + } +} + +export async function createCollections( + db: Db, + name: string, + options: CreateCollectionOptions +): Promise> { + const timeoutContext = TimeoutContext.create({ + session: options.session, + serverSelectionTimeoutMS: db.client.s.options.serverSelectionTimeoutMS, + waitQueueTimeoutMS: db.client.s.options.waitQueueTimeoutMS, + timeoutMS: options.timeoutMS + }); + + const encryptedFields: Document | undefined = + options.encryptedFields ?? + db.client.s.options.autoEncryption?.encryptedFieldsMap?.[`${db.databaseName}.${name}`]; + + if (encryptedFields) { + class CreateSupportingFLEv2CollectionOperation extends CreateCollectionOperation { + override execute( + server: Server, + session: ClientSession | undefined, + timeoutContext: TimeoutContext + ): Promise { + // Creating a QE collection required min server of 7.0.0 + // TODO(NODE-5353): Get wire version information from connection. + if ( + !server.loadBalanced && + server.description.maxWireVersion < MIN_SUPPORTED_QE_WIRE_VERSION + ) { + throw new MongoCompatibilityError( + `${INVALID_QE_VERSION} The minimum server version required is ${MIN_SUPPORTED_QE_SERVER_VERSION}` + ); + } + + return super.execute(server, session, timeoutContext); } } - const coll = await this.executeWithoutEncryptedFieldsCheck(server, session, timeoutContext); - - if (encryptedFields) { - // Create the required index for queryable encryption support. - const createIndexOp = CreateIndexesOperation.fromIndexSpecification( - db, - name, - { __safeContent__: 1 }, - {} - ); - await createIndexOp.execute(server, session, timeoutContext); + // Create auxilliary collections for queryable encryption support. + const escCollection = encryptedFields.escCollection ?? `enxcol_.${name}.esc`; + const ecocCollection = encryptedFields.ecocCollection ?? `enxcol_.${name}.ecoc`; + + for (const collectionName of [escCollection, ecocCollection]) { + const createOp = new CreateSupportingFLEv2CollectionOperation(db, collectionName, { + clusteredIndex: { + key: { _id: 1 }, + unique: true + }, + session: options.session + }); + await executeOperation(db.client, createOp, timeoutContext); } - return coll; + if (!options.encryptedFields) { + options = { ...options, encryptedFields }; + } } - private async executeWithoutEncryptedFieldsCheck( - server: Server, - session: ClientSession | undefined, - timeoutContext: TimeoutContext - ): Promise { - const db = this.db; - const name = this.name; - const options = this.options; - - const cmd: Document = { create: name }; - for (const n in options) { - if ( - (options as any)[n] != null && - typeof (options as any)[n] !== 'function' && - !ILLEGAL_COMMAND_FIELDS.has(n) - ) { - cmd[n] = (options as any)[n]; - } - } - // otherwise just execute the command - await super.executeCommand(server, session, cmd, timeoutContext); - return new Collection(db, name, options); + const coll = await executeOperation( + db.client, + new CreateCollectionOperation(db, name, options), + timeoutContext + ); + + if (encryptedFields) { + // Create the required index for queryable encryption support. + const createIndexOp = CreateIndexesOperation.fromIndexSpecification( + db, + name, + { __safeContent__: 1 }, + { session: options.session } + ); + await executeOperation(db.client, createIndexOp, timeoutContext); } + + return coll as unknown as Collection; } defineAspects(CreateCollectionOperation, [Aspect.WRITE_OPERATION]); diff --git a/src/operations/drop.ts b/src/operations/drop.ts index 0ead5a4927..3fd4ac6dac 100644 --- a/src/operations/drop.ts +++ b/src/operations/drop.ts @@ -1,10 +1,12 @@ import type { Document } from '../bson'; +import { CursorTimeoutContext } from '../cursor/abstract_cursor'; import type { Db } from '../db'; import { MONGODB_ERROR_CODES, MongoServerError } from '../error'; import type { Server } from '../sdam/server'; import type { ClientSession } from '../sessions'; -import { type TimeoutContext } from '../timeout'; +import { TimeoutContext } from '../timeout'; import { CommandOperation, type CommandOperationOptions } from './command'; +import { executeOperation } from './execute_operation'; import { Aspect, defineAspects } from './operation'; /** @public */ @@ -16,12 +18,10 @@ export interface DropCollectionOptions extends CommandOperationOptions { /** @internal */ export class DropCollectionOperation extends CommandOperation { override options: DropCollectionOptions; - db: Db; name: string; constructor(db: Db, name: string, options: DropCollectionOptions = {}) { super(db, options); - this.db = db; this.options = options; this.name = name; } @@ -35,56 +35,70 @@ export class DropCollectionOperation extends CommandOperation { session: ClientSession | undefined, timeoutContext: TimeoutContext ): Promise { - const db = this.db; - const options = this.options; - const name = this.name; - - const encryptedFieldsMap = db.client.s.options.autoEncryption?.encryptedFieldsMap; - let encryptedFields: Document | undefined = - options.encryptedFields ?? encryptedFieldsMap?.[`${db.databaseName}.${name}`]; - - if (!encryptedFields && encryptedFieldsMap) { - // If the MongoClient was configured with an encryptedFieldsMap, - // and no encryptedFields config was available in it or explicitly - // passed as an argument, the spec tells us to look one up using - // listCollections(). - const listCollectionsResult = await db - .listCollections({ name }, { nameOnly: false }) - .toArray(); - encryptedFields = listCollectionsResult?.[0]?.options?.encryptedFields; - } + await super.executeCommand(server, session, { drop: this.name }, timeoutContext); + return true; + } +} + +export async function dropCollections( + db: Db, + name: string, + options: DropCollectionOptions +): Promise { + const timeoutContext = TimeoutContext.create({ + session: options.session, + serverSelectionTimeoutMS: db.client.s.options.serverSelectionTimeoutMS, + waitQueueTimeoutMS: db.client.s.options.waitQueueTimeoutMS, + timeoutMS: options.timeoutMS + }); + + const encryptedFieldsMap = db.client.s.options.autoEncryption?.encryptedFieldsMap; + let encryptedFields: Document | undefined = + options.encryptedFields ?? encryptedFieldsMap?.[`${db.databaseName}.${name}`]; + + if (!encryptedFields && encryptedFieldsMap) { + // If the MongoClient was configured with an encryptedFieldsMap, + // and no encryptedFields config was available in it or explicitly + // passed as an argument, the spec tells us to look one up using + // listCollections(). + const listCollectionsResult = await db + .listCollections( + { name }, + { + nameOnly: false, + session: options.session, + timeoutContext: new CursorTimeoutContext(timeoutContext, Symbol()) + } + ) + .toArray(); + encryptedFields = listCollectionsResult?.[0]?.options?.encryptedFields; + } - if (encryptedFields) { - const escCollection = encryptedFields.escCollection || `enxcol_.${name}.esc`; - const ecocCollection = encryptedFields.ecocCollection || `enxcol_.${name}.ecoc`; - - for (const collectionName of [escCollection, ecocCollection]) { - // Drop auxilliary collections, ignoring potential NamespaceNotFound errors. - const dropOp = new DropCollectionOperation(db, collectionName); - try { - await dropOp.executeWithoutEncryptedFieldsCheck(server, session, timeoutContext); - } catch (err) { - if ( - !(err instanceof MongoServerError) || - err.code !== MONGODB_ERROR_CODES.NamespaceNotFound - ) { - throw err; - } + if (encryptedFields) { + const escCollection = encryptedFields.escCollection || `enxcol_.${name}.esc`; + const ecocCollection = encryptedFields.ecocCollection || `enxcol_.${name}.ecoc`; + + for (const collectionName of [escCollection, ecocCollection]) { + // Drop auxilliary collections, ignoring potential NamespaceNotFound errors. + const dropOp = new DropCollectionOperation(db, collectionName, options); + try { + await executeOperation(db.client, dropOp, timeoutContext); + } catch (err) { + if ( + !(err instanceof MongoServerError) || + err.code !== MONGODB_ERROR_CODES.NamespaceNotFound + ) { + throw err; } } } - - return await this.executeWithoutEncryptedFieldsCheck(server, session, timeoutContext); } - private async executeWithoutEncryptedFieldsCheck( - server: Server, - session: ClientSession | undefined, - timeoutContext: TimeoutContext - ): Promise { - await super.executeCommand(server, session, { drop: this.name }, timeoutContext); - return true; - } + return await executeOperation( + db.client, + new DropCollectionOperation(db, name, options), + timeoutContext + ); } /** @public */ diff --git a/test/integration/collection-management/collection_db_management.test.ts b/test/integration/collection-management/collection_db_management.test.ts index 0cb90b3b59..327c7f4113 100644 --- a/test/integration/collection-management/collection_db_management.test.ts +++ b/test/integration/collection-management/collection_db_management.test.ts @@ -6,9 +6,10 @@ describe('Collection Management and Db Management', function () { let client: MongoClient; let db: Db; - beforeEach(function () { + beforeEach(async function () { client = this.configuration.newClient(); db = client.db(); + await db.dropDatabase(); }); afterEach(async function () {