From 598e3840cafc73cac8753000774c3f3bfc6f91da Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Sat, 9 Mar 2024 09:42:43 +0800 Subject: [PATCH] [Workspace] Consume workspace id in saved object client (#6014) * feat: consume current workspace in saved objects management and saved objects client Signed-off-by: SuZhou-Joe * feat: add unit test Signed-off-by: SuZhou-Joe * feat: add unit test for each change Signed-off-by: SuZhou-Joe * fix: update snapshot of unit test Signed-off-by: SuZhou-Joe * fix: unit test Signed-off-by: SuZhou-Joe * fix: unit test Signed-off-by: SuZhou-Joe * fix: unit test Signed-off-by: SuZhou-Joe * feat: update snapshot Signed-off-by: SuZhou-Joe * revert: saved object management changes Signed-off-by: SuZhou-Joe * feat: add CHANGELOG Signed-off-by: SuZhou-Joe * feat: address some comment Signed-off-by: SuZhou-Joe * feat: address comment Signed-off-by: SuZhou-Joe * feat: remove useless return Signed-off-by: SuZhou-Joe * fix: bootstrap error Signed-off-by: SuZhou-Joe * feat: update CHANGELOG Signed-off-by: SuZhou-Joe * feat: update CHANGELOG Signed-off-by: SuZhou-Joe * feat: optimize code Signed-off-by: SuZhou-Joe * feat: update comment Signed-off-by: SuZhou-Joe --------- Signed-off-by: SuZhou-Joe --- .../saved_objects_client.test.ts | 332 ++++++++++++++++++ .../saved_objects/saved_objects_client.ts | 81 ++--- .../get_sorted_objects_for_export.test.ts | 67 ++++ .../import/resolve_import_errors.test.ts | 4 +- .../import/resolve_import_errors.ts | 1 + src/plugins/workspace/public/plugin.test.ts | 8 + 6 files changed, 450 insertions(+), 43 deletions(-) diff --git a/src/core/public/saved_objects/saved_objects_client.test.ts b/src/core/public/saved_objects/saved_objects_client.test.ts index c3cde2f6d6c3..9bcfb2dbfb6d 100644 --- a/src/core/public/saved_objects/saved_objects_client.test.ts +++ b/src/core/public/saved_objects/saved_objects_client.test.ts @@ -329,6 +329,26 @@ describe('SavedObjectsClient', () => { `); }); + test('makes HTTP call with workspaces', () => { + savedObjectsClient.create('index-pattern', attributes, { + workspaces: ['foo'], + }); + expect(http.fetch.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/saved_objects/index-pattern", + Object { + "body": "{\\"attributes\\":{\\"foo\\":\\"Foo\\",\\"bar\\":\\"Bar\\"},\\"workspaces\\":[\\"foo\\"]}", + "method": "POST", + "query": Object { + "overwrite": undefined, + }, + }, + ], + ] + `); + }); + test('rejects when HTTP call fails', async () => { http.fetch.mockRejectedValueOnce(new Error('Request failed')); await expect( @@ -386,6 +406,29 @@ describe('SavedObjectsClient', () => { ] `); }); + + test('makes HTTP call with workspaces', () => { + savedObjectsClient.bulkCreate([doc], { + workspaces: ['foo'], + }); + expect(http.fetch.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/saved_objects/_bulk_create", + Object { + "body": "[{\\"id\\":\\"AVwSwFxtcMV38qjDZoQg\\",\\"type\\":\\"config\\",\\"attributes\\":{\\"title\\":\\"Example title\\"},\\"version\\":\\"foo\\",\\"updated_at\\":\\"${updatedAt}\\"}]", + "method": "POST", + "query": Object { + "overwrite": undefined, + "workspaces": Array [ + "foo", + ], + }, + }, + ], + ] + `); + }); }); describe('#bulk_update', () => { @@ -510,5 +553,294 @@ describe('SavedObjectsClient', () => { ] `); }); + + test('makes HTTP call correctly with workspaces', () => { + const options = { + invalid: true, + workspaces: ['foo'], + }; + + // @ts-expect-error + savedObjectsClient.find(options); + expect(http.fetch.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/saved_objects/_find", + Object { + "body": undefined, + "method": "GET", + "query": Object { + "workspaces": Array [ + "foo", + ], + }, + }, + ], + ] + `); + }); + }); +}); + +describe('SavedObjectsClientWithWorkspaceSet', () => { + const updatedAt = new Date().toISOString(); + const doc = { + id: 'AVwSwFxtcMV38qjDZoQg', + type: 'config', + attributes: { title: 'Example title' }, + version: 'foo', + updated_at: updatedAt, + }; + + const http = httpServiceMock.createStartContract(); + let savedObjectsClient: SavedObjectsClient; + + beforeEach(() => { + savedObjectsClient = new SavedObjectsClient(http); + savedObjectsClient.setCurrentWorkspace('foo'); + http.fetch.mockClear(); + }); + + describe('#create', () => { + const attributes = { foo: 'Foo', bar: 'Bar' }; + + beforeEach(() => { + http.fetch.mockResolvedValue({ id: 'serverId', type: 'server-type', attributes }); + }); + + test('makes HTTP call with ID', () => { + savedObjectsClient.create('index-pattern', attributes, { id: 'myId' }); + expect(http.fetch.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/saved_objects/index-pattern/myId", + Object { + "body": "{\\"attributes\\":{\\"foo\\":\\"Foo\\",\\"bar\\":\\"Bar\\"},\\"workspaces\\":[\\"foo\\"]}", + "method": "POST", + "query": Object { + "overwrite": undefined, + }, + }, + ], + ] + `); + }); + + test('makes HTTP call without ID', () => { + savedObjectsClient.create('index-pattern', attributes); + expect(http.fetch.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/saved_objects/index-pattern", + Object { + "body": "{\\"attributes\\":{\\"foo\\":\\"Foo\\",\\"bar\\":\\"Bar\\"},\\"workspaces\\":[\\"foo\\"]}", + "method": "POST", + "query": Object { + "overwrite": undefined, + }, + }, + ], + ] + `); + }); + + test('makes HTTP call with workspaces', () => { + savedObjectsClient.create('index-pattern', attributes, { + workspaces: ['foo'], + }); + expect(http.fetch.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/saved_objects/index-pattern", + Object { + "body": "{\\"attributes\\":{\\"foo\\":\\"Foo\\",\\"bar\\":\\"Bar\\"},\\"workspaces\\":[\\"foo\\"]}", + "method": "POST", + "query": Object { + "overwrite": undefined, + }, + }, + ], + ] + `); + }); + }); + + describe('#bulk_create', () => { + beforeEach(() => { + http.fetch.mockResolvedValue({ saved_objects: [doc] }); + }); + + test('makes HTTP call', async () => { + await savedObjectsClient.bulkCreate([doc]); + expect(http.fetch.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/saved_objects/_bulk_create", + Object { + "body": "[{\\"id\\":\\"AVwSwFxtcMV38qjDZoQg\\",\\"type\\":\\"config\\",\\"attributes\\":{\\"title\\":\\"Example title\\"},\\"version\\":\\"foo\\",\\"updated_at\\":\\"${updatedAt}\\"}]", + "method": "POST", + "query": Object { + "overwrite": false, + "workspaces": Array [ + "foo", + ], + }, + }, + ], + ] + `); + }); + + test('makes HTTP call with overwrite query paramater', async () => { + await savedObjectsClient.bulkCreate([doc], { overwrite: true }); + expect(http.fetch.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/saved_objects/_bulk_create", + Object { + "body": "[{\\"id\\":\\"AVwSwFxtcMV38qjDZoQg\\",\\"type\\":\\"config\\",\\"attributes\\":{\\"title\\":\\"Example title\\"},\\"version\\":\\"foo\\",\\"updated_at\\":\\"${updatedAt}\\"}]", + "method": "POST", + "query": Object { + "overwrite": true, + "workspaces": Array [ + "foo", + ], + }, + }, + ], + ] + `); + }); + + test('makes HTTP call with workspaces', () => { + savedObjectsClient.bulkCreate([doc], { + workspaces: ['bar'], + }); + expect(http.fetch.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/saved_objects/_bulk_create", + Object { + "body": "[{\\"id\\":\\"AVwSwFxtcMV38qjDZoQg\\",\\"type\\":\\"config\\",\\"attributes\\":{\\"title\\":\\"Example title\\"},\\"version\\":\\"foo\\",\\"updated_at\\":\\"${updatedAt}\\"}]", + "method": "POST", + "query": Object { + "overwrite": undefined, + "workspaces": Array [ + "bar", + ], + }, + }, + ], + ] + `); + }); + }); + + describe('#bulk_update', () => { + const bulkUpdateDoc = { + id: 'AVwSwFxtcMV38qjDZoQg', + type: 'config', + attributes: { title: 'Example title' }, + version: 'foo', + }; + beforeEach(() => { + http.fetch.mockResolvedValue({ saved_objects: [bulkUpdateDoc] }); + }); + + test('makes HTTP call', async () => { + await savedObjectsClient.bulkUpdate([bulkUpdateDoc]); + expect(http.fetch.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/saved_objects/_bulk_update", + Object { + "body": "[{\\"id\\":\\"AVwSwFxtcMV38qjDZoQg\\",\\"type\\":\\"config\\",\\"attributes\\":{\\"title\\":\\"Example title\\"},\\"version\\":\\"foo\\"}]", + "method": "PUT", + "query": undefined, + }, + ], + ] + `); + }); + }); + + describe('#find', () => { + const object = { id: 'logstash-*', type: 'index-pattern', title: 'Test' }; + + beforeEach(() => { + http.fetch.mockResolvedValue({ saved_objects: [object], page: 0, per_page: 1, total: 1 }); + }); + + test('makes HTTP call correctly mapping options into snake case query parameters', () => { + const options = { + defaultSearchOperator: 'OR' as const, + fields: ['title'], + hasReference: { id: '1', type: 'reference' }, + page: 10, + perPage: 100, + search: 'what is the meaning of life?|life', + searchFields: ['title^5', 'body'], + sortField: 'sort_field', + type: 'index-pattern', + }; + + savedObjectsClient.find(options); + expect(http.fetch.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/saved_objects/_find", + Object { + "body": undefined, + "method": "GET", + "query": Object { + "default_search_operator": "OR", + "fields": Array [ + "title", + ], + "has_reference": "{\\"id\\":\\"1\\",\\"type\\":\\"reference\\"}", + "page": 10, + "per_page": 100, + "search": "what is the meaning of life?|life", + "search_fields": Array [ + "title^5", + "body", + ], + "sort_field": "sort_field", + "type": "index-pattern", + "workspaces": Array [ + "foo", + ], + }, + }, + ], + ] + `); + }); + + test('makes HTTP call correctly with workspaces', () => { + const options = { + invalid: true, + workspaces: ['bar'], + }; + + // @ts-expect-error + savedObjectsClient.find(options); + expect(http.fetch.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/saved_objects/_find", + Object { + "body": undefined, + "method": "GET", + "query": Object { + "workspaces": Array [ + "bar", + ], + }, + }, + ], + ] + `); + }); }); }); diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index 7681117c7977..0e9c5cab0ad9 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -45,7 +45,7 @@ import { HttpFetchOptions, HttpSetup } from '../http'; type SavedObjectsFindOptions = Omit< SavedObjectFindOptionsServer, - 'sortOrder' | 'rootSearchFields' | 'typeToNamespacesMap' + 'sortOrder' | 'rootSearchFields' | 'typeToNamespacesMap' | 'ACLSearchParams' >; type PromiseType> = T extends Promise ? U : never; @@ -79,6 +79,7 @@ export interface SavedObjectsBulkCreateObject extends SavedObjectsC export interface SavedObjectsBulkCreateOptions { /** If a document with the given `id` already exists, overwrite it's contents (default=false). */ overwrite?: boolean; + workspaces?: string[]; } /** @public */ @@ -185,11 +186,35 @@ export class SavedObjectsClient { private http: HttpSetup; private batchQueue: BatchQueueEntry[]; /** - * if currentWorkspaceId is undefined, it means - * we should not carry out workspace info when doing any operation. + * The currentWorkspaceId may be undefined when workspace plugin is not enabled. */ private currentWorkspaceId: string | undefined; + /** + * Check if workspaces field present in given options, if so, overwrite the current workspace id. + * @param options + * @returns + */ + private formatWorkspacesParams(options: { + workspaces?: SavedObjectsCreateOptions['workspaces']; + }): { workspaces: string[] } | {} { + const currentWorkspaceId = this.currentWorkspaceId; + let finalWorkspaces; + if (options.hasOwnProperty('workspaces')) { + finalWorkspaces = options.workspaces; + } else if (typeof currentWorkspaceId === 'string') { + finalWorkspaces = [currentWorkspaceId]; + } + + if (finalWorkspaces) { + return { + workspaces: finalWorkspaces, + }; + } + + return {}; + } + /** * Throttled processing of get requests into bulk requests at 100ms interval */ @@ -233,13 +258,8 @@ export class SavedObjectsClient { this.batchQueue = []; } - private _getCurrentWorkspace(): string | undefined { - return this.currentWorkspaceId; - } - - public setCurrentWorkspace(workspaceId: string): boolean { + public setCurrentWorkspace(workspaceId: string) { this.currentWorkspaceId = workspaceId; - return true; } /** @@ -263,13 +283,6 @@ export class SavedObjectsClient { const query = { overwrite: options.overwrite, }; - const currentWorkspaceId = this._getCurrentWorkspace(); - let finalWorkspaces; - if (options.hasOwnProperty('workspaces')) { - finalWorkspaces = options.workspaces; - } else if (typeof currentWorkspaceId === 'string') { - finalWorkspaces = [currentWorkspaceId]; - } const createRequest: Promise> = this.savedObjectsFetch(path, { method: 'POST', @@ -278,11 +291,7 @@ export class SavedObjectsClient { attributes, migrationVersion: options.migrationVersion, references: options.references, - ...(finalWorkspaces - ? { - workspaces: finalWorkspaces, - } - : {}), + ...this.formatWorkspacesParams(options), }), }); @@ -302,11 +311,14 @@ export class SavedObjectsClient { options: SavedObjectsBulkCreateOptions = { overwrite: false } ) => { const path = this.getPath(['_bulk_create']); - const query = { overwrite: options.overwrite }; + const query: HttpFetchOptions['query'] = { overwrite: options.overwrite }; const request: ReturnType = this.savedObjectsFetch(path, { method: 'POST', - query, + query: { + ...query, + ...this.formatWorkspacesParams(options), + }, body: JSON.stringify(objects), }); return request.then((resp) => { @@ -376,25 +388,10 @@ export class SavedObjectsClient { flags: 'flags', }; - const currentWorkspaceId = this._getCurrentWorkspace(); - let finalWorkspaces; - if (options.hasOwnProperty('workspaces')) { - finalWorkspaces = options.workspaces; - } else if (typeof currentWorkspaceId === 'string') { - finalWorkspaces = Array.from(new Set([currentWorkspaceId])); - } - - const renamedQuery = renameKeys, any>( - renameMap, - { - ...options, - ...(finalWorkspaces - ? { - workspaces: finalWorkspaces, - } - : {}), - } - ); + const renamedQuery = renameKeys(renameMap, { + ...options, + ...this.formatWorkspacesParams(options), + }); const query = pick.apply(null, [renamedQuery, ...Object.values(renameMap)]) as Partial< Record >; diff --git a/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts b/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts index 19737c9f8dec..20779f5775c8 100644 --- a/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts +++ b/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts @@ -814,6 +814,73 @@ describe('getSortedObjectsForExport()', () => { `); }); + test('exports selected objects when passed workspaces', async () => { + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '2', + type: 'search', + attributes: {}, + references: [ + { + id: '1', + name: 'name', + type: 'index-pattern', + }, + ], + }, + { + id: '1', + type: 'index-pattern', + attributes: {}, + references: [], + }, + ], + }); + await exportSavedObjectsToStream({ + exportSizeLimit: 10000, + savedObjectsClient, + objects: [ + { + type: 'index-pattern', + id: '1', + }, + { + type: 'search', + id: '2', + }, + ], + workspaces: ['foo'], + }); + expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` + [MockFunction] { + "calls": Array [ + Array [ + Array [ + Object { + id: 1, + type: index-pattern, + }, + Object { + id: 2, + type: search, + }, + ], + Object { + namespace: undefined, + }, + ], + ], + "results": Array [ + Object { + type: return, + value: Promise {}, + }, + ], + } + `); + }); + test('export selected objects throws error when exceeding exportSizeLimit', async () => { const exportOpts = { exportSizeLimit: 1, diff --git a/src/core/server/saved_objects/import/resolve_import_errors.test.ts b/src/core/server/saved_objects/import/resolve_import_errors.test.ts index ef22155f046b..35ca022df276 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.test.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.test.ts @@ -242,7 +242,8 @@ describe('#importSavedObjectsFromStream', () => { test('checks conflicts', async () => { const createNewCopies = (Symbol() as unknown) as boolean; const retries = [createRetry()]; - const options = setupOptions(retries, createNewCopies); + const workspaces = ['foo']; + const options = { ...setupOptions(retries, createNewCopies), workspaces }; const collectedObjects = [createObject()]; getMockFn(collectSavedObjects).mockResolvedValue({ errors: [], @@ -257,6 +258,7 @@ describe('#importSavedObjectsFromStream', () => { namespace, retries, createNewCopies, + workspaces, }; expect(checkConflicts).toHaveBeenCalledWith(checkConflictsParams); }); diff --git a/src/core/server/saved_objects/import/resolve_import_errors.ts b/src/core/server/saved_objects/import/resolve_import_errors.ts index 24bbc1934de3..7e991cc0e1aa 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.ts @@ -132,6 +132,7 @@ export async function resolveSavedObjectsImportErrors({ createNewCopies, workspaces, dataSourceId, + workspaces, }; const checkConflictsResult = await checkConflicts(checkConflictsParams); errorAccumulator = [...errorAccumulator, ...checkConflictsResult.errors]; diff --git a/src/plugins/workspace/public/plugin.test.ts b/src/plugins/workspace/public/plugin.test.ts index 9a476b60d208..4ad4bef0ba42 100644 --- a/src/plugins/workspace/public/plugin.test.ts +++ b/src/plugins/workspace/public/plugin.test.ts @@ -158,4 +158,12 @@ describe('Workspace plugin', () => { workspacePlugin.start(coreStart); expect(navLinksService.setNavLinks).toHaveBeenCalledWith(filteredNavLinksMap); }); + + it('#call savedObjectsClient.setCurrentWorkspace when current workspace id changed', () => { + const workspacePlugin = new WorkspacePlugin(); + const coreStart = coreMock.createStart(); + workspacePlugin.start(coreStart); + coreStart.workspaces.currentWorkspaceId$.next('foo'); + expect(coreStart.savedObjects.client.setCurrentWorkspace).toHaveBeenCalledWith('foo'); + }); });