Skip to content

refactor(NODE-7085): refactor db collection management APIs to use modernized operation #4612

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Aug 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 22 additions & 31 deletions src/operations/create_collection.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<Collection> {
export class CreateCollectionOperation extends ModernizedCommandOperation<Collection> {
override SERVER_COMMAND_RESPONSE_TYPE = MongoDBResponse;
override options: CreateCollectionOptions;
db: Db;
name: string;
Expand All @@ -127,25 +130,19 @@ export class CreateCollectionOperation extends CommandOperation<Collection> {
return 'create' as const;
}

override async execute(
server: Server,
session: ClientSession | undefined,
timeoutContext: TimeoutContext
): Promise<Collection> {
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<typeof this.SERVER_COMMAND_RESPONSE_TYPE>
): Collection<Document> {
return new Collection(this.db, this.name, this.options);
}
}

Expand All @@ -167,23 +164,17 @@ export async function createCollections<TSchema extends Document>(

if (encryptedFields) {
class CreateSupportingFLEv2CollectionOperation extends CreateCollectionOperation {
override execute(
server: Server,
session: ClientSession | undefined,
timeoutContext: TimeoutContext
): Promise<Collection> {
// 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);
}
}

Expand Down
35 changes: 18 additions & 17 deletions src/operations/drop.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -17,7 +17,9 @@ export interface DropCollectionOptions extends CommandOperationOptions {
}

/** @internal */
export class DropCollectionOperation extends CommandOperation<boolean> {
export class DropCollectionOperation extends ModernizedCommandOperation<boolean> {
override SERVER_COMMAND_RESPONSE_TYPE = MongoDBResponse;

override options: DropCollectionOptions;
name: string;

Expand All @@ -31,12 +33,11 @@ export class DropCollectionOperation extends CommandOperation<boolean> {
return 'drop' as const;
}

override async execute(
server: Server,
session: ClientSession | undefined,
timeoutContext: TimeoutContext
): Promise<boolean> {
await super.executeCommand(server, session, { drop: this.name }, timeoutContext);
override buildCommandDocument(_connection: Connection, _session?: ClientSession): Document {
return { drop: this.name };
}

override handleOk(_response: InstanceType<typeof this.SERVER_COMMAND_RESPONSE_TYPE>): boolean {
return true;
}
}
Expand Down Expand Up @@ -106,7 +107,8 @@ export async function dropCollections(
export type DropDatabaseOptions = CommandOperationOptions;

/** @internal */
export class DropDatabaseOperation extends CommandOperation<boolean> {
export class DropDatabaseOperation extends ModernizedCommandOperation<boolean> {
override SERVER_COMMAND_RESPONSE_TYPE = MongoDBResponse;
override options: DropDatabaseOptions;

constructor(db: Db, options: DropDatabaseOptions) {
Expand All @@ -117,12 +119,11 @@ export class DropDatabaseOperation extends CommandOperation<boolean> {
return 'dropDatabase' as const;
}

override async execute(
server: Server,
session: ClientSession | undefined,
timeoutContext: TimeoutContext
): Promise<boolean> {
await super.executeCommand(server, session, { dropDatabase: 1 }, timeoutContext);
override buildCommandDocument(_connection: Connection, _session?: ClientSession): Document {
return { dropDatabase: 1 };
}

override handleOk(_response: InstanceType<typeof this.SERVER_COMMAND_RESPONSE_TYPE>): boolean {
return true;
}
}
Expand Down
25 changes: 8 additions & 17 deletions src/operations/list_databases.ts
Original file line number Diff line number Diff line change
@@ -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 */
Expand All @@ -27,7 +26,8 @@ export interface ListDatabasesOptions extends CommandOperationOptions {
}

/** @internal */
export class ListDatabasesOperation extends CommandOperation<ListDatabasesResult> {
export class ListDatabasesOperation extends ModernizedCommandOperation<ListDatabasesResult> {
override SERVER_COMMAND_RESPONSE_TYPE = MongoDBResponse;
override options: ListDatabasesOptions;

constructor(db: Db, options?: ListDatabasesOptions) {
Expand All @@ -40,11 +40,7 @@ export class ListDatabasesOperation extends CommandOperation<ListDatabasesResult
return 'listDatabases' as const;
}

override async execute(
server: Server,
session: ClientSession | undefined,
timeoutContext: TimeoutContext
): Promise<ListDatabasesResult> {
override buildCommandDocument(connection: Connection, _session?: ClientSession): Document {
const cmd: Document = { listDatabases: 1 };

if (typeof this.options.nameOnly === 'boolean') {
Expand All @@ -61,16 +57,11 @@ export class ListDatabasesOperation extends CommandOperation<ListDatabasesResult

// we check for undefined specifically here to allow falsy values
// eslint-disable-next-line no-restricted-syntax
if (maxWireVersion(server) >= 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<TODO_NODE_3286>);
return cmd;
}
}

Expand Down
29 changes: 13 additions & 16 deletions src/operations/rename.ts
Original file line number Diff line number Diff line change
@@ -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 */
Expand All @@ -16,7 +16,8 @@ export interface RenameOptions extends CommandOperationOptions {
}

/** @internal */
export class RenameOperation extends CommandOperation<Document> {
export class RenameOperation extends ModernizedCommandOperation<Document> {
override SERVER_COMMAND_RESPONSE_TYPE = MongoDBResponse;
collection: Collection;
newName: string;
override options: RenameOptions;
Expand All @@ -33,24 +34,20 @@ export class RenameOperation extends CommandOperation<Document> {
return 'renameCollection' as const;
}

override async execute(
server: Server,
session: ClientSession | undefined,
timeoutContext: TimeoutContext
): Promise<Collection> {
// 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<typeof this.SERVER_COMMAND_RESPONSE_TYPE>): Document {
return new Collection(this.collection.s.db, this.newName, this.collection.s.options);
}
}
Expand Down
50 changes: 21 additions & 29 deletions src/operations/validate_collection.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -13,46 +13,38 @@ export interface ValidateCollectionOptions extends CommandOperationOptions {
}

/** @internal */
export class ValidateCollectionOperation extends CommandOperation<Document> {
export class ValidateCollectionOperation extends ModernizedCommandOperation<Document> {
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;
}

override get commandName() {
return 'validate' as const;
}

override async execute(
server: Server,
session: ClientSession | undefined,
timeoutContext: TimeoutContext
): Promise<Document> {
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<typeof this.SERVER_COMMAND_RESPONSE_TYPE>): 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;
}
}
Loading