diff --git a/src/operations/create_collection.ts b/src/operations/create_collection.ts index 3326d63c8e..955058ad4f 100644 --- a/src/operations/create_collection.ts +++ b/src/operations/create_collection.ts @@ -1,16 +1,18 @@ +import { type Connection } from '..'; import type { Document } from '../bson'; import { MIN_SUPPORTED_QE_SERVER_VERSION, MIN_SUPPORTED_QE_WIRE_VERSION } from '../cmap/wire_protocol/constants'; +import { MongoDBResponse } from '../cmap/wire_protocol/responses'; import { Collection } from '../collection'; import type { Db } from '../db'; import { MongoCompatibilityError } from '../error'; import type { PkFactory } from '../mongo_client'; -import type { Server } from '../sdam/server'; import type { ClientSession } from '../sessions'; import { TimeoutContext } from '../timeout'; -import { CommandOperation, type CommandOperationOptions } from './command'; +import { maxWireVersion } from '../utils'; +import { type CommandOperationOptions, ModernizedCommandOperation } from './command'; import { executeOperation } from './execute_operation'; import { CreateIndexesOperation } from './indexes'; import { Aspect, defineAspects } from './operation'; @@ -110,7 +112,8 @@ const INVALID_QE_VERSION = 'Driver support of Queryable Encryption is incompatible with server. Upgrade server to use Queryable Encryption.'; /** @internal */ -export class CreateCollectionOperation extends CommandOperation { +export class CreateCollectionOperation extends ModernizedCommandOperation { + override SERVER_COMMAND_RESPONSE_TYPE = MongoDBResponse; override options: CreateCollectionOptions; db: Db; name: string; @@ -127,25 +130,19 @@ export class CreateCollectionOperation extends CommandOperation { return 'create' as const; } - override async execute( - 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 [option, value] of Object.entries(options)) { - if (value != null && typeof value !== 'function' && !ILLEGAL_COMMAND_FIELDS.has(option)) { - cmd[option] = value; - } - } + override buildCommandDocument(_connection: Connection, _session?: ClientSession): Document { + const isOptionValid = ([k, v]: [k: string, v: unknown]) => + v != null && typeof v !== 'function' && !ILLEGAL_COMMAND_FIELDS.has(k); + return { + create: this.name, + ...Object.fromEntries(Object.entries(this.options).filter(isOptionValid)) + }; + } - // otherwise just execute the command - await super.executeCommand(server, session, cmd, timeoutContext); - return new Collection(db, name, options); + override handleOk( + _response: InstanceType + ): Collection { + return new Collection(this.db, this.name, this.options); } } @@ -167,23 +164,17 @@ export async function createCollections( 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. + override buildCommandDocument(connection: Connection, session?: ClientSession): Document { if ( - !server.loadBalanced && - server.description.maxWireVersion < MIN_SUPPORTED_QE_WIRE_VERSION + !connection.description.loadBalanced && + maxWireVersion(connection) < 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); + return super.buildCommandDocument(connection, session); } } diff --git a/src/operations/drop.ts b/src/operations/drop.ts index 833321fe01..c46e1444fc 100644 --- a/src/operations/drop.ts +++ b/src/operations/drop.ts @@ -1,12 +1,12 @@ -import { MongoServerError } from '..'; +import { type Connection, MongoServerError } from '..'; import type { Document } from '../bson'; +import { MongoDBResponse } from '../cmap/wire_protocol/responses'; import { CursorTimeoutContext } from '../cursor/abstract_cursor'; import type { Db } from '../db'; import { MONGODB_ERROR_CODES } from '../error'; -import type { Server } from '../sdam/server'; import type { ClientSession } from '../sessions'; import { TimeoutContext } from '../timeout'; -import { CommandOperation, type CommandOperationOptions } from './command'; +import { type CommandOperationOptions, ModernizedCommandOperation } from './command'; import { executeOperation } from './execute_operation'; import { Aspect, defineAspects } from './operation'; @@ -17,7 +17,9 @@ export interface DropCollectionOptions extends CommandOperationOptions { } /** @internal */ -export class DropCollectionOperation extends CommandOperation { +export class DropCollectionOperation extends ModernizedCommandOperation { + override SERVER_COMMAND_RESPONSE_TYPE = MongoDBResponse; + override options: DropCollectionOptions; name: string; @@ -31,12 +33,11 @@ export class DropCollectionOperation extends CommandOperation { return 'drop' as const; } - override async execute( - server: Server, - session: ClientSession | undefined, - timeoutContext: TimeoutContext - ): Promise { - await super.executeCommand(server, session, { drop: this.name }, timeoutContext); + override buildCommandDocument(_connection: Connection, _session?: ClientSession): Document { + return { drop: this.name }; + } + + override handleOk(_response: InstanceType): boolean { return true; } } @@ -106,7 +107,8 @@ export async function dropCollections( export type DropDatabaseOptions = CommandOperationOptions; /** @internal */ -export class DropDatabaseOperation extends CommandOperation { +export class DropDatabaseOperation extends ModernizedCommandOperation { + override SERVER_COMMAND_RESPONSE_TYPE = MongoDBResponse; override options: DropDatabaseOptions; constructor(db: Db, options: DropDatabaseOptions) { @@ -117,12 +119,11 @@ export class DropDatabaseOperation extends CommandOperation { return 'dropDatabase' as const; } - override async execute( - server: Server, - session: ClientSession | undefined, - timeoutContext: TimeoutContext - ): Promise { - await super.executeCommand(server, session, { dropDatabase: 1 }, timeoutContext); + override buildCommandDocument(_connection: Connection, _session?: ClientSession): Document { + return { dropDatabase: 1 }; + } + + override handleOk(_response: InstanceType): boolean { return true; } } diff --git a/src/operations/list_databases.ts b/src/operations/list_databases.ts index bd740d50c6..2b7dcabf60 100644 --- a/src/operations/list_databases.ts +++ b/src/operations/list_databases.ts @@ -1,11 +1,10 @@ +import { type Connection } from '..'; import type { Document } from '../bson'; +import { MongoDBResponse } from '../cmap/wire_protocol/responses'; import type { Db } from '../db'; -import { type TODO_NODE_3286 } from '../mongo_types'; -import type { Server } from '../sdam/server'; import type { ClientSession } from '../sessions'; -import { type TimeoutContext } from '../timeout'; import { maxWireVersion, MongoDBNamespace } from '../utils'; -import { CommandOperation, type CommandOperationOptions } from './command'; +import { type CommandOperationOptions, ModernizedCommandOperation } from './command'; import { Aspect, defineAspects } from './operation'; /** @public */ @@ -27,7 +26,8 @@ export interface ListDatabasesOptions extends CommandOperationOptions { } /** @internal */ -export class ListDatabasesOperation extends CommandOperation { +export class ListDatabasesOperation extends ModernizedCommandOperation { + override SERVER_COMMAND_RESPONSE_TYPE = MongoDBResponse; override options: ListDatabasesOptions; constructor(db: Db, options?: ListDatabasesOptions) { @@ -40,11 +40,7 @@ export class ListDatabasesOperation extends CommandOperation { + override buildCommandDocument(connection: Connection, _session?: ClientSession): Document { const cmd: Document = { listDatabases: 1 }; if (typeof this.options.nameOnly === 'boolean') { @@ -61,16 +57,11 @@ export class ListDatabasesOperation extends CommandOperation= 9 && this.options.comment !== undefined) { + if (maxWireVersion(connection) >= 9 && this.options.comment !== undefined) { cmd.comment = this.options.comment; } - return await (super.executeCommand( - server, - session, - cmd, - timeoutContext - ) as Promise); + return cmd; } } diff --git a/src/operations/rename.ts b/src/operations/rename.ts index 62659a7baf..64c751f426 100644 --- a/src/operations/rename.ts +++ b/src/operations/rename.ts @@ -1,10 +1,10 @@ +import { type Connection } from '..'; import type { Document } from '../bson'; +import { MongoDBResponse } from '../cmap/wire_protocol/responses'; import { Collection } from '../collection'; -import type { Server } from '../sdam/server'; import type { ClientSession } from '../sessions'; -import { type TimeoutContext } from '../timeout'; import { MongoDBNamespace } from '../utils'; -import { CommandOperation, type CommandOperationOptions } from './command'; +import { type CommandOperationOptions, ModernizedCommandOperation } from './command'; import { Aspect, defineAspects } from './operation'; /** @public */ @@ -16,7 +16,8 @@ export interface RenameOptions extends CommandOperationOptions { } /** @internal */ -export class RenameOperation extends CommandOperation { +export class RenameOperation extends ModernizedCommandOperation { + override SERVER_COMMAND_RESPONSE_TYPE = MongoDBResponse; collection: Collection; newName: string; override options: RenameOptions; @@ -33,24 +34,20 @@ export class RenameOperation extends CommandOperation { return 'renameCollection' as const; } - override async execute( - server: Server, - session: ClientSession | undefined, - timeoutContext: TimeoutContext - ): Promise { - // Build the command + override buildCommandDocument(_connection: Connection, _session?: ClientSession): Document { const renameCollection = this.collection.namespace; - const toCollection = this.collection.s.namespace.withCollection(this.newName).toString(); + const to = this.collection.s.namespace.withCollection(this.newName).toString(); const dropTarget = typeof this.options.dropTarget === 'boolean' ? this.options.dropTarget : false; - const command = { - renameCollection: renameCollection, - to: toCollection, - dropTarget: dropTarget + return { + renameCollection, + to, + dropTarget }; + } - await super.executeCommand(server, session, command, timeoutContext); + override handleOk(_response: InstanceType): Document { return new Collection(this.collection.s.db, this.newName, this.collection.s.options); } } diff --git a/src/operations/validate_collection.ts b/src/operations/validate_collection.ts index 16ae4cad9e..dd1bd6fecc 100644 --- a/src/operations/validate_collection.ts +++ b/src/operations/validate_collection.ts @@ -1,10 +1,10 @@ +import { type Connection } from '..'; import type { Admin } from '../admin'; -import type { Document } from '../bson'; +import { type Document } from '../bson'; +import { MongoDBResponse } from '../cmap/wire_protocol/responses'; import { MongoUnexpectedServerResponseError } from '../error'; -import type { Server } from '../sdam/server'; import type { ClientSession } from '../sessions'; -import { type TimeoutContext } from '../timeout'; -import { CommandOperation, type CommandOperationOptions } from './command'; +import { type CommandOperationOptions, ModernizedCommandOperation } from './command'; /** @public */ export interface ValidateCollectionOptions extends CommandOperationOptions { @@ -13,24 +13,14 @@ export interface ValidateCollectionOptions extends CommandOperationOptions { } /** @internal */ -export class ValidateCollectionOperation extends CommandOperation { +export class ValidateCollectionOperation extends ModernizedCommandOperation { + override SERVER_COMMAND_RESPONSE_TYPE = MongoDBResponse; override options: ValidateCollectionOptions; collectionName: string; - command: Document; constructor(admin: Admin, collectionName: string, options: ValidateCollectionOptions) { - // Decorate command with extra options - const command: Document = { validate: collectionName }; - const keys = Object.keys(options); - for (let i = 0; i < keys.length; i++) { - if (Object.prototype.hasOwnProperty.call(options, keys[i]) && keys[i] !== 'session') { - command[keys[i]] = (options as Document)[keys[i]]; - } - } - super(admin.s.db, options); this.options = options; - this.command = command; this.collectionName = collectionName; } @@ -38,21 +28,23 @@ export class ValidateCollectionOperation extends CommandOperation { return 'validate' as const; } - override async execute( - server: Server, - session: ClientSession | undefined, - timeoutContext: TimeoutContext - ): Promise { - const collectionName = this.collectionName; + override buildCommandDocument(_connection: Connection, _session?: ClientSession): Document { + // Decorate command with extra options + return { + validate: this.collectionName, + ...Object.fromEntries(Object.entries(this.options).filter(entry => entry[0] !== 'session')) + }; + } - const doc = await super.executeCommand(server, session, this.command, timeoutContext); - if (doc.result != null && typeof doc.result !== 'string') + override handleOk(response: InstanceType): Document { + const result = super.handleOk(response); + if (result.result != null && typeof result.result !== 'string') throw new MongoUnexpectedServerResponseError('Error with validation data'); - if (doc.result != null && doc.result.match(/exception|corrupt/) != null) - throw new MongoUnexpectedServerResponseError(`Invalid collection ${collectionName}`); - if (doc.valid != null && !doc.valid) - throw new MongoUnexpectedServerResponseError(`Invalid collection ${collectionName}`); + if (result.result != null && result.result.match(/exception|corrupt/) != null) + throw new MongoUnexpectedServerResponseError(`Invalid collection ${this.collectionName}`); + if (result.valid != null && !result.valid) + throw new MongoUnexpectedServerResponseError(`Invalid collection ${this.collectionName}`); - return doc; + return response; } } diff --git a/test/integration/node-specific/bson-options/use_bigint_64.test.ts b/test/integration/node-specific/bson-options/use_bigint_64.test.ts index c91a82fe5b..e4cc2d4241 100644 --- a/test/integration/node-specific/bson-options/use_bigint_64.test.ts +++ b/test/integration/node-specific/bson-options/use_bigint_64.test.ts @@ -171,10 +171,20 @@ describe('useBigInt64 option', function () { beforeEach(async function () { client = await this.configuration.newClient().connect(); db = client.db('bsonOptions', { promoteLongs: false, useBigInt64: true }); + + await db.createCollection('foo'); + await db.createCollection('bar'); + }); + + afterEach(async function () { + await db.dropDatabase(); }); it('throws a BSONError', async function () { - const e = await db.createCollection('bsonError').catch(e => e); + const e = await db + .listCollections() + .toArray() + .catch(e => e); expect(e).to.be.instanceOf(BSON.BSONError); }); }); @@ -186,8 +196,11 @@ describe('useBigInt64 option', function () { }); it('throws a BSONError', async function () { - const e = await db - .createCollection('bsonError', { promoteLongs: false, useBigInt64: true }) + const collection = db.collection('bsonError', { promoteLongs: false, useBigInt64: true }); + + const e = await collection + .insertOne({ name: 'bailey ' }) + .then(() => null) .catch(e => e); expect(e).to.be.instanceOf(BSON.BSONError); }); @@ -229,11 +242,21 @@ describe('useBigInt64 option', function () { describe('when set at DB level', function () { beforeEach(async function () { client = await this.configuration.newClient().connect(); - db = client.db('bsonOptions', { promoteValues: false, useBigInt64: true }); + db = client.db('bsonOptions', { promoteLongs: false, useBigInt64: true }); + + await db.createCollection('foo'); + await db.createCollection('bar'); + }); + + afterEach(async function () { + await db.dropDatabase(); }); it('throws a BSONError', async function () { - const e = await db.createCollection('bsonError').catch(e => e); + const e = await db + .listCollections() + .toArray() + .catch(e => e); expect(e).to.be.instanceOf(BSON.BSONError); }); }); @@ -245,8 +268,11 @@ describe('useBigInt64 option', function () { }); it('throws a BSONError', async function () { - const e = await db - .createCollection('bsonError', { promoteValues: false, useBigInt64: true }) + const collection = db.collection('bsonError', { promoteValues: false, useBigInt64: true }); + + const e = await collection + .insertOne({ name: 'bailey ' }) + .then(() => null) .catch(e => e); expect(e).to.be.instanceOf(BSON.BSONError); }); diff --git a/test/integration/node-specific/validate_collection.test.ts b/test/integration/node-specific/validate_collection.test.ts new file mode 100644 index 0000000000..b19f19e708 --- /dev/null +++ b/test/integration/node-specific/validate_collection.test.ts @@ -0,0 +1,84 @@ +import { expect } from 'chai'; + +import { ValidateCollectionOperation } from '../../mongodb'; + +describe('ValidateCollectionOperation', function () { + let client; + + beforeEach(function () { + client = this.configuration.newClient(); + }); + + afterEach(async function () { + await client.close(); + }); + + describe('buildCommandDocument()', function () { + it('builds the base command when no options are provided', function () { + client = this.configuration.newClient(); + const op = new ValidateCollectionOperation(client.db('foo').admin(), 'bar', {}); + + const doc = op.buildCommandDocument({} as any, {} as any); + expect(doc).to.deep.equal({ + validate: 'bar' + }); + }); + + it('supports background=true', function () { + client = this.configuration.newClient(); + const op = new ValidateCollectionOperation(client.db('foo').admin(), 'bar', { + background: true + }); + + const doc = op.buildCommandDocument({} as any, {} as any); + expect(doc).to.deep.equal({ + validate: 'bar', + background: true + }); + }); + + it('supports background=false', function () { + client = this.configuration.newClient(); + const op = new ValidateCollectionOperation(client.db('foo').admin(), 'bar', { + background: false + }); + + const doc = op.buildCommandDocument({} as any, {} as any); + expect(doc).to.deep.equal({ + validate: 'bar', + background: false + }); + }); + + it('attaches all options to the command document', function () { + client = this.configuration.newClient(); + const op = new ValidateCollectionOperation(client.db('foo').admin(), 'bar', { + background: false, + a: 1, + b: 2, + c: 3 + }); + + const doc = op.buildCommandDocument({} as any, {} as any); + expect(doc).to.deep.equal({ + validate: 'bar', + background: false, + a: 1, + b: 2, + c: 3 + }); + }); + + it('does not attach session command document', function () { + client = this.configuration.newClient(); + const op = new ValidateCollectionOperation(client.db('foo').admin(), 'bar', { + session: client.startSession() + }); + + const doc = op.buildCommandDocument({} as any, {} as any); + expect(doc).to.deep.equal({ + validate: 'bar' + }); + }); + }); +});