diff --git a/.evergreen/config.in.yml b/.evergreen/config.in.yml index 36c567727e..ce33990284 100644 --- a/.evergreen/config.in.yml +++ b/.evergreen/config.in.yml @@ -45,6 +45,15 @@ functions: params: file: src/expansion.yml + "run search index management tests": + - command: subprocess.exec + params: + binary: bash + working_dir: src + add_expansions_to_env: true + args: + - .evergreen/run-search-index-management-tests.sh + "bootstrap mongo-orchestration": - command: subprocess.exec params: diff --git a/.evergreen/config.yml b/.evergreen/config.yml index d4907b1415..4ac62fd3e4 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -25,6 +25,14 @@ functions: - command: expansions.update params: file: src/expansion.yml + run search index management tests: + - command: subprocess.exec + params: + binary: bash + working_dir: src + add_expansions_to_env: true + args: + - .evergreen/run-search-index-management-tests.sh bootstrap mongo-orchestration: - command: subprocess.exec params: @@ -2596,6 +2604,17 @@ tasks: variant: '*' status: '*' patch_optional: true + - name: test-search-index-helpers + tags: [] + commands: + - func: install dependencies + vars: + NODE_LTS_NAME: 20 + - func: bootstrap mongo-orchestration + vars: + VERSION: latest + TOPOLOGY: replica_set + - func: run search index management tests - name: run-custom-csfle-tests-5.0-pinned-commit tags: - run-custom-dependency-tests @@ -4008,3 +4027,8 @@ buildvariants: - test-lambda-example - test-lambda-aws-auth-example - test-deployed-lambda + - name: rhel8-test-seach-index-management-helpers + display_name: Search Index Management Helpers Tests + run_on: rhel80-large + tasks: + - test-search-index-helpers diff --git a/.evergreen/generate_evergreen_tasks.js b/.evergreen/generate_evergreen_tasks.js index f15b84b24b..78f660cc3c 100644 --- a/.evergreen/generate_evergreen_tasks.js +++ b/.evergreen/generate_evergreen_tasks.js @@ -699,6 +699,26 @@ const coverageTask = { }; SINGLETON_TASKS.push(coverageTask); +SINGLETON_TASKS.push({ + name: 'test-search-index-helpers', + tags: [], + commands: [ + { + func: 'install dependencies', + vars: { + NODE_LTS_NAME: LATEST_LTS + } + }, + { + func: 'bootstrap mongo-orchestration', + vars: { + VERSION: 'latest', + TOPOLOGY: 'replica_set' + } + }, + { func: 'run search index management tests' } + ] +}) SINGLETON_TASKS.push(...oneOffFuncAsTasks); BUILD_VARIANTS.push({ @@ -751,6 +771,13 @@ BUILD_VARIANTS.push({ tasks: ['test-lambda-example', 'test-lambda-aws-auth-example', 'test-deployed-lambda'] }); +BUILD_VARIANTS.push({ + name: 'rhel8-test-seach-index-management-helpers', + display_name: 'Search Index Management Helpers Tests', + run_on: DEFAULT_OS, + tasks: ['test-search-index-helpers'] +}) + // TODO(NODE-4575): unskip zstd and snappy on node 16 for (const variant of BUILD_VARIANTS.filter( variant => diff --git a/.evergreen/run-search-index-management-tests.sh b/.evergreen/run-search-index-management-tests.sh new file mode 100644 index 0000000000..825e5732ad --- /dev/null +++ b/.evergreen/run-search-index-management-tests.sh @@ -0,0 +1,5 @@ +#! /bin/bash + +source "${PROJECT_DIRECTORY}/.evergreen/init-nvm.sh" + +npm run check:search-indexes diff --git a/package.json b/package.json index 264b14268f..dccfc836e3 100644 --- a/package.json +++ b/package.json @@ -120,6 +120,7 @@ "check:tsd": "tsd --version && tsd", "check:dependencies": "mocha test/action/dependency.test.ts", "check:dts": "node ./node_modules/typescript/bin/tsc --noEmit mongodb.d.ts && tsd", + "check:search-indexes": "nyc mocha --config test/mocha_mongodb.json test/manual/search-index-management.spec.test.ts", "check:test": "mocha --config test/mocha_mongodb.json test/integration", "check:unit": "mocha test/unit", "check:ts": "node ./node_modules/typescript/bin/tsc -v && node ./node_modules/typescript/bin/tsc --noEmit", diff --git a/src/admin.ts b/src/admin.ts index a6bad5885e..224deaa54a 100644 --- a/src/admin.ts +++ b/src/admin.ts @@ -75,7 +75,7 @@ export class Admin { */ async command(command: Document, options?: RunCommandOptions): Promise { return executeOperation( - this.s.db.s.client, + this.s.db.client, new RunCommandOperation(this.s.db, command, { dbName: 'admin', ...options }) ); } @@ -138,7 +138,7 @@ export class Admin { : undefined; const password = typeof passwordOrOptions === 'string' ? passwordOrOptions : undefined; return executeOperation( - this.s.db.s.client, + this.s.db.client, new AddUserOperation(this.s.db, username, password, { dbName: 'admin', ...options }) ); } @@ -151,7 +151,7 @@ export class Admin { */ async removeUser(username: string, options?: RemoveUserOptions): Promise { return executeOperation( - this.s.db.s.client, + this.s.db.client, new RemoveUserOperation(this.s.db, username, { dbName: 'admin', ...options }) ); } @@ -167,7 +167,7 @@ export class Admin { options: ValidateCollectionOptions = {} ): Promise { return executeOperation( - this.s.db.s.client, + this.s.db.client, new ValidateCollectionOperation(this, collectionName, options) ); } @@ -178,7 +178,7 @@ export class Admin { * @param options - Optional settings for the command */ async listDatabases(options?: ListDatabasesOptions): Promise { - return executeOperation(this.s.db.s.client, new ListDatabasesOperation(this.s.db, options)); + return executeOperation(this.s.db.client, new ListDatabasesOperation(this.s.db, options)); } /** diff --git a/src/bulk/common.ts b/src/bulk/common.ts index f8a46a8f14..7436a88de6 100644 --- a/src/bulk/common.ts +++ b/src/bulk/common.ts @@ -597,19 +597,19 @@ function executeCommands( try { if (isInsertBatch(batch)) { executeOperation( - bulkOperation.s.collection.s.db.s.client, + bulkOperation.s.collection.client, new InsertOperation(bulkOperation.s.namespace, batch.operations, finalOptions), resultHandler ); } else if (isUpdateBatch(batch)) { executeOperation( - bulkOperation.s.collection.s.db.s.client, + bulkOperation.s.collection.client, new UpdateOperation(bulkOperation.s.namespace, batch.operations, finalOptions), resultHandler ); } else if (isDeleteBatch(batch)) { executeOperation( - bulkOperation.s.collection.s.db.s.client, + bulkOperation.s.collection.client, new DeleteOperation(bulkOperation.s.namespace, batch.operations, finalOptions), resultHandler ); @@ -1222,7 +1222,7 @@ export abstract class BulkOperationBase { const finalOptions = { ...this.s.options, ...options }; const operation = new BulkWriteShimOperation(this, finalOptions); - return executeOperation(this.s.collection.s.db.s.client, operation); + return executeOperation(this.s.collection.client, operation); } /** diff --git a/src/change_stream.ts b/src/change_stream.ts index ad77624d31..c002c60858 100644 --- a/src/change_stream.ts +++ b/src/change_stream.ts @@ -818,9 +818,9 @@ export class ChangeStream< this.type === CHANGE_DOMAIN_TYPES.CLUSTER ? (this.parent as MongoClient) : this.type === CHANGE_DOMAIN_TYPES.DATABASE - ? (this.parent as Db).s.client + ? (this.parent as Db).client : this.type === CHANGE_DOMAIN_TYPES.COLLECTION - ? (this.parent as Collection).s.db.s.client + ? (this.parent as Collection).client : null; if (client == null) { diff --git a/src/collection.ts b/src/collection.ts index 95b7447bf9..8736cc2153 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -6,9 +6,13 @@ import { ChangeStream, ChangeStreamDocument, ChangeStreamOptions } from './chang import { AggregationCursor } from './cursor/aggregation_cursor'; import { FindCursor } from './cursor/find_cursor'; import { ListIndexesCursor } from './cursor/list_indexes_cursor'; +import { + ListSearchIndexesCursor, + ListSearchIndexesOptions +} from './cursor/list_search_indexes_cursor'; import type { Db } from './db'; import { MongoInvalidArgumentError } from './error'; -import type { PkFactory } from './mongo_client'; +import type { MongoClient, PkFactory } from './mongo_client'; import type { Filter, Flatten, @@ -70,6 +74,12 @@ import { IsCappedOperation } from './operations/is_capped'; import type { Hint, OperationOptions } from './operations/operation'; import { OptionsOperation } from './operations/options_operation'; import { RenameOperation, RenameOptions } from './operations/rename'; +import { + CreateSearchIndexesOperation, + SearchIndexDescription +} from './operations/search_indexes/create'; +import { DropSearchIndexOperation } from './operations/search_indexes/drop'; +import { UpdateSearchIndexOperation } from './operations/search_indexes/update'; import { CollStats, CollStatsOperation, CollStatsOptions } from './operations/stats'; import { ReplaceOneOperation, @@ -84,7 +94,7 @@ import { ReadPreference, ReadPreferenceLike } from './read_preference'; import { checkCollectionName, DEFAULT_PK_FACTORY, - MongoDBNamespace, + MongoDBCollectionNamespace, normalizeHintField, resolveOptions } from './utils'; @@ -115,7 +125,7 @@ export interface CollectionPrivate { pkFactory: PkFactory; db: Db; options: any; - namespace: MongoDBNamespace; + namespace: MongoDBCollectionNamespace; readPreference?: ReadPreference; bsonOptions: BSONSerializeOptions; collectionHint?: Hint; @@ -153,6 +163,9 @@ export class Collection { /** @internal */ s: CollectionPrivate; + /** @internal */ + client: MongoClient; + /** * Create a new Collection instance * @internal @@ -164,13 +177,15 @@ export class Collection { this.s = { db, options, - namespace: new MongoDBNamespace(db.databaseName, name), + namespace: new MongoDBCollectionNamespace(db.databaseName, name), pkFactory: db.options?.pkFactory ?? DEFAULT_PK_FACTORY, readPreference: ReadPreference.fromOptions(options), bsonOptions: resolveBSONOptions(options, db), readConcern: ReadConcern.fromOptions(options), writeConcern: WriteConcern.fromOptions(options) }; + + this.client = db.client; } /** @@ -184,15 +199,23 @@ export class Collection { * The name of this collection */ get collectionName(): string { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return this.s.namespace.collection!; + return this.s.namespace.collection; } /** * The namespace of this collection, in the format `${this.dbName}.${this.collectionName}` */ get namespace(): string { - return this.s.namespace.toString(); + return this.fullNamespace.toString(); + } + + /** + * @internal + * + * The `MongoDBNamespace` for the collection. + */ + get fullNamespace(): MongoDBCollectionNamespace { + return this.s.namespace; } /** @@ -255,7 +278,7 @@ export class Collection { options?: InsertOneOptions ): Promise> { return executeOperation( - this.s.db.s.client, + this.client, new InsertOneOperation( this as TODO_NODE_3286, doc, @@ -277,7 +300,7 @@ export class Collection { options?: BulkWriteOptions ): Promise> { return executeOperation( - this.s.db.s.client, + this.client, new InsertManyOperation( this as TODO_NODE_3286, docs, @@ -314,7 +337,7 @@ export class Collection { } return executeOperation( - this.s.db.s.client, + this.client, new BulkWriteOperation( this as TODO_NODE_3286, operations as TODO_NODE_3286, @@ -336,7 +359,7 @@ export class Collection { options?: UpdateOptions ): Promise> { return executeOperation( - this.s.db.s.client, + this.client, new UpdateOneOperation( this as TODO_NODE_3286, filter, @@ -359,7 +382,7 @@ export class Collection { options?: ReplaceOptions ): Promise | Document> { return executeOperation( - this.s.db.s.client, + this.client, new ReplaceOneOperation( this as TODO_NODE_3286, filter, @@ -382,7 +405,7 @@ export class Collection { options?: UpdateOptions ): Promise> { return executeOperation( - this.s.db.s.client, + this.client, new UpdateManyOperation( this as TODO_NODE_3286, filter, @@ -403,7 +426,7 @@ export class Collection { options: DeleteOptions = {} ): Promise { return executeOperation( - this.s.db.s.client, + this.client, new DeleteOneOperation(this as TODO_NODE_3286, filter, resolveOptions(this, options)) ); } @@ -419,7 +442,7 @@ export class Collection { options: DeleteOptions = {} ): Promise { return executeOperation( - this.s.db.s.client, + this.client, new DeleteManyOperation(this as TODO_NODE_3286, filter, resolveOptions(this, options)) ); } @@ -436,7 +459,7 @@ export class Collection { async rename(newName: string, options?: RenameOptions): Promise { // Intentionally, we do not inherit options from parent for this operation. return executeOperation( - this.s.db.s.client, + this.client, new RenameOperation(this as TODO_NODE_3286, newName, { ...options, readPreference: ReadPreference.PRIMARY @@ -451,7 +474,7 @@ export class Collection { */ async drop(options?: DropCollectionOptions): Promise { return executeOperation( - this.s.db.s.client, + this.client, new DropCollectionOperation(this.s.db, this.collectionName, options) ); } @@ -488,7 +511,7 @@ export class Collection { find(filter: Filter, options?: FindOptions): FindCursor; find(filter: Filter = {}, options: FindOptions = {}): FindCursor> { return new FindCursor>( - this.s.db.s.client, + this.client, this.s.namespace, filter, resolveOptions(this as TODO_NODE_3286, options) @@ -502,7 +525,7 @@ export class Collection { */ async options(options?: OperationOptions): Promise { return executeOperation( - this.s.db.s.client, + this.client, new OptionsOperation(this as TODO_NODE_3286, resolveOptions(this, options)) ); } @@ -514,7 +537,7 @@ export class Collection { */ async isCapped(options?: OperationOptions): Promise { return executeOperation( - this.s.db.s.client, + this.client, new IsCappedOperation(this as TODO_NODE_3286, resolveOptions(this, options)) ); } @@ -552,7 +575,7 @@ export class Collection { options?: CreateIndexesOptions ): Promise { return executeOperation( - this.s.db.s.client, + this.client, new CreateIndexOperation( this as TODO_NODE_3286, this.collectionName, @@ -598,7 +621,7 @@ export class Collection { options?: CreateIndexesOptions ): Promise { return executeOperation( - this.s.db.s.client, + this.client, new CreateIndexesOperation( this as TODO_NODE_3286, this.collectionName, @@ -616,7 +639,7 @@ export class Collection { */ async dropIndex(indexName: string, options?: DropIndexesOptions): Promise { return executeOperation( - this.s.db.s.client, + this.client, new DropIndexOperation(this as TODO_NODE_3286, indexName, { ...resolveOptions(this, options), readPreference: ReadPreference.primary @@ -631,7 +654,7 @@ export class Collection { */ async dropIndexes(options?: DropIndexesOptions): Promise { return executeOperation( - this.s.db.s.client, + this.client, new DropIndexesOperation(this as TODO_NODE_3286, resolveOptions(this, options)) ); } @@ -656,7 +679,7 @@ export class Collection { options?: IndexInformationOptions ): Promise { return executeOperation( - this.s.db.s.client, + this.client, new IndexExistsOperation(this as TODO_NODE_3286, indexes, resolveOptions(this, options)) ); } @@ -668,7 +691,7 @@ export class Collection { */ async indexInformation(options?: IndexInformationOptions): Promise { return executeOperation( - this.s.db.s.client, + this.client, new IndexInformationOperation(this.s.db, this.collectionName, resolveOptions(this, options)) ); } @@ -688,7 +711,7 @@ export class Collection { */ async estimatedDocumentCount(options?: EstimatedDocumentCountOptions): Promise { return executeOperation( - this.s.db.s.client, + this.client, new EstimatedDocumentCountOperation(this as TODO_NODE_3286, resolveOptions(this, options)) ); } @@ -723,7 +746,7 @@ export class Collection { options: CountDocumentsOptions = {} ): Promise { return executeOperation( - this.s.db.s.client, + this.client, new CountDocumentsOperation(this as TODO_NODE_3286, filter, resolveOptions(this, options)) ); } @@ -759,7 +782,7 @@ export class Collection { options: DistinctOptions = {} ): Promise { return executeOperation( - this.s.db.s.client, + this.client, new DistinctOperation( this as TODO_NODE_3286, key as TODO_NODE_3286, @@ -776,7 +799,7 @@ export class Collection { */ async indexes(options?: IndexInformationOptions): Promise { return executeOperation( - this.s.db.s.client, + this.client, new IndexesOperation(this as TODO_NODE_3286, resolveOptions(this, options)) ); } @@ -791,7 +814,7 @@ export class Collection { */ async stats(options?: CollStatsOptions): Promise { return executeOperation( - this.s.db.s.client, + this.client, new CollStatsOperation(this as TODO_NODE_3286, options) as TODO_NODE_3286 ); } @@ -807,7 +830,7 @@ export class Collection { options?: FindOneAndDeleteOptions ): Promise> { return executeOperation( - this.s.db.s.client, + this.client, new FindOneAndDeleteOperation( this as TODO_NODE_3286, filter, @@ -829,7 +852,7 @@ export class Collection { options?: FindOneAndReplaceOptions ): Promise> { return executeOperation( - this.s.db.s.client, + this.client, new FindOneAndReplaceOperation( this as TODO_NODE_3286, filter, @@ -852,7 +875,7 @@ export class Collection { options?: FindOneAndUpdateOptions ): Promise> { return executeOperation( - this.s.db.s.client, + this.client, new FindOneAndUpdateOperation( this as TODO_NODE_3286, filter, @@ -879,7 +902,7 @@ export class Collection { } return new AggregationCursor( - this.s.db.s.client, + this.client, this.s.namespace, pipeline, resolveOptions(this, options) @@ -975,12 +998,111 @@ export class Collection { */ async count(filter: Filter = {}, options: CountOptions = {}): Promise { return executeOperation( - this.s.db.s.client, - new CountOperation( - MongoDBNamespace.fromString(this.namespace), - filter, - resolveOptions(this, options) - ) + this.client, + new CountOperation(this.fullNamespace, filter, resolveOptions(this, options)) + ); + } + + /** + * @internal + * + * Returns all search indexes for the current collection. + * + * @param options - The options for the list indexes operation. + * + * @remarks Only available when used against a 7.0+ Atlas cluster. + */ + listSearchIndexes(options?: ListSearchIndexesOptions): ListSearchIndexesCursor; + /** + * @internal + * + * Returns all search indexes for the current collection. + * + * @param name - The name of the index to search for. Only indexes with matching index names will be returned. + * @param options - The options for the list indexes operation. + * + * @remarks Only available when used against a 7.0+ Atlas cluster. + */ + listSearchIndexes(name: string, options?: ListSearchIndexesOptions): ListSearchIndexesCursor; + listSearchIndexes( + indexNameOrOptions?: string | ListSearchIndexesOptions, + options?: ListSearchIndexesOptions + ): ListSearchIndexesCursor { + options = + typeof indexNameOrOptions === 'object' ? indexNameOrOptions : options == null ? {} : options; + const indexName = + indexNameOrOptions == null + ? null + : typeof indexNameOrOptions === 'object' + ? null + : indexNameOrOptions; + + return new ListSearchIndexesCursor(this as TODO_NODE_3286, indexName, options); + } + + /** + * @internal + * + * Creates a single search index for the collection. + * + * @param description - The index description for the new search index. + * @returns A promise that resolves to the name of the new search index. + * + * @remarks Only available when used against a 7.0+ Atlas cluster. + */ + async createSearchIndex(description: SearchIndexDescription): Promise { + const [index] = await this.createSearchIndexes([description]); + return index; + } + + /** + * @internal + * + * Creates multiple search indexes for the current collection. + * + * @param descriptions - An array of `SearchIndexDescription`s for the new search indexes. + * @returns A promise that resolves to an array of the newly created search index names. + * + * @remarks Only available when used against a 7.0+ Atlas cluster. + * @returns + */ + async createSearchIndexes(descriptions: SearchIndexDescription[]): Promise { + return executeOperation( + this.client, + new CreateSearchIndexesOperation(this as TODO_NODE_3286, descriptions) + ); + } + + /** + * @internal + * + * Deletes a search index by index name. + * + * @param name - The name of the search index to be deleted. + * + * @remarks Only available when used against a 7.0+ Atlas cluster. + */ + async dropSearchIndex(name: string): Promise { + return executeOperation( + this.client, + new DropSearchIndexOperation(this as TODO_NODE_3286, name) + ); + } + + /** + * @internal + * + * Updates a search index by replacing the existing index definition with the provided definition. + * + * @param name - The name of the search index to update. + * @param definition - The new search index definition. + * + * @remarks Only available when used against a 7.0+ Atlas cluster. + */ + async updateSearchIndex(name: string, definition: Document): Promise { + return executeOperation( + this.client, + new UpdateSearchIndexOperation(this as TODO_NODE_3286, name, definition) ); } } diff --git a/src/cursor/list_collections_cursor.ts b/src/cursor/list_collections_cursor.ts index 460126be8a..0af06df541 100644 --- a/src/cursor/list_collections_cursor.ts +++ b/src/cursor/list_collections_cursor.ts @@ -21,7 +21,7 @@ export class ListCollectionsCursor< options?: ListCollectionsOptions; constructor(db: Db, filter: Document, options?: ListCollectionsOptions) { - super(db.s.client, db.s.namespace, options); + super(db.client, db.s.namespace, options); this.parent = db; this.filter = filter; this.options = options; @@ -42,7 +42,7 @@ export class ListCollectionsCursor< session }); - executeOperation(this.parent.s.client, operation, (err, response) => { + executeOperation(this.parent.client, operation, (err, response) => { if (err || response == null) return callback(err); // TODO: NODE-2882 diff --git a/src/cursor/list_indexes_cursor.ts b/src/cursor/list_indexes_cursor.ts index 25336d84dd..4919a0cf16 100644 --- a/src/cursor/list_indexes_cursor.ts +++ b/src/cursor/list_indexes_cursor.ts @@ -11,7 +11,7 @@ export class ListIndexesCursor extends AbstractCursor { options?: ListIndexesOptions; constructor(collection: Collection, options?: ListIndexesOptions) { - super(collection.s.db.s.client, collection.s.namespace, options); + super(collection.client, collection.s.namespace, options); this.parent = collection; this.options = options; } @@ -31,7 +31,7 @@ export class ListIndexesCursor extends AbstractCursor { session }); - executeOperation(this.parent.s.db.s.client, operation, (err, response) => { + executeOperation(this.parent.client, operation, (err, response) => { if (err || response == null) return callback(err); // TODO: NODE-2882 diff --git a/src/cursor/list_search_indexes_cursor.ts b/src/cursor/list_search_indexes_cursor.ts new file mode 100644 index 0000000000..285af02ef3 --- /dev/null +++ b/src/cursor/list_search_indexes_cursor.ts @@ -0,0 +1,20 @@ +import type { Collection } from '../collection'; +import type { AggregateOptions } from '../operations/aggregate'; +import { AggregationCursor } from './aggregation_cursor'; + +/** @internal */ +export type ListSearchIndexesOptions = AggregateOptions; + +/** @internal */ +export class ListSearchIndexesCursor extends AggregationCursor<{ name: string }> { + /** @internal */ + constructor( + { fullNamespace: ns, client }: Collection, + name: string | null, + options: ListSearchIndexesOptions = {} + ) { + const pipeline = + name == null ? [{ $listSearchIndexes: {} }] : [{ $listSearchIndexes: { name } }]; + super(client, ns, pipeline, options); + } +} diff --git a/src/cursor/run_command_cursor.ts b/src/cursor/run_command_cursor.ts index 95dcef4983..9dbc85b2d4 100644 --- a/src/cursor/run_command_cursor.ts +++ b/src/cursor/run_command_cursor.ts @@ -96,7 +96,7 @@ export class RunCommandCursor extends AbstractCursor { /** @internal */ constructor(db: Db, command: Document, options: RunCursorCommandOptions = {}) { - super(db.s.client, ns(db.namespace), options); + super(db.client, ns(db.namespace), options); this.db = db; this.command = Object.freeze({ ...command }); } diff --git a/src/db.ts b/src/db.ts index 548d183094..1d02649e04 100644 --- a/src/db.ts +++ b/src/db.ts @@ -70,7 +70,6 @@ const DB_OPTIONS_ALLOW_LIST = [ /** @internal */ export interface DbPrivate { - client: MongoClient; options?: DbOptions; readPreference?: ReadPreference; pkFactory: PkFactory; @@ -122,6 +121,9 @@ export class Db { /** @internal */ s: DbPrivate; + /** @internal */ + readonly client: MongoClient; + public static SYSTEM_NAMESPACE_COLLECTION = CONSTANTS.SYSTEM_NAMESPACE_COLLECTION; public static SYSTEM_INDEX_COLLECTION = CONSTANTS.SYSTEM_INDEX_COLLECTION; public static SYSTEM_PROFILE_COLLECTION = CONSTANTS.SYSTEM_PROFILE_COLLECTION; @@ -147,8 +149,6 @@ export class Db { // Internal state of the db object this.s = { - // Client - client, // Options options, // Unpack read preference @@ -163,6 +163,8 @@ export class Db { // Namespace namespace: new MongoDBNamespace(databaseName) }; + + this.client = client; } get databaseName(): string { @@ -191,7 +193,7 @@ export class Db { */ get readPreference(): ReadPreference { if (this.s.readPreference == null) { - return this.s.client.readPreference; + return this.client.readPreference; } return this.s.readPreference; @@ -222,7 +224,7 @@ export class Db { options?: CreateCollectionOptions ): Promise> { return executeOperation( - this.s.client, + this.client, new CreateCollectionOperation(this, name, resolveOptions(this, options)) as TODO_NODE_3286 ); } @@ -254,7 +256,7 @@ export class Db { */ async command(command: Document, options?: RunCommandOptions): Promise { // Intentionally, we do not inherit options from parent for this operation. - return executeOperation(this.s.client, new RunCommandOperation(this, command, options)); + return executeOperation(this.client, new RunCommandOperation(this, command, options)); } /** @@ -268,7 +270,7 @@ export class Db { options?: AggregateOptions ): AggregationCursor { return new AggregationCursor( - this.s.client, + this.client, this.s.namespace, pipeline, resolveOptions(this, options) @@ -302,10 +304,7 @@ export class Db { * @param options - Optional settings for the command */ async stats(options?: DbStatsOptions): Promise { - return executeOperation( - this.s.client, - new DbStatsOperation(this, resolveOptions(this, options)) - ); + return executeOperation(this.client, new DbStatsOperation(this, resolveOptions(this, options))); } /** @@ -352,7 +351,7 @@ export class Db { ): Promise> { // Intentionally, we do not inherit options from parent for this operation. return executeOperation( - this.s.client, + this.client, new RenameOperation( this.collection(fromCollection) as TODO_NODE_3286, toCollection, @@ -369,7 +368,7 @@ export class Db { */ async dropCollection(name: string, options?: DropCollectionOptions): Promise { return executeOperation( - this.s.client, + this.client, new DropCollectionOperation(this, name, resolveOptions(this, options)) ); } @@ -381,7 +380,7 @@ export class Db { */ async dropDatabase(options?: DropDatabaseOptions): Promise { return executeOperation( - this.s.client, + this.client, new DropDatabaseOperation(this, resolveOptions(this, options)) ); } @@ -393,7 +392,7 @@ export class Db { */ async collections(options?: ListCollectionsOptions): Promise { return executeOperation( - this.s.client, + this.client, new CollectionsOperation(this, resolveOptions(this, options)) ); } @@ -411,7 +410,7 @@ export class Db { options?: CreateIndexesOptions ): Promise { return executeOperation( - this.s.client, + this.client, new CreateIndexOperation(this, name, indexSpec, resolveOptions(this, options)) ); } @@ -438,7 +437,7 @@ export class Db { : undefined; const password = typeof passwordOrOptions === 'string' ? passwordOrOptions : undefined; return executeOperation( - this.s.client, + this.client, new AddUserOperation(this, username, password, resolveOptions(this, options)) ); } @@ -451,7 +450,7 @@ export class Db { */ async removeUser(username: string, options?: RemoveUserOptions): Promise { return executeOperation( - this.s.client, + this.client, new RemoveUserOperation(this, username, resolveOptions(this, options)) ); } @@ -467,7 +466,7 @@ export class Db { options?: SetProfilingLevelOptions ): Promise { return executeOperation( - this.s.client, + this.client, new SetProfilingLevelOperation(this, level, resolveOptions(this, options)) ); } @@ -479,7 +478,7 @@ export class Db { */ async profilingLevel(options?: ProfilingLevelOptions): Promise { return executeOperation( - this.s.client, + this.client, new ProfilingLevelOperation(this, resolveOptions(this, options)) ); } @@ -492,7 +491,7 @@ export class Db { */ async indexInformation(name: string, options?: IndexInformationOptions): Promise { return executeOperation( - this.s.client, + this.client, new IndexInformationOperation(this, name, resolveOptions(this, options)) ); } diff --git a/src/index.ts b/src/index.ts index 7917e23539..12346002ed 100644 --- a/src/index.ts +++ b/src/index.ts @@ -277,6 +277,10 @@ export type { ChangeStreamAggregateRawResult, ChangeStreamCursorOptions } from './cursor/change_stream_cursor'; +export type { + ListSearchIndexesCursor, + ListSearchIndexesOptions +} from './cursor/list_search_indexes_cursor'; export type { RunCursorCommandOptions } from './cursor/run_command_cursor'; export type { DbOptions, DbPrivate } from './db'; export type { AutoEncrypter, AutoEncryptionOptions, AutoEncryptionTlsOptions } from './deps'; @@ -418,6 +422,7 @@ export type { ProfilingLevelOptions } from './operations/profiling_level'; export type { RemoveUserOptions } from './operations/remove_user'; export type { RenameOptions } from './operations/rename'; export type { RunCommandOptions } from './operations/run_command'; +export type { SearchIndexDescription } from './operations/search_indexes/create'; export type { SetProfilingLevelOptions } from './operations/set_profiling_level'; export type { CollStats, @@ -489,6 +494,7 @@ export type { EventEmitterWithState, HostAddress, List, + MongoDBCollectionNamespace, MongoDBNamespace } from './utils'; export type { W, WriteConcernOptions, WriteConcernSettings } from './write_concern'; diff --git a/src/operations/create_collection.ts b/src/operations/create_collection.ts index ebc99bc600..bd0af76424 100644 --- a/src/operations/create_collection.ts +++ b/src/operations/create_collection.ts @@ -133,7 +133,7 @@ export class CreateCollectionOperation extends CommandOperation { const encryptedFields: Document | undefined = options.encryptedFields ?? - db.s.client.options.autoEncryption?.encryptedFieldsMap?.[`${db.databaseName}.${name}`]; + db.client.options.autoEncryption?.encryptedFieldsMap?.[`${db.databaseName}.${name}`]; if (encryptedFields) { // Creating a QE collection required min server of 7.0.0 diff --git a/src/operations/drop.ts b/src/operations/drop.ts index b22fe65cde..65e2f95485 100644 --- a/src/operations/drop.ts +++ b/src/operations/drop.ts @@ -36,7 +36,7 @@ export class DropCollectionOperation extends CommandOperation { const options = this.options; const name = this.name; - const encryptedFieldsMap = db.s.client.options.autoEncryption?.encryptedFieldsMap; + const encryptedFieldsMap = db.client.options.autoEncryption?.encryptedFieldsMap; let encryptedFields: Document | undefined = options.encryptedFields ?? encryptedFieldsMap?.[`${db.databaseName}.${name}`]; diff --git a/src/operations/search_indexes/create.ts b/src/operations/search_indexes/create.ts new file mode 100644 index 0000000000..cce926de88 --- /dev/null +++ b/src/operations/search_indexes/create.ts @@ -0,0 +1,48 @@ +import type { Document } from 'bson'; + +import type { Collection } from '../../collection'; +import type { Server } from '../../sdam/server'; +import type { ClientSession } from '../../sessions'; +import type { Callback } from '../../utils'; +import { AbstractOperation } from '../operation'; + +/** @internal */ +export interface SearchIndexDescription { + /** The name of the index. */ + name?: string; + + /** The index definition. */ + description: Document; +} + +/** @internal */ +export class CreateSearchIndexesOperation extends AbstractOperation { + constructor( + private readonly collection: Collection, + private readonly descriptions: ReadonlyArray + ) { + super(); + } + + execute(server: Server, session: ClientSession | undefined, callback: Callback): void { + const namespace = this.collection.fullNamespace; + const command = { + createSearchIndexes: namespace.collection, + indexes: this.descriptions + }; + + server.command(namespace, command, { session }, (err, res) => { + if (err || !res) { + callback(err); + return; + } + + const indexesCreated: Array<{ name: string }> = res?.indexesCreated ?? []; + + callback( + undefined, + indexesCreated.map(({ name }) => name) + ); + }); + } +} diff --git a/src/operations/search_indexes/drop.ts b/src/operations/search_indexes/drop.ts new file mode 100644 index 0000000000..4e3ed88c11 --- /dev/null +++ b/src/operations/search_indexes/drop.ts @@ -0,0 +1,35 @@ +import type { Document } from 'bson'; + +import type { Collection } from '../../collection'; +import type { Server } from '../../sdam/server'; +import type { ClientSession } from '../../sessions'; +import type { Callback } from '../../utils'; +import { AbstractOperation } from '../operation'; + +/** @internal */ +export class DropSearchIndexOperation extends AbstractOperation { + constructor(private readonly collection: Collection, private readonly name: string) { + super(); + } + + execute(server: Server, session: ClientSession | undefined, callback: Callback): void { + const namespace = this.collection.fullNamespace; + + const command: Document = { + dropSearchIndex: namespace.collection + }; + + if (typeof this.name === 'string') { + command.name = this.name; + } + + server.command(namespace, command, { session }, err => { + if (err) { + callback(err); + return; + } + + callback(); + }); + } +} diff --git a/src/operations/search_indexes/update.ts b/src/operations/search_indexes/update.ts new file mode 100644 index 0000000000..0ed63450c3 --- /dev/null +++ b/src/operations/search_indexes/update.ts @@ -0,0 +1,36 @@ +import type { Document } from 'bson'; + +import type { Collection } from '../../collection'; +import type { Server } from '../../sdam/server'; +import type { ClientSession } from '../../sessions'; +import type { Callback } from '../../utils'; +import { AbstractOperation } from '../operation'; + +/** @internal */ +export class UpdateSearchIndexOperation extends AbstractOperation { + constructor( + private readonly collection: Collection, + private readonly name: string, + private readonly definition: Document + ) { + super(); + } + + execute(server: Server, session: ClientSession | undefined, callback: Callback): void { + const namespace = this.collection.fullNamespace; + const command = { + updateSearchIndex: namespace.collection, + name: this.name, + definition: this.definition + }; + + server.command(namespace, command, { session }, err => { + if (err) { + callback(err); + return; + } + + callback(); + }); + } +} diff --git a/src/utils.ts b/src/utils.ts index 23debd74e8..6c0c08737e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -288,10 +288,8 @@ export function getTopology(provider: TopologyProvider): Topology { // MongoClient or ClientSession or AbstractCursor if ('topology' in provider && provider.topology) { return provider.topology; - } else if ('s' in provider && 'client' in provider.s && provider.s.client.topology) { - return provider.s.client.topology; - } else if ('s' in provider && 'db' in provider.s && provider.s.db.s.client.topology) { - return provider.s.db.s.client.topology; + } else if ('client' in provider && provider.client.topology) { + return provider.client.topology; } throw new MongoNotConnectedError('MongoClient must be connected to perform this operation'); @@ -304,16 +302,13 @@ export function ns(ns: string): MongoDBNamespace { /** @public */ export class MongoDBNamespace { - db: string; - collection: string | undefined; /** * Create a namespace object * * @param db - database name * @param collection - collection name */ - constructor(db: string, collection?: string) { - this.db = db; + constructor(public db: string, public collection?: string) { this.collection = collection === '' ? undefined : collection; } @@ -321,8 +316,8 @@ export class MongoDBNamespace { return this.collection ? `${this.db}.${this.collection}` : this.db; } - withCollection(collection: string): MongoDBNamespace { - return new MongoDBNamespace(this.db, collection); + withCollection(collection: string): MongoDBCollectionNamespace { + return new MongoDBCollectionNamespace(this.db, collection); } static fromString(namespace?: string): MongoDBNamespace { @@ -337,6 +332,19 @@ export class MongoDBNamespace { } } +/** + * @public + * + * A class representing a collection's namespace. This class enforces (through Typescript) that + * the `collection` portion of the namespace is defined and should only be + * used in scenarios where this can be guaranteed. + */ +export class MongoDBCollectionNamespace extends MongoDBNamespace { + constructor(db: string, override collection: string) { + super(db, collection); + } +} + /** @internal */ export function* makeCounter(seed = 0): Generator { let count = seed; diff --git a/test/manual/search-index-management.spec.test.ts b/test/manual/search-index-management.spec.test.ts new file mode 100644 index 0000000000..ce7b7958fe --- /dev/null +++ b/test/manual/search-index-management.spec.test.ts @@ -0,0 +1,8 @@ +import { join } from 'path'; + +import { loadSpecTests } from '../spec'; +import { runUnifiedSuite } from '../tools/unified-spec-runner/runner'; + +describe('Search Index Management Tests (Unified)', function () { + runUnifiedSuite(loadSpecTests(join('index-management'))); +}); diff --git a/test/spec/index-management/README.rst b/test/spec/index-management/README.rst new file mode 100644 index 0000000000..e697dab348 --- /dev/null +++ b/test/spec/index-management/README.rst @@ -0,0 +1,48 @@ +====================== +Index Management Tests +====================== + +.. contents:: + +---- + +Test Plan +========= + +These prose tests are ported from the legacy enumerate-indexes spec. + +Configurations +-------------- + +- standalone node +- replica set primary node +- replica set secondary node +- mongos node + +Preparation +----------- + +For each of the configurations: + +- Create a (new) database +- Create a collection +- Create a single column index, a compound index, and a unique index +- Insert at least one document containing all the fields that the above + indicated indexes act on + +Tests +----- + +- Run the driver's method that returns a list of index names, and: + + - verify that *all* index names are represented in the result + - verify that there are no duplicate index names + - verify there are no returned indexes that do not exist + +- Run the driver's method that returns a list of index information records, and: + + - verify all the indexes are represented in the result + - verify the "unique" flags show up for the unique index + - verify there are no duplicates in the returned list + - if the result consists of statically defined index models that include an ``ns`` field, verify + that its value is accurate \ No newline at end of file diff --git a/test/spec/index-management/createSearchIndex.json b/test/spec/index-management/createSearchIndex.json new file mode 100644 index 0000000000..da664631e7 --- /dev/null +++ b/test/spec/index-management/createSearchIndex.json @@ -0,0 +1,134 @@ +{ + "description": "createSearchIndex", + "schemaVersion": "1.4", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "database0" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "collection0" + } + } + ], + "runOnRequirements": [ + { + "minServerVersion": "7.0.0", + "topologies": [ + "replicaset", + "load-balanced", + "sharded" + ], + "serverless": "forbid" + } + ], + "tests": [ + { + "description": "no name provided for an index definition", + "operations": [ + { + "name": "createSearchIndex", + "object": "collection0", + "arguments": { + "model": { + "definition": { + "mappings": { + "dynamic": true + } + } + } + }, + "expectError": { + "isError": true + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "createSearchIndexes": "collection0", + "indexes": [ + { + "definition": { + "mappings": { + "dynamic": true + } + } + } + ], + "$db": "database0" + } + } + } + ] + } + ] + }, + { + "description": "name provided for an index definition", + "operations": [ + { + "name": "createSearchIndex", + "object": "collection0", + "arguments": { + "model": { + "definition": { + "mappings": { + "dynamic": true + } + }, + "name": "test index" + } + }, + "expectError": { + "isError": true + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "createSearchIndexes": "collection0", + "indexes": [ + { + "definition": { + "mappings": { + "dynamic": true + } + }, + "name": "test index" + } + ], + "$db": "database0" + } + } + } + ] + } + ] + } + ] +} diff --git a/test/spec/index-management/createSearchIndex.yml b/test/spec/index-management/createSearchIndex.yml new file mode 100644 index 0000000000..0eb5c1ab1d --- /dev/null +++ b/test/spec/index-management/createSearchIndex.yml @@ -0,0 +1,60 @@ +description: "createSearchIndex" +schemaVersion: "1.4" +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeEvents: + - commandStartedEvent + - database: + id: &database0 database0 + client: *client0 + databaseName: *database0 + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: *collection0 + +runOnRequirements: + - minServerVersion: "7.0.0" + topologies: [ replicaset, load-balanced, sharded ] + serverless: forbid + +tests: + - description: "no name provided for an index definition" + operations: + - name: createSearchIndex + object: *collection0 + arguments: + model: { definition: &definition { mappings: { dynamic: true } } } + expectError: + # Search indexes are only available on 7.0+ atlas clusters. DRIVERS-2630 will add e2e testing + # against an Atlas cluster and the expectError will be removed. + isError: true + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + createSearchIndexes: *collection0 + indexes: [ { definition: *definition } ] + $db: *database0 + + - description: "name provided for an index definition" + operations: + - name: createSearchIndex + object: *collection0 + arguments: + model: { definition: &definition { mappings: { dynamic: true } } , name: 'test index' } + expectError: + # Search indexes are only available on 7.0+ atlas clusters. DRIVERS-2630 will add e2e testing + # against an Atlas cluster and the expectError will be removed. + isError: true + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + createSearchIndexes: *collection0 + indexes: [ { definition: *definition, name: 'test index' } ] + $db: *database0 \ No newline at end of file diff --git a/test/spec/index-management/createSearchIndexes.json b/test/spec/index-management/createSearchIndexes.json new file mode 100644 index 0000000000..b78b3ea6c8 --- /dev/null +++ b/test/spec/index-management/createSearchIndexes.json @@ -0,0 +1,169 @@ +{ + "description": "createSearchIndexes", + "schemaVersion": "1.4", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "database0" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "collection0" + } + } + ], + "runOnRequirements": [ + { + "minServerVersion": "7.0.0", + "topologies": [ + "replicaset", + "load-balanced", + "sharded" + ], + "serverless": "forbid" + } + ], + "tests": [ + { + "description": "empty index definition array", + "operations": [ + { + "name": "createSearchIndexes", + "object": "collection0", + "arguments": { + "models": [] + }, + "expectError": { + "isError": true + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "createSearchIndexes": "collection0", + "indexes": [], + "$db": "database0" + } + } + } + ] + } + ] + }, + { + "description": "no name provided for an index definition", + "operations": [ + { + "name": "createSearchIndexes", + "object": "collection0", + "arguments": { + "models": [ + { + "definition": { + "mappings": { + "dynamic": true + } + } + } + ] + }, + "expectError": { + "isError": true + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "createSearchIndexes": "collection0", + "indexes": [ + { + "definition": { + "mappings": { + "dynamic": true + } + } + } + ], + "$db": "database0" + } + } + } + ] + } + ] + }, + { + "description": "name provided for an index definition", + "operations": [ + { + "name": "createSearchIndexes", + "object": "collection0", + "arguments": { + "models": [ + { + "definition": { + "mappings": { + "dynamic": true + } + }, + "name": "test index" + } + ] + }, + "expectError": { + "isError": true + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "createSearchIndexes": "collection0", + "indexes": [ + { + "definition": { + "mappings": { + "dynamic": true + } + }, + "name": "test index" + } + ], + "$db": "database0" + } + } + } + ] + } + ] + } + ] +} diff --git a/test/spec/index-management/createSearchIndexes.yml b/test/spec/index-management/createSearchIndexes.yml new file mode 100644 index 0000000000..dc01c1b166 --- /dev/null +++ b/test/spec/index-management/createSearchIndexes.yml @@ -0,0 +1,80 @@ +description: "createSearchIndexes" +schemaVersion: "1.4" +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeEvents: + - commandStartedEvent + - database: + id: &database0 database0 + client: *client0 + databaseName: *database0 + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: *collection0 + +runOnRequirements: + - minServerVersion: "7.0.0" + topologies: [ replicaset, load-balanced, sharded ] + serverless: forbid + +tests: + - description: "empty index definition array" + operations: + - name: createSearchIndexes + object: *collection0 + arguments: + models: [] + expectError: + # Search indexes are only available on 7.0+ atlas clusters. DRIVERS-2630 will add e2e testing + # against an Atlas cluster and the expectError will be removed. + isError: true + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + createSearchIndexes: *collection0 + indexes: [] + $db: *database0 + + + - description: "no name provided for an index definition" + operations: + - name: createSearchIndexes + object: *collection0 + arguments: + models: [ { definition: &definition { mappings: { dynamic: true } } } ] + expectError: + # Search indexes are only available on 7.0+ atlas clusters. DRIVERS-2630 will add e2e testing + # against an Atlas cluster and the expectError will be removed. + isError: true + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + createSearchIndexes: *collection0 + indexes: [ { definition: *definition } ] + $db: *database0 + + - description: "name provided for an index definition" + operations: + - name: createSearchIndexes + object: *collection0 + arguments: + models: [ { definition: &definition { mappings: { dynamic: true } } , name: 'test index' } ] + expectError: + # Search indexes are only available on 7.0+ atlas clusters. DRIVERS-2630 will add e2e testing + # against an Atlas cluster and the expectError will be removed. + isError: true + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + createSearchIndexes: *collection0 + indexes: [ { definition: *definition, name: 'test index' } ] + $db: *database0 \ No newline at end of file diff --git a/test/spec/index-management/dropSearchIndex.json b/test/spec/index-management/dropSearchIndex.json new file mode 100644 index 0000000000..b73447f602 --- /dev/null +++ b/test/spec/index-management/dropSearchIndex.json @@ -0,0 +1,73 @@ +{ + "description": "dropSearchIndex", + "schemaVersion": "1.4", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "database0" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "collection0" + } + } + ], + "runOnRequirements": [ + { + "minServerVersion": "7.0.0", + "topologies": [ + "replicaset", + "load-balanced", + "sharded" + ], + "serverless": "forbid" + } + ], + "tests": [ + { + "description": "sends the correct command", + "operations": [ + { + "name": "dropSearchIndex", + "object": "collection0", + "arguments": { + "name": "test index" + }, + "expectError": { + "isError": true + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "dropSearchIndex": "collection0", + "name": "test index", + "$db": "database0" + } + } + } + ] + } + ] + } + ] +} diff --git a/test/spec/index-management/dropSearchIndex.yml b/test/spec/index-management/dropSearchIndex.yml new file mode 100644 index 0000000000..5ffef7d17e --- /dev/null +++ b/test/spec/index-management/dropSearchIndex.yml @@ -0,0 +1,42 @@ +description: "dropSearchIndex" +schemaVersion: "1.4" +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeEvents: + - commandStartedEvent + - database: + id: &database0 database0 + client: *client0 + databaseName: *database0 + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: *collection0 + +runOnRequirements: + - minServerVersion: "7.0.0" + topologies: [ replicaset, load-balanced, sharded ] + serverless: forbid + +tests: + - description: "sends the correct command" + operations: + - name: dropSearchIndex + object: *collection0 + arguments: + name: &indexName 'test index' + expectError: + # Search indexes are only available on 7.0+ atlas clusters. DRIVERS-2630 will add e2e testing + # against an Atlas cluster and the expectError will be removed. + isError: true + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + dropSearchIndex: *collection0 + name: *indexName + $db: *database0 + diff --git a/test/spec/index-management/dropSearchIndexes.json b/test/spec/index-management/dropSearchIndexes.json new file mode 100644 index 0000000000..b73447f602 --- /dev/null +++ b/test/spec/index-management/dropSearchIndexes.json @@ -0,0 +1,73 @@ +{ + "description": "dropSearchIndex", + "schemaVersion": "1.4", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "database0" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "collection0" + } + } + ], + "runOnRequirements": [ + { + "minServerVersion": "7.0.0", + "topologies": [ + "replicaset", + "load-balanced", + "sharded" + ], + "serverless": "forbid" + } + ], + "tests": [ + { + "description": "sends the correct command", + "operations": [ + { + "name": "dropSearchIndex", + "object": "collection0", + "arguments": { + "name": "test index" + }, + "expectError": { + "isError": true + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "dropSearchIndex": "collection0", + "name": "test index", + "$db": "database0" + } + } + } + ] + } + ] + } + ] +} diff --git a/test/spec/index-management/dropSearchIndexes.yml b/test/spec/index-management/dropSearchIndexes.yml new file mode 100644 index 0000000000..5ffef7d17e --- /dev/null +++ b/test/spec/index-management/dropSearchIndexes.yml @@ -0,0 +1,42 @@ +description: "dropSearchIndex" +schemaVersion: "1.4" +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeEvents: + - commandStartedEvent + - database: + id: &database0 database0 + client: *client0 + databaseName: *database0 + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: *collection0 + +runOnRequirements: + - minServerVersion: "7.0.0" + topologies: [ replicaset, load-balanced, sharded ] + serverless: forbid + +tests: + - description: "sends the correct command" + operations: + - name: dropSearchIndex + object: *collection0 + arguments: + name: &indexName 'test index' + expectError: + # Search indexes are only available on 7.0+ atlas clusters. DRIVERS-2630 will add e2e testing + # against an Atlas cluster and the expectError will be removed. + isError: true + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + dropSearchIndex: *collection0 + name: *indexName + $db: *database0 + diff --git a/test/spec/index-management/listSearchIndexes.json b/test/spec/index-management/listSearchIndexes.json new file mode 100644 index 0000000000..41e2655fb3 --- /dev/null +++ b/test/spec/index-management/listSearchIndexes.json @@ -0,0 +1,153 @@ +{ + "description": "listSearchIndexes", + "schemaVersion": "1.4", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "database0" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "collection0" + } + } + ], + "runOnRequirements": [ + { + "minServerVersion": "7.0.0", + "topologies": [ + "replicaset", + "load-balanced", + "sharded" + ], + "serverless": "forbid" + } + ], + "tests": [ + { + "description": "when no name is provided, it does not populate the filter", + "operations": [ + { + "name": "listSearchIndexes", + "object": "collection0", + "expectError": { + "isError": true + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "aggregate": "collection0", + "pipeline": [ + { + "$listSearchIndexes": {} + } + ] + } + } + } + ] + } + ] + }, + { + "description": "when a name is provided, it is present in the filter", + "operations": [ + { + "name": "listSearchIndexes", + "object": "collection0", + "arguments": { + "name": "test index" + }, + "expectError": { + "isError": true + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "aggregate": "collection0", + "pipeline": [ + { + "$listSearchIndexes": { + "name": "test index" + } + } + ], + "$db": "database0" + } + } + } + ] + } + ] + }, + { + "description": "aggregation cursor options are supported", + "operations": [ + { + "name": "listSearchIndexes", + "object": "collection0", + "arguments": { + "name": "test index", + "aggregationOptions": { + "batchSize": 10 + } + }, + "expectError": { + "isError": true + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "aggregate": "collection0", + "cursor": { + "batchSize": 10 + }, + "pipeline": [ + { + "$listSearchIndexes": { + "name": "test index" + } + } + ], + "$db": "database0" + } + } + } + ] + } + ] + } + ] +} diff --git a/test/spec/index-management/listSearchIndexes.yml b/test/spec/index-management/listSearchIndexes.yml new file mode 100644 index 0000000000..7d3c3187b9 --- /dev/null +++ b/test/spec/index-management/listSearchIndexes.yml @@ -0,0 +1,82 @@ +description: "listSearchIndexes" +schemaVersion: "1.4" +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeEvents: + - commandStartedEvent + - database: + id: &database0 database0 + client: *client0 + databaseName: *database0 + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: *collection0 + +runOnRequirements: + - minServerVersion: "7.0.0" + topologies: [ replicaset, load-balanced, sharded ] + serverless: forbid + +tests: + - description: "when no name is provided, it does not populate the filter" + operations: + - name: listSearchIndexes + object: *collection0 + expectError: + # Search indexes are only available on 7.0+ atlas clusters. DRIVERS-2630 will add e2e testing + # against an Atlas cluster and the expectError will be removed. + isError: true + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + aggregate: *collection0 + pipeline: + - $listSearchIndexes: {} + + - description: "when a name is provided, it is present in the filter" + operations: + - name: listSearchIndexes + object: *collection0 + arguments: + name: &indexName "test index" + expectError: + # Search indexes are only available on 7.0+ atlas clusters. DRIVERS-2630 will add e2e testing + # against an Atlas cluster and the expectError will be removed. + isError: true + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + aggregate: *collection0 + pipeline: + - $listSearchIndexes: { name: *indexName } + $db: *database0 + + - description: aggregation cursor options are supported + operations: + - name: listSearchIndexes + object: *collection0 + arguments: + name: &indexName "test index" + aggregationOptions: + batchSize: 10 + expectError: + # Search indexes are only available on 7.0+ atlas clusters. DRIVERS-2630 will add e2e testing + # against an Atlas cluster and the expectError will be removed. + isError: true + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + aggregate: *collection0 + cursor: { batchSize: 10 } + pipeline: + - $listSearchIndexes: { name: *indexName } + $db: *database0 \ No newline at end of file diff --git a/test/spec/index-management/updateSearchIndex.json b/test/spec/index-management/updateSearchIndex.json new file mode 100644 index 0000000000..00cd7e7541 --- /dev/null +++ b/test/spec/index-management/updateSearchIndex.json @@ -0,0 +1,75 @@ +{ + "description": "updateSearchIndex", + "schemaVersion": "1.4", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "database0" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "collection0" + } + } + ], + "runOnRequirements": [ + { + "minServerVersion": "7.0.0", + "topologies": [ + "replicaset", + "load-balanced", + "sharded" + ], + "serverless": "forbid" + } + ], + "tests": [ + { + "description": "sends the correct command", + "operations": [ + { + "name": "updateSearchIndex", + "object": "collection0", + "arguments": { + "name": "test index", + "definition": {} + }, + "expectError": { + "isError": true + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "updateSearchIndex": "collection0", + "name": "test index", + "definition": {}, + "$db": "database0" + } + } + } + ] + } + ] + } + ] +} diff --git a/test/spec/index-management/updateSearchIndex.yml b/test/spec/index-management/updateSearchIndex.yml new file mode 100644 index 0000000000..215dbc42f9 --- /dev/null +++ b/test/spec/index-management/updateSearchIndex.yml @@ -0,0 +1,44 @@ +description: "updateSearchIndex" +schemaVersion: "1.4" +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeEvents: + - commandStartedEvent + - database: + id: &database0 database0 + client: *client0 + databaseName: *database0 + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: *collection0 + +runOnRequirements: + - minServerVersion: "7.0.0" + topologies: [ replicaset, load-balanced, sharded ] + serverless: forbid + +tests: + - description: "sends the correct command" + operations: + - name: updateSearchIndex + object: *collection0 + arguments: + name: &indexName 'test index' + definition: &definition {} + expectError: + # Search indexes are only available on 7.0+ atlas clusters. DRIVERS-2630 will add e2e testing + # against an Atlas cluster and the expectError will be removed. + isError: true + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + updateSearchIndex: *collection0 + name: *indexName + definition: *definition + $db: *database0 + diff --git a/test/tools/unified-spec-runner/operations.ts b/test/tools/unified-spec-runner/operations.ts index 3118a51889..d0da156efb 100644 --- a/test/tools/unified-spec-runner/operations.ts +++ b/test/tools/unified-spec-runner/operations.ts @@ -35,7 +35,7 @@ interface OperationFunctionParams { type RunOperationFn = ( p: OperationFunctionParams -) => Promise; +) => Promise; export const operations = new Map(); operations.set('createEntities', async ({ entities, operation, testConfig }) => { @@ -748,11 +748,41 @@ operations.set('removeKeyAltName', async ({ entities, operation }) => { operations.set('getKeyByAltName', async ({ entities, operation }) => { const clientEncryption = entities.getEntity('clientEncryption', operation.object); - const { keyAltName } = operation.arguments!; + const { keyAltName } = operation.arguments ?? {}; return clientEncryption.getKeyByAltName(keyAltName); }); +operations.set('listSearchIndexes', async ({ entities, operation }) => { + const collection: Collection = entities.getEntity('collection', operation.object); + const { name, aggregationOptions } = operation.arguments ?? {}; + return collection.listSearchIndexes(name, aggregationOptions).toArray(); +}); + +operations.set('dropSearchIndex', async ({ entities, operation }) => { + const collection: Collection = entities.getEntity('collection', operation.object); + const { name } = operation.arguments ?? {}; + return collection.dropSearchIndex(name); +}); + +operations.set('updateSearchIndex', async ({ entities, operation }) => { + const collection: Collection = entities.getEntity('collection', operation.object); + const { name, definition } = operation.arguments ?? {}; + return collection.updateSearchIndex(name, definition); +}); + +operations.set('createSearchIndex', async ({ entities, operation }) => { + const collection: Collection = entities.getEntity('collection', operation.object); + const { model } = operation.arguments ?? {}; + return collection.createSearchIndex(model); +}); + +operations.set('createSearchIndexes', async ({ entities, operation }) => { + const collection: Collection = entities.getEntity('collection', operation.object); + const { models } = operation.arguments ?? {}; + return collection.createSearchIndexes(models); +}); + export async function executeOperationAndCheck( operation: OperationDescription, entities: EntitiesMap, diff --git a/test/unit/utils.test.ts b/test/unit/utils.test.ts index 519aef7845..e3d82c50bd 100644 --- a/test/unit/utils.test.ts +++ b/test/unit/utils.test.ts @@ -12,6 +12,7 @@ import { List, matchesParentDomain, maybeCallback, + MongoDBCollectionNamespace, MongoDBNamespace, MongoRuntimeError, ObjectId, @@ -772,6 +773,21 @@ describe('driver utils', function () { expect(withCollectionNamespace).to.have.property('db', 'test'); expect(withCollectionNamespace).to.have.property('collection', 'pets'); }); + + it('returns a MongoDBCollectionNamespaceObject', () => { + expect(dbNamespace.withCollection('pets')).to.be.instanceOf(MongoDBCollectionNamespace); + }); + }); + }); + + describe('MongoDBCollectionNamespace', () => { + it('is a subclass of MongoDBNamespace', () => { + expect(new MongoDBCollectionNamespace('db', 'collection')).to.be.instanceOf(MongoDBNamespace); + }); + + it('does not enforce the collection property at runtime', () => { + // @ts-expect-error Intentionally calling constructor incorrectly. + expect(new MongoDBCollectionNamespace('db')).to.have.property('collection', undefined); }); });