From b7d0093368b3b002957bbf52955589d43de81087 Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Thu, 7 Sep 2023 09:58:05 +0100 Subject: [PATCH] feat: search index data-service methods COMPASS-7185 (#4802) * WIP: search index data-service methods * mocked unit tests for search indexes for now * add @op * async * add chai-as-promised --- package-lock.json | 2 + packages/data-service/package.json | 1 + .../data-service/src/data-service.spec.ts | 168 +++++++++++++++++- packages/data-service/src/data-service.ts | 66 +++++++ packages/data-service/src/index.ts | 1 + .../src/search-index-detail-helper.ts | 9 + packages/data-service/test/helpers.ts | 32 ++++ 7 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 packages/data-service/src/search-index-detail-helper.ts diff --git a/package-lock.json b/package-lock.json index 8b8d9a3d477..acb4f4b50d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48491,6 +48491,7 @@ "@types/whatwg-url": "^8.2.1", "bson": "^5.2.0", "chai": "^4.2.0", + "chai-as-promised": "^7.1.1", "depcheck": "^1.4.1", "eslint": "^7.25.0", "kerberos": "^2.0.0", @@ -86085,6 +86086,7 @@ "@types/whatwg-url": "^8.2.1", "bson": "^5.2.0", "chai": "^4.2.0", + "chai-as-promised": "^7.1.1", "depcheck": "^1.4.1", "eslint": "^7.25.0", "kerberos": "^2.0.0", diff --git a/packages/data-service/package.json b/packages/data-service/package.json index 16f27f7d6e3..8bb38298cea 100644 --- a/packages/data-service/package.json +++ b/packages/data-service/package.json @@ -74,6 +74,7 @@ "@types/whatwg-url": "^8.2.1", "bson": "^5.2.0", "chai": "^4.2.0", + "chai-as-promised": "^7.1.1", "depcheck": "^1.4.1", "eslint": "^7.25.0", "kerberos": "^2.0.0", diff --git a/packages/data-service/src/data-service.spec.ts b/packages/data-service/src/data-service.spec.ts index de90755949e..71ce6eb55e7 100644 --- a/packages/data-service/src/data-service.spec.ts +++ b/packages/data-service/src/data-service.spec.ts @@ -1,7 +1,9 @@ import assert from 'assert'; import { ObjectId } from 'bson'; -import { expect } from 'chai'; +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; import type { Sort } from 'mongodb'; +import { MongoServerError } from 'mongodb'; import { MongoClient } from 'mongodb'; import sinon from 'sinon'; import { v4 as uuid } from 'uuid'; @@ -18,6 +20,10 @@ import { AbortController } from '../test/mocks'; import { createClonedClient } from './connect-mongo-client'; import { runCommand } from './run-command'; import { mochaTestServer } from '@mongodb-js/compass-test-server'; +import type { SearchIndex } from './search-index-detail-helper'; + +const { expect } = chai; +chai.use(chaiAsPromised); const TEST_DOCS = [ { @@ -1123,6 +1129,58 @@ describe('DataService', function () { expect(stop.callCount).to.equal(1); }); }); + + describe('#isListSearchIndexesSupported', function () { + it('returns false', async function () { + expect( + await dataService.isListSearchIndexesSupported(testNamespace) + ).to.be.false; + }); + }); + + describe('#getSearchIndexes', function () { + it('throws an error', async function () { + await expect( + dataService.getSearchIndexes(testNamespace) + ).to.be.rejectedWith( + MongoServerError, + "Unrecognized pipeline stage name: '$listSearchIndexes'" + ); + }); + }); + + describe('#createSearchIndex', function () { + it('throws an error', async function () { + await expect( + dataService.createSearchIndex(testNamespace, 'my-index', {}) + ).to.be.rejectedWith( + MongoServerError, + "no such command: 'createSearchIndexes'" + ); + }); + }); + + describe('#updateSearchIndex', function () { + it('throws an error', async function () { + await expect( + dataService.updateSearchIndex(testNamespace, 'my-index', {}) + ).to.be.rejectedWith( + MongoServerError, + "no such command: 'updateSearchIndex'" + ); + }); + }); + + describe('#dropSearchIndex', function () { + it('throws an error', async function () { + await expect( + dataService.dropSearchIndex(testNamespace, 'my-index') + ).to.be.rejectedWith( + MongoServerError, + "no such command: 'dropSearchIndex'" + ); + }); + }); }); context('with mocked client', function () { @@ -1483,5 +1541,113 @@ describe('DataService', function () { expect(dataService['_crudClient']).to.equal(fakeClonedClient); }); }); + + describe('#isListSearchIndexesSupported', function () { + it('resolves to true if listSearchIndexes succeeds', async function () { + const searchIndexes: SearchIndex[] = [ + { + id: '1', + name: 'a', + status: 'READY', + queryable: true, + latestDefinition: {}, + }, + { + id: '2', + name: 'b', + status: 'READY', + queryable: true, + latestDefinition: {}, + }, + ]; + + const dataService: any = createDataServiceWithMockedClient({ + searchIndexes: { + test: { + test: searchIndexes, + }, + }, + }); + expect( + await dataService.isListSearchIndexesSupported('test.test') + ).to.be.true; + }); + + it('resolves to false if listSearchIndexes fails', async function () { + const dataService: any = createDataServiceWithMockedClient({ + searchIndexes: { + test: { + test: new Error('fake error'), + }, + }, + }); + expect( + await dataService.isListSearchIndexesSupported('test.test') + ).to.be.false; + }); + }); + + describe('#getSearchIndexes', function () { + it('returns the search indexes', async function () { + const searchIndexes: SearchIndex[] = [ + { + id: '1', + name: 'a', + status: 'READY', + queryable: true, + latestDefinition: {}, + }, + { + id: '2', + name: 'b', + status: 'READY', + queryable: true, + latestDefinition: {}, + }, + ]; + + const dataService: any = createDataServiceWithMockedClient({ + searchIndexes: { + test: { + test: searchIndexes, + }, + }, + }); + expect(await dataService.getSearchIndexes('test.test')).to.deep.equal( + searchIndexes + ); + }); + }); + + describe('#createSearchIndex', function () { + it('creates a search index', async function () { + const dataService: any = createDataServiceWithMockedClient({}); + expect( + await dataService.createSearchIndex('test.test', 'my-index', { + mappings: { dynamic: true }, + }) + ).to.deep.equal('my-index'); + }); + }); + + describe('#updateSearchIndex', function () { + it('updates a search index', async function () { + const dataService: any = createDataServiceWithMockedClient({}); + expect( + await dataService.updateSearchIndex('test.test', 'my-index', { + mappings: { dynamic: true }, + }) + ).to.be.undefined; + }); + }); + + describe('#dropSearchIndex', function () { + it('drops a search index', async function () { + const dataService: any = createDataServiceWithMockedClient({}); + expect( + await dataService.dropSearchIndex('test.test', 'my-index') + ).to.be.undefined; + }); + }); }); }); diff --git a/packages/data-service/src/data-service.ts b/packages/data-service/src/data-service.ts index cf5fb56906e..279b68a57b1 100644 --- a/packages/data-service/src/data-service.ts +++ b/packages/data-service/src/data-service.ts @@ -91,6 +91,7 @@ import type { IndexInfo, } from './index-detail-helper'; import { createIndexDefinition } from './index-detail-helper'; +import type { SearchIndex } from './search-index-detail-helper'; import type { BoundLogger, DataServiceImplLogger, @@ -424,6 +425,26 @@ export interface DataService { */ dropIndex(ns: string, name: string): Promise; + /*** SearchIndexes ***/ + + isListSearchIndexesSupported(ns: string): Promise; + + getSearchIndexes(ns: string): Promise; + + createSearchIndex( + ns: string, + name: string, + definition: Document + ): Promise; + + updateSearchIndex( + ns: string, + name: string, + definition: Document + ): Promise; + + dropSearchIndex(ns: string, name: string): Promise; + /*** Aggregation ***/ /** @@ -1513,6 +1534,51 @@ class DataServiceImpl extends WithLogContext implements DataService { return await coll.dropIndex(name); } + @op(mongoLogId(1_001_000_237)) + async isListSearchIndexesSupported(ns: string): Promise { + try { + await this.getSearchIndexes(ns); + } catch (err) { + return false; + } + return true; + } + + @op(mongoLogId(1_001_000_238)) + async getSearchIndexes(ns: string): Promise { + const coll = this._collection(ns, 'CRUD'); + const cursor = coll.listSearchIndexes(); + const indexes = await cursor.toArray(); + void cursor.close(); + return indexes as SearchIndex[]; + } + + @op(mongoLogId(1_001_000_239)) + async createSearchIndex( + ns: string, + name: string, + definition: Document + ): Promise { + const coll = this._collection(ns, 'CRUD'); + return coll.createSearchIndex({ name, definition }); + } + + @op(mongoLogId(1_001_000_240)) + async updateSearchIndex( + ns: string, + name: string, + definition: Document + ): Promise { + const coll = this._collection(ns, 'CRUD'); + return coll.updateSearchIndex(name, definition); + } + + @op(mongoLogId(1_001_000_241)) + async dropSearchIndex(ns: string, name: string): Promise { + const coll = this._collection(ns, 'CRUD'); + return coll.dropSearchIndex(name); + } + @op(mongoLogId(1_001_000_041), ([ns, pipeline]) => { return { ns, stages: pipeline.map((stage) => Object.keys(stage)[0]) }; }) diff --git a/packages/data-service/src/index.ts b/packages/data-service/src/index.ts index d377c9b1b95..eca0eb506b5 100644 --- a/packages/data-service/src/index.ts +++ b/packages/data-service/src/index.ts @@ -14,3 +14,4 @@ export { export type { ReauthenticationHandler } from './connect-mongo-client'; export type { ExplainExecuteOptions } from './data-service'; export type { IndexDefinition } from './index-detail-helper'; +export type { SearchIndex } from './search-index-detail-helper'; diff --git a/packages/data-service/src/search-index-detail-helper.ts b/packages/data-service/src/search-index-detail-helper.ts new file mode 100644 index 00000000000..13885868d86 --- /dev/null +++ b/packages/data-service/src/search-index-detail-helper.ts @@ -0,0 +1,9 @@ +import type { Document } from 'mongodb'; + +export type SearchIndex = { + id: string; + name: string; + status: 'BUILDING' | 'FAILED' | 'PENDING' | 'READY' | 'STALE'; + queryable: boolean; + latestDefinition: Document; +}; diff --git a/packages/data-service/test/helpers.ts b/packages/data-service/test/helpers.ts index 23b63b76604..78e2588cf92 100644 --- a/packages/data-service/test/helpers.ts +++ b/packages/data-service/test/helpers.ts @@ -1,6 +1,8 @@ import type { MongoClient } from 'mongodb'; import { ConnectionString } from 'mongodb-connection-string-url'; +import type { SearchIndex } from '../src/search-index-detail-helper'; + export type ClientMockOptions = { hosts: [{ host: string; port: number }]; commands: Partial<{ @@ -14,6 +16,7 @@ export type ClientMockOptions = { collMod: unknown; }>; collections: Record; + searchIndexes: Record>; clientOptions: Record; }; @@ -21,6 +24,7 @@ export function createMongoClientMock({ hosts = [{ host: 'localhost', port: 9999 }], commands = {}, collections = {}, + searchIndexes = {}, clientOptions = {}, }: Partial = {}): { client: MongoClient; @@ -85,6 +89,34 @@ export function createMongoClientMock({ }, }; }, + collection(collectionName: string) { + return { + listSearchIndexes() { + return { + toArray() { + const indexes = + searchIndexes[databaseName][collectionName] ?? []; + if (indexes instanceof Error) { + return Promise.reject(indexes); + } + return Promise.resolve(indexes); + }, + close() { + /* ignore */ + }, + }; + }, + createSearchIndex({ name }: { name: string }) { + return Promise.resolve(name); + }, + updateSearchIndex() { + return Promise.resolve(); + }, + dropSearchIndex() { + return Promise.resolve(); + }, + }; + }, }; }, options: {