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 952a74a76940..19737c9f8dec 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 @@ -128,7 +128,6 @@ describe('getSortedObjectsForExport()', () => { index-pattern, search, ], - workspaces: undefined, }, ], ], @@ -219,7 +218,6 @@ describe('getSortedObjectsForExport()', () => { index-pattern, search, ], - workspaces: undefined, }, ], ], @@ -370,7 +368,6 @@ describe('getSortedObjectsForExport()', () => { index-pattern, search, ], - workspaces: undefined, }, ], ], @@ -462,7 +459,6 @@ describe('getSortedObjectsForExport()', () => { index-pattern, search, ], - workspaces: undefined, }, ], ], diff --git a/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts b/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts index 8ca085639f10..189318522bec 100644 --- a/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts +++ b/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts @@ -125,7 +125,7 @@ async function fetchObjectsToExport({ search, perPage: exportSizeLimit, namespaces: namespace ? [namespace] : undefined, - workspaces, + ...(workspaces ? { workspaces } : {}), }); if (findResponse.total > exportSizeLimit) { throw Boom.badRequest(`Can't export more than ${exportSizeLimit} objects`); diff --git a/src/core/server/saved_objects/import/import_saved_objects.test.ts b/src/core/server/saved_objects/import/import_saved_objects.test.ts index dcb8d685d42c..3dda6931bd1e 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.test.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.test.ts @@ -42,7 +42,7 @@ import { typeRegistryMock } from '../saved_objects_type_registry.mock'; import { importSavedObjectsFromStream } from './import_saved_objects'; import { collectSavedObjects } from './collect_saved_objects'; -import { regenerateIds, regenerateIdsWithReference } from './regenerate_ids'; +import { regenerateIds } from './regenerate_ids'; import { validateReferences } from './validate_references'; import { checkConflicts } from './check_conflicts'; import { checkOriginConflicts } from './check_origin_conflicts'; @@ -70,7 +70,6 @@ describe('#importSavedObjectsFromStream', () => { importIdMap: new Map(), }); getMockFn(regenerateIds).mockReturnValue(new Map()); - getMockFn(regenerateIdsWithReference).mockReturnValue(Promise.resolve(new Map())); getMockFn(validateReferences).mockResolvedValue([]); getMockFn(checkConflicts).mockResolvedValue({ errors: [], @@ -279,15 +278,6 @@ describe('#importSavedObjectsFromStream', () => { ]), }); getMockFn(validateReferences).mockResolvedValue([errors[1]]); - getMockFn(regenerateIdsWithReference).mockResolvedValue( - Promise.resolve( - new Map([ - ['foo', {}], - ['bar', {}], - ['baz', {}], - ]) - ) - ); getMockFn(checkConflicts).mockResolvedValue({ errors: [errors[2]], filteredObjects, diff --git a/src/core/server/saved_objects/import/import_saved_objects.ts b/src/core/server/saved_objects/import/import_saved_objects.ts index 51a790aca5d2..7cdb6970ca9d 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.ts @@ -38,7 +38,7 @@ import { validateReferences } from './validate_references'; import { checkOriginConflicts } from './check_origin_conflicts'; import { createSavedObjects } from './create_saved_objects'; import { checkConflicts } from './check_conflicts'; -import { regenerateIds, regenerateIdsWithReference } from './regenerate_ids'; +import { regenerateIds } from './regenerate_ids'; import { checkConflictsForDataSource } from './check_conflict_for_data_source'; /** @@ -86,13 +86,6 @@ export async function importSavedObjectsFromStream({ // randomly generated id importIdMap = regenerateIds(collectSavedObjectsResult.collectedObjects, dataSourceId); } else { - importIdMap = await regenerateIdsWithReference({ - savedObjects: collectSavedObjectsResult.collectedObjects, - savedObjectsClient, - workspaces, - objectLimit, - importIdMap, - }); // in check conclict and override mode // Check single-namespace objects for conflicts in this namespace, and check multi-namespace objects for conflicts across all namespaces const checkConflictsParams = { @@ -152,6 +145,7 @@ export async function importSavedObjectsFromStream({ ...(workspaces ? { workspaces } : {}), dataSourceId, dataSourceTitle, + ...(workspaces ? { workspaces } : {}), }; const createSavedObjectsResult = await createSavedObjects(createSavedObjectsParams); errorAccumulator = [...errorAccumulator, ...createSavedObjectsResult.errors]; diff --git a/src/core/server/saved_objects/import/regenerate_ids.ts b/src/core/server/saved_objects/import/regenerate_ids.ts index b412bb80bc6b..f1092bed7f55 100644 --- a/src/core/server/saved_objects/import/regenerate_ids.ts +++ b/src/core/server/saved_objects/import/regenerate_ids.ts @@ -29,8 +29,7 @@ */ import { v4 as uuidv4 } from 'uuid'; -import { SavedObject, SavedObjectsClientContract } from '../types'; -import { SavedObjectsUtils } from '../service'; +import { SavedObject } from '../types'; /** * Takes an array of saved objects and returns an importIdMap of randomly-generated new IDs. @@ -48,40 +47,3 @@ export const regenerateIds = (objects: SavedObject[], dataSourceId: string | und }, new Map()); return importIdMap; }; - -export const regenerateIdsWithReference = async (props: { - savedObjects: SavedObject[]; - savedObjectsClient: SavedObjectsClientContract; - workspaces?: string[]; - objectLimit: number; - importIdMap: Map; -}): Promise> => { - const { savedObjects, savedObjectsClient, workspaces, importIdMap } = props; - if (!workspaces || !workspaces.length) { - return savedObjects.reduce((acc, object) => { - return acc.set(`${object.type}:${object.id}`, { id: object.id, omitOriginId: false }); - }, importIdMap); - } - - const bulkGetResult = await savedObjectsClient.bulkGet( - savedObjects.map((item) => ({ type: item.type, id: item.id })) - ); - - return bulkGetResult.saved_objects.reduce((acc, object) => { - if (object.error?.statusCode === 404) { - acc.set(`${object.type}:${object.id}`, { id: object.id, omitOriginId: true }); - return acc; - } - - const filteredWorkspaces = SavedObjectsUtils.filterWorkspacesAccordingToBaseWorkspaces( - workspaces, - object.workspaces - ); - if (filteredWorkspaces.length) { - acc.set(`${object.type}:${object.id}`, { id: uuidv4(), omitOriginId: true }); - } else { - acc.set(`${object.type}:${object.id}`, { id: object.id, omitOriginId: false }); - } - return acc; - }, importIdMap); -}; diff --git a/src/core/server/saved_objects/import/types.ts b/src/core/server/saved_objects/import/types.ts index a02d42e5c363..d0433b72766a 100644 --- a/src/core/server/saved_objects/import/types.ts +++ b/src/core/server/saved_objects/import/types.ts @@ -187,7 +187,7 @@ export interface SavedObjectsImportOptions { namespace?: string; /** If true, will create new copies of import objects, each with a random `id` and undefined `originId`. */ createNewCopies: boolean; - /** if specified, will import in given workspaces, else will import as global object */ + /** if specified, will import in given workspaces */ workspaces?: string[]; dataSourceId?: string; dataSourceTitle?: string; @@ -212,7 +212,7 @@ export interface SavedObjectsResolveImportErrorsOptions { namespace?: string; /** If true, will create new copies of import objects, each with a random `id` and undefined `originId`. */ createNewCopies: boolean; - /** if specified, will import in given workspaces, else will import as global object */ + /** if specified, will import in given workspaces */ workspaces?: string[]; dataSourceId?: string; dataSourceTitle?: string; diff --git a/src/core/server/saved_objects/import/validate_references.ts b/src/core/server/saved_objects/import/validate_references.ts index fb75eb837443..545f26ebe1af 100644 --- a/src/core/server/saved_objects/import/validate_references.ts +++ b/src/core/server/saved_objects/import/validate_references.ts @@ -73,7 +73,10 @@ export async function getNonExistingReferenceAsKeys( } // Fetch references to see if they exist - const bulkGetOpts = Array.from(collector.values()).map((obj) => ({ ...obj, fields: ['id'] })); + const bulkGetOpts = Array.from(collector.values()).map((obj) => ({ + ...obj, + fields: ['id'], + })); const bulkGetResponse = await savedObjectsClient.bulkGet(bulkGetOpts, { namespace }); // Error handling diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 816d1013e5be..c548fb144a95 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -629,6 +629,7 @@ describe('SavedObjectsRepository', () => { references: [{ name: 'ref_0', type: 'test', id: '2' }], }; const namespace = 'foo-namespace'; + const workspace = 'foo-workspace'; const getMockBulkCreateResponse = (objects, namespace) => { return { @@ -651,7 +652,6 @@ describe('SavedObjectsRepository', () => { }; const bulkCreateSuccess = async (objects, options) => { - const originalObjects = JSON.parse(JSON.stringify(objects)); const multiNamespaceObjects = objects.filter( ({ type, id }) => registry.isMultiNamespace(type) && id ); @@ -666,9 +666,7 @@ describe('SavedObjectsRepository', () => { opensearchClientMock.createSuccessTransportRequestPromise(response) ); const result = await savedObjectsRepository.bulkCreate(objects, options); - expect(client.mget).toHaveBeenCalledTimes( - multiNamespaceObjects?.length || originalObjects?.some((item) => item.id) ? 1 : 0 - ); + expect(client.mget).toHaveBeenCalledTimes(multiNamespaceObjects?.length ? 1 : 0); return result; }; @@ -726,10 +724,7 @@ describe('SavedObjectsRepository', () => { await bulkCreateSuccess(objects); expect(client.bulk).toHaveBeenCalledTimes(1); expect(client.mget).toHaveBeenCalledTimes(1); - const docs = [ - expect.objectContaining({ _id: `${obj1.type}:${obj1.id}` }), - expect.objectContaining({ _id: `${MULTI_NAMESPACE_TYPE}:${obj2.id}` }), - ]; + const docs = [expect.objectContaining({ _id: `${MULTI_NAMESPACE_TYPE}:${obj2.id}` })]; expect(client.mget.mock.calls[0][0].body).toEqual({ docs }); }); @@ -922,6 +917,16 @@ describe('SavedObjectsRepository', () => { await bulkCreateSuccess(objects, { namespace }); expectClientCallArgsAction(objects, { method: 'create', getId }); }); + + it(`adds workspaces to request body for any types`, async () => { + await bulkCreateSuccess([obj1, obj2], { workspaces: [workspace] }); + const expected = expect.objectContaining({ workspaces: [workspace] }); + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + }); }); describe('errors', () => { @@ -1007,12 +1012,6 @@ describe('SavedObjectsRepository', () => { const response1 = { status: 200, docs: [ - { - found: true, - _source: { - type: obj1.type, - }, - }, { found: true, _source: { @@ -1036,11 +1035,7 @@ describe('SavedObjectsRepository', () => { expect(client.mget).toHaveBeenCalled(); const body1 = { - docs: [ - expect.objectContaining({ _id: `${obj1.type}:${obj1.id}` }), - expect.objectContaining({ _id: `${obj.type}:${obj.id}` }), - expect.objectContaining({ _id: `${obj2.type}:${obj2.id}` }), - ], + docs: [expect.objectContaining({ _id: `${obj.type}:${obj.id}` })], }; expect(client.mget).toHaveBeenCalledWith( expect.objectContaining({ body: body1 }), @@ -2050,17 +2045,9 @@ describe('SavedObjectsRepository', () => { const createSuccess = async (type, attributes, options) => { const result = await savedObjectsRepository.create(type, attributes, options); - let count = 0; - if (options?.overwrite && options?.id) { - /** - * workspace will call extra one to get latest status of current object - */ - count++; - } - if (registry.isMultiNamespace(type) && options.overwrite) { - count++; - } - expect(client.get).toHaveBeenCalledTimes(count); + expect(client.get).toHaveBeenCalledTimes( + registry.isMultiNamespace(type) && options.overwrite ? 1 : 0 + ); return result; }; diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 3ee7889475d2..314f96b2016c 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -287,26 +287,6 @@ export class SavedObjectsRepository { } } - let savedObjectWorkspaces = workspaces; - - if (id && overwrite) { - try { - const currentItem = await this.get(type, id); - if ( - SavedObjectsUtils.filterWorkspacesAccordingToBaseWorkspaces( - workspaces, - currentItem.workspaces - ).length - ) { - throw SavedObjectsErrorHelpers.createConflictError(type, id); - } else { - savedObjectWorkspaces = currentItem.workspaces; - } - } catch (e) { - // this.get will throw an error when no items can be found - } - } - const migrated = this._migrator.migrateDocument({ id, type, @@ -317,7 +297,7 @@ export class SavedObjectsRepository { migrationVersion, updated_at: time, ...(Array.isArray(references) && { references }), - ...(Array.isArray(savedObjectWorkspaces) && { workspaces: savedObjectWorkspaces }), + ...(Array.isArray(workspaces) && { workspaces }), ...(permissions && { permissions }), }); @@ -385,28 +365,15 @@ export class SavedObjectsRepository { const method = object.id && overwrite ? 'index' : 'create'; const requiresNamespacesCheck = object.id && this._registry.isMultiNamespace(object.type); - /** - * Only when importing an object to a target workspace should we check if the object is workspace-specific. - */ - const requiresWorkspaceCheck = object.id; if (object.id == null) object.id = uuid.v1(); - let opensearchRequestIndexPayload = {}; - - if (requiresNamespacesCheck || requiresWorkspaceCheck) { - opensearchRequestIndexPayload = { - opensearchRequestIndex: bulkGetRequestIndexCounter, - }; - bulkGetRequestIndexCounter++; - } - return { tag: 'Right' as 'Right', value: { method, object, - ...opensearchRequestIndexPayload, + ...(requiresNamespacesCheck && { opensearchRequestIndex: bulkGetRequestIndexCounter++ }), }, }; }); @@ -417,7 +384,7 @@ export class SavedObjectsRepository { .map(({ value: { object: { type, id } } }) => ({ _id: this._serializer.generateRawId(namespace, type, id), _index: this.getIndexForType(type), - _source: ['type', 'namespaces', 'workspaces'], + _source: ['type', 'namespaces'], })); const bulkGetResponse = bulkGetDocs.length ? await this.client.mget( @@ -445,7 +412,6 @@ export class SavedObjectsRepository { object: { initialNamespaces, version, ...object }, method, } = expectedBulkGetResult.value; - let savedObjectWorkspaces: string[] | undefined; if (opensearchRequestIndex !== undefined) { const indexFound = bulkGetResponse?.statusCode !== 404; const actualResult = indexFound @@ -492,39 +458,10 @@ export class SavedObjectsRepository { versionProperties = getExpectedVersionProperties(version); } - savedObjectWorkspaces = options.workspaces; + let savedObjectWorkspaces = options.workspaces; if (expectedBulkGetResult.value.method !== 'create') { - const rawId = this._serializer.generateRawId(namespace, object.type, object.id); - const findObject = - bulkGetResponse?.statusCode !== 404 - ? bulkGetResponse?.body.docs?.find((item) => item._id === rawId) - : null; - if (findObject && findObject.found) { - const transformedObject = this._serializer.rawToSavedObject( - findObject as SavedObjectsRawDoc - ) as SavedObject; - const filteredWorkspaces = SavedObjectsUtils.filterWorkspacesAccordingToBaseWorkspaces( - options.workspaces, - transformedObject.workspaces - ); - if (filteredWorkspaces.length) { - const { id, type } = object; - return { - tag: 'Left' as 'Left', - error: { - id, - type, - error: { - ...errorContent(SavedObjectsErrorHelpers.createConflictError(type, id)), - metadata: { isNotOverwritable: true }, - }, - }, - }; - } else { - savedObjectWorkspaces = transformedObject.workspaces; - } - } + savedObjectWorkspaces = object.workspaces; } const expectedResult = { @@ -541,7 +478,7 @@ export class SavedObjectsRepository { updated_at: time, references: object.references || [], originId: object.originId, - workspaces: savedObjectWorkspaces, + ...(savedObjectWorkspaces && { workspaces: savedObjectWorkspaces }), }) as SavedObjectSanitizedDoc ), }; @@ -639,7 +576,7 @@ export class SavedObjectsRepository { const bulkGetDocs = expectedBulkGetResults.filter(isRight).map(({ value: { type, id } }) => ({ _id: this._serializer.generateRawId(namespace, type, id), _index: this.getIndexForType(type), - _source: ['type', 'namespaces', 'workspaces'], + _source: ['type', 'namespaces'], })); const bulkGetResponse = bulkGetDocs.length ? await this.client.mget( @@ -662,24 +599,13 @@ export class SavedObjectsRepository { const { type, id, opensearchRequestIndex } = expectedResult.value; const doc = bulkGetResponse?.body.docs[opensearchRequestIndex]; if (doc?.found) { - let workspaceConflict = false; - if (options.workspaces) { - const transformedObject = this._serializer.rawToSavedObject(doc as SavedObjectsRawDoc); - const filteredWorkspaces = SavedObjectsUtils.filterWorkspacesAccordingToBaseWorkspaces( - options.workspaces, - transformedObject.workspaces - ); - if (filteredWorkspaces.length) { - workspaceConflict = true; - } - } errors.push({ id, type, error: { ...errorContent(SavedObjectsErrorHelpers.createConflictError(type, id)), // @ts-expect-error MultiGetHit._source is optional - ...((!this.rawDocExistsInNamespace(doc!, namespace) || workspaceConflict) && { + ...(!this.rawDocExistsInNamespace(doc!, namespace) && { metadata: { isNotOverwritable: true }, }), }, diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts index a4c1e602829c..9c4cb9519102 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts @@ -644,6 +644,27 @@ describe('#getQueryParams', () => { ]); }); }); + + describe('when using workspace search', () => { + it('using normal workspaces', () => { + const result: Result = getQueryParams({ + registry, + workspaces: ['foo'], + }); + expect(result.query.bool.filter[1]).toEqual({ + bool: { + should: [ + { + bool: { + must: [{ term: { workspaces: 'foo' } }], + }, + }, + ], + minimum_should_match: 1, + }, + }); + }); + }); }); describe('namespaces property', () => { diff --git a/src/core/server/saved_objects/service/lib/utils.ts b/src/core/server/saved_objects/service/lib/utils.ts index 490c2b7083d2..4823e52d77c9 100644 --- a/src/core/server/saved_objects/service/lib/utils.ts +++ b/src/core/server/saved_objects/service/lib/utils.ts @@ -80,11 +80,4 @@ export class SavedObjectsUtils { total: 0, saved_objects: [], }); - - public static filterWorkspacesAccordingToBaseWorkspaces( - targetWorkspaces?: string[], - baseWorkspaces?: string[] - ): string[] { - return targetWorkspaces?.filter((item) => !baseWorkspaces?.includes(item)) || []; - } } diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts index 4f430d7f7134..a53684c833e2 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -94,6 +94,10 @@ export interface SavedObjectsBulkCreateObject { * Note: this can only be used for multi-namespace object types. */ initialNamespaces?: string[]; + /** + * workspaces the objects belong to, will only be used when overwrite is enabled. + */ + workspaces?: string[]; } /** diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index 90ffcd311fca..d5333fb40aea 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -113,6 +113,7 @@ export interface SavedObjectsFindOptions { typeToNamespacesMap?: Map; /** An optional OpenSearch preference value to be used for the query **/ preference?: string; + /** If specified, will only retrieve objects that are in the workspaces */ workspaces?: string[]; /** * The params here will be combined with bool clause and is used for filtering with ACL structure. @@ -131,6 +132,7 @@ export interface SavedObjectsFindOptions { export interface SavedObjectsBaseOptions { /** Specify the namespace for this operation */ namespace?: string; + /** Specify the workspaces for this operation */ workspaces?: string[]; } diff --git a/src/core/types/saved_objects.ts b/src/core/types/saved_objects.ts index fccce5f72947..c1c8ede27593 100644 --- a/src/core/types/saved_objects.ts +++ b/src/core/types/saved_objects.ts @@ -114,6 +114,7 @@ export interface SavedObject { * space. */ originId?: string; + /** Workspace(s) that this saved object exists in. */ workspaces?: string[]; permissions?: Permissions; } diff --git a/src/plugins/discover_legacy/public/application/components/top_nav/__snapshots__/open_search_panel.test.js.snap b/src/plugins/discover_legacy/public/application/components/top_nav/__snapshots__/open_search_panel.test.js.snap deleted file mode 100644 index 54c90bd3ce92..000000000000 --- a/src/plugins/discover_legacy/public/application/components/top_nav/__snapshots__/open_search_panel.test.js.snap +++ /dev/null @@ -1,69 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`render 1`] = ` - - - -

- -

-
-
- - - } - onChoose={[Function]} - savedObjectMetaData={ - Array [ - Object { - "getIconForSavedObject": [Function], - "name": "Saved search", - "type": "search", - }, - ] - } - savedObjects={Object {}} - uiSettings={Object {}} - /> - - - - - - - - - - -
-`; diff --git a/src/plugins/discover_legacy/public/application/components/top_nav/open_search_panel.js b/src/plugins/discover_legacy/public/application/components/top_nav/open_search_panel.js deleted file mode 100644 index 7f5d04d415e7..000000000000 --- a/src/plugins/discover_legacy/public/application/components/top_nav/open_search_panel.js +++ /dev/null @@ -1,122 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import rison from 'rison-node'; -import { i18n } from '@osd/i18n'; -import { FormattedMessage } from '@osd/i18n/react'; -import { - EuiButton, - EuiFlexGroup, - EuiFlexItem, - EuiFlyout, - EuiFlyoutHeader, - EuiFlyoutFooter, - EuiFlyoutBody, - EuiTitle, -} from '@elastic/eui'; -import { SavedObjectFinderUi } from '../../../../../saved_objects/public'; -import { getServices } from '../../../opensearch_dashboards_services'; - -const SEARCH_OBJECT_TYPE = 'search'; - -export function OpenSearchPanel(props) { - const { - core: { uiSettings, savedObjects }, - addBasePath, - } = getServices(); - - return ( - - - -

- -

-
-
- - - } - savedObjectMetaData={[ - { - type: SEARCH_OBJECT_TYPE, - getIconForSavedObject: () => 'search', - name: i18n.translate('discover.savedSearch.savedObjectName', { - defaultMessage: 'Saved search', - }), - }, - ]} - onChoose={(id) => { - window.location.assign(props.makeUrl(id)); - props.onClose(); - }} - uiSettings={uiSettings} - savedObjects={savedObjects} - /> - - - - - {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} - - - - - - -
- ); -} - -OpenSearchPanel.propTypes = { - onClose: PropTypes.func.isRequired, - makeUrl: PropTypes.func.isRequired, -}; diff --git a/src/plugins/workspace/common/constants.ts b/src/plugins/workspace/common/constants.ts index ef0821c88619..426055588905 100644 --- a/src/plugins/workspace/common/constants.ts +++ b/src/plugins/workspace/common/constants.ts @@ -19,3 +19,5 @@ export const PATHS = { export const WORKSPACE_OP_TYPE_CREATE = 'create'; export const WORKSPACE_OP_TYPE_UPDATE = 'update'; export const WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID = 'workspace'; +export const WORKSPACE_CONFLICT_CONTROL_SAVED_OBJECTS_CLIENT_WRAPPER_ID = + 'workspace_conflict_control'; diff --git a/src/plugins/workspace/server/plugin.ts b/src/plugins/workspace/server/plugin.ts index 12c101cae6bb..17552891439b 100644 --- a/src/plugins/workspace/server/plugin.ts +++ b/src/plugins/workspace/server/plugin.ts @@ -11,13 +11,15 @@ import { Plugin, Logger, SavedObjectsClient, - WORKSPACE_TYPE, } from '../../../core/server'; import { IWorkspaceClientImpl } from './types'; import { WorkspaceClientWithSavedObject } from './workspace_client'; import { WorkspaceSavedObjectsClientWrapper } from './saved_objects'; import { registerRoutes } from './routes'; -import { WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID } from '../common/constants'; +import { + WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID, + WORKSPACE_CONFLICT_CONTROL_SAVED_OBJECTS_CLIENT_WRAPPER_ID, +} from '../common/constants'; import { SavedObjectsPermissionControl, SavedObjectsPermissionControlContract, @@ -25,6 +27,7 @@ import { import { registerPermissionCheckRoutes } from './permission_control/routes'; import { ConfigSchema } from '../config'; import { cleanWorkspaceId, getWorkspaceIdFromUrl } from '../../../core/server/utils'; +import { WorkspaceConflictSavedObjectsClientWrapper } from './saved_objects/saved_objects_wrapper_for_check_workspace_conflict'; export class WorkspacePlugin implements Plugin<{}, {}> { private readonly logger: Logger; @@ -32,6 +35,7 @@ export class WorkspacePlugin implements Plugin<{}, {}> { private permissionControl?: SavedObjectsPermissionControlContract; private readonly config$: Observable; private workspaceSavedObjectsClientWrapper?: WorkspaceSavedObjectsClientWrapper; + private workspaceConflictControl?: WorkspaceConflictSavedObjectsClientWrapper; private proxyWorkspaceTrafficToRealHandler(setupDeps: CoreSetup) { /** @@ -85,6 +89,13 @@ export class WorkspacePlugin implements Plugin<{}, {}> { } this.proxyWorkspaceTrafficToRealHandler(core); + this.workspaceConflictControl = new WorkspaceConflictSavedObjectsClientWrapper(); + + core.savedObjects.addClientWrapper( + -1, + WORKSPACE_CONFLICT_CONTROL_SAVED_OBJECTS_CLIENT_WRAPPER_ID, + this.workspaceConflictControl.wrapperFactory + ); registerRoutes({ http: core.http, @@ -114,6 +125,7 @@ export class WorkspacePlugin implements Plugin<{}, {}> { this.permissionControl?.setup(core.savedObjects.getScopedClient); this.client?.setSavedObjects(core.savedObjects); this.workspaceSavedObjectsClientWrapper?.setScopedClient(core.savedObjects.getScopedClient); + this.workspaceConflictControl?.setSerializer(core.savedObjects.createSerializer()); return { client: this.client as IWorkspaceClientImpl, diff --git a/src/plugins/workspace/server/saved_objects/integration_tests/saved_objects_wrapper_for_check_workspace_conflict.test.ts b/src/plugins/workspace/server/saved_objects/integration_tests/saved_objects_wrapper_for_check_workspace_conflict.test.ts new file mode 100644 index 000000000000..147f0ab159d3 --- /dev/null +++ b/src/plugins/workspace/server/saved_objects/integration_tests/saved_objects_wrapper_for_check_workspace_conflict.test.ts @@ -0,0 +1,344 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObject } from 'src/core/types'; +import { isEqual } from 'lodash'; +import * as osdTestServer from '../../../../../core/test_helpers/osd_server'; + +const dashboard: Omit = { + type: 'dashboard', + attributes: {}, + references: [], +}; + +interface WorkspaceAttributes { + id: string; + name?: string; +} + +describe('saved_objects_wrapper_for_check_workspace_conflict integration test', () => { + let root: ReturnType; + let opensearchServer: osdTestServer.TestOpenSearchUtils; + let createdFooWorkspace: WorkspaceAttributes = { + id: '', + }; + let createdBarWorkspace: WorkspaceAttributes = { + id: '', + }; + beforeAll(async () => { + const { startOpenSearch, startOpenSearchDashboards } = osdTestServer.createTestServers({ + adjustTimeout: (t: number) => jest.setTimeout(t), + settings: { + osd: { + workspace: { + enabled: true, + permission: { + enabled: true, + }, + }, + }, + }, + }); + opensearchServer = await startOpenSearch(); + const startOSDResp = await startOpenSearchDashboards(); + root = startOSDResp.root; + const createWorkspace = (workspaceAttribute: Omit) => + osdTestServer.request.post(root, `/api/workspaces`).send({ + attributes: workspaceAttribute, + }); + + createdFooWorkspace = await createWorkspace({ + name: 'foo', + }).then((resp) => resp.body.result); + createdBarWorkspace = await createWorkspace({ + name: 'bar', + }).then((resp) => resp.body.result); + }, 30000); + afterAll(async () => { + await root.shutdown(); + await opensearchServer.stop(); + }); + + const deleteItem = async (object: Pick) => { + expect( + [200, 404].includes( + (await osdTestServer.request.delete(root, `/api/saved_objects/${object.type}/${object.id}`)) + .statusCode + ) + ).toEqual(true); + }; + + const getItem = async (object: Pick) => { + return await osdTestServer.request + .get(root, `/api/saved_objects/${object.type}/${object.id}`) + .expect(200); + }; + + const clearFooAndBar = async () => { + await deleteItem({ + type: dashboard.type, + id: 'foo', + }); + await deleteItem({ + type: dashboard.type, + id: 'bar', + }); + }; + + describe('workspace related CRUD', () => { + it('create', async () => { + const createResult = await osdTestServer.request + .post(root, `/api/saved_objects/${dashboard.type}`) + .send({ + attributes: dashboard.attributes, + workspaces: [createdFooWorkspace.id], + }) + .expect(200); + + expect(createResult.body.workspaces).toEqual([createdFooWorkspace.id]); + await deleteItem({ + type: dashboard.type, + id: createResult.body.id, + }); + }); + + it('create-with-override', async () => { + const createResult = await osdTestServer.request + .post(root, `/api/saved_objects/${dashboard.type}`) + .send({ + attributes: dashboard.attributes, + workspaces: [createdFooWorkspace.id], + }) + .expect(200); + + await osdTestServer.request + .post(root, `/api/saved_objects/${dashboard.type}/${createResult.body.id}?overwrite=true`) + .send({ + attributes: dashboard.attributes, + workspaces: [createdBarWorkspace.id], + }) + .expect(409); + + await deleteItem({ + type: dashboard.type, + id: createResult.body.id, + }); + }); + + it('bulk create', async () => { + await clearFooAndBar(); + const createResultFoo = await osdTestServer.request + .post(root, `/api/saved_objects/_bulk_create?workspaces=${createdFooWorkspace.id}`) + .send([ + { + ...dashboard, + id: 'foo', + }, + ]) + .expect(200); + + const createResultBar = await osdTestServer.request + .post(root, `/api/saved_objects/_bulk_create?workspaces=${createdBarWorkspace.id}`) + .send([ + { + ...dashboard, + id: 'bar', + }, + ]) + .expect(200); + + expect((createResultFoo.body.saved_objects as any[]).some((item) => item.error)).toEqual( + false + ); + expect( + (createResultFoo.body.saved_objects as any[]).every((item) => + isEqual(item.workspaces, [createdFooWorkspace.id]) + ) + ).toEqual(true); + expect((createResultBar.body.saved_objects as any[]).some((item) => item.error)).toEqual( + false + ); + expect( + (createResultBar.body.saved_objects as any[]).every((item) => + isEqual(item.workspaces, [createdBarWorkspace.id]) + ) + ).toEqual(true); + await Promise.all( + [...createResultFoo.body.saved_objects, ...createResultBar.body.saved_objects].map((item) => + deleteItem({ + type: item.type, + id: item.id, + }) + ) + ); + }); + + it('bulk create with conflict', async () => { + await clearFooAndBar(); + const createResultFoo = await osdTestServer.request + .post(root, `/api/saved_objects/_bulk_create?workspaces=${createdFooWorkspace.id}`) + .send([ + { + ...dashboard, + id: 'foo', + }, + ]) + .expect(200); + + const createResultBar = await osdTestServer.request + .post(root, `/api/saved_objects/_bulk_create?workspaces=${createdBarWorkspace.id}`) + .send([ + { + ...dashboard, + id: 'bar', + }, + ]) + .expect(200); + + /** + * overwrite with workspaces + */ + const overwriteWithWorkspacesResult = await osdTestServer.request + .post( + root, + `/api/saved_objects/_bulk_create?overwrite=true&workspaces=${createdFooWorkspace.id}` + ) + .send([ + { + ...dashboard, + id: 'bar', + }, + { + ...dashboard, + id: 'foo', + attributes: { + title: 'foo', + }, + }, + ]) + .expect(200); + + expect(overwriteWithWorkspacesResult.body.saved_objects[0].error.statusCode).toEqual(409); + expect(overwriteWithWorkspacesResult.body.saved_objects[1].attributes.title).toEqual('foo'); + expect(overwriteWithWorkspacesResult.body.saved_objects[1].workspaces).toEqual([ + createdFooWorkspace.id, + ]); + + await Promise.all( + [...createResultFoo.body.saved_objects, ...createResultBar.body.saved_objects].map((item) => + deleteItem({ + type: item.type, + id: item.id, + }) + ) + ); + }); + + it('checkConflicts when importing ndjson', async () => { + await clearFooAndBar(); + const createResultFoo = await osdTestServer.request + .post(root, `/api/saved_objects/_bulk_create?workspaces=${createdFooWorkspace.id}`) + .send([ + { + ...dashboard, + id: 'foo', + }, + ]) + .expect(200); + + const createResultBar = await osdTestServer.request + .post(root, `/api/saved_objects/_bulk_create?workspaces=${createdBarWorkspace.id}`) + .send([ + { + ...dashboard, + id: 'bar', + }, + ]) + .expect(200); + + const getResultFoo = await getItem({ + type: dashboard.type, + id: 'foo', + }); + const getResultBar = await getItem({ + type: dashboard.type, + id: 'bar', + }); + + /** + * import with workspaces when conflicts + */ + const importWithWorkspacesResult = await osdTestServer.request + .post( + root, + `/api/saved_objects/_import?workspaces=${createdFooWorkspace.id}&overwrite=false` + ) + .attach( + 'file', + Buffer.from( + [JSON.stringify(getResultFoo.body), JSON.stringify(getResultBar.body)].join('\n'), + 'utf-8' + ), + 'tmp.ndjson' + ) + .expect(200); + + expect(importWithWorkspacesResult.body.success).toEqual(false); + expect(importWithWorkspacesResult.body.errors.length).toEqual(1); + expect(importWithWorkspacesResult.body.errors[0].id).toEqual('foo'); + expect(importWithWorkspacesResult.body.errors[0].error.type).toEqual('conflict'); + + await Promise.all( + [...createResultFoo.body.saved_objects, ...createResultBar.body.saved_objects].map((item) => + deleteItem({ + type: item.type, + id: item.id, + }) + ) + ); + }); + + it('find by workspaces', async () => { + const createResultFoo = await osdTestServer.request + .post(root, `/api/saved_objects/_bulk_create?workspaces=${createdFooWorkspace.id}`) + .send([ + { + ...dashboard, + id: 'foo', + }, + ]) + .expect(200); + + const createResultBar = await osdTestServer.request + .post(root, `/api/saved_objects/_bulk_create?workspaces=${createdBarWorkspace.id}`) + .send([ + { + ...dashboard, + id: 'bar', + }, + ]) + .expect(200); + + const findResult = await osdTestServer.request + .get( + root, + `/api/saved_objects/_find?workspaces=${createdBarWorkspace.id}&type=${dashboard.type}` + ) + .expect(200); + + expect(findResult.body.total).toEqual(1); + expect(findResult.body.saved_objects[0].workspaces).toEqual([createdBarWorkspace.id]); + + await Promise.all( + [...createResultFoo.body.saved_objects, ...createResultBar.body.saved_objects].map((item) => + deleteItem({ + type: item.type, + id: item.id, + }) + ) + ); + }); + }); +}); diff --git a/src/plugins/workspace/server/saved_objects/saved_objects_wrapper_for_check_workspace_conflict.test.ts b/src/plugins/workspace/server/saved_objects/saved_objects_wrapper_for_check_workspace_conflict.test.ts new file mode 100644 index 000000000000..cac06d789822 --- /dev/null +++ b/src/plugins/workspace/server/saved_objects/saved_objects_wrapper_for_check_workspace_conflict.test.ts @@ -0,0 +1,351 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObject } from '../../../../core/public'; +import { httpServerMock, savedObjectsClientMock, coreMock } from '../../../../core/server/mocks'; +import { WorkspaceConflictSavedObjectsClientWrapper } from './saved_objects_wrapper_for_check_workspace_conflict'; +import { SavedObjectsSerializer } from '../../../../core/server'; + +describe('WorkspaceConflictSavedObjectsClientWrapper', () => { + const requestHandlerContext = coreMock.createRequestHandlerContext(); + const wrapperInstance = new WorkspaceConflictSavedObjectsClientWrapper(); + const mockedClient = savedObjectsClientMock.create(); + const wrapperClient = wrapperInstance.wrapperFactory({ + client: mockedClient, + typeRegistry: requestHandlerContext.savedObjects.typeRegistry, + request: httpServerMock.createOpenSearchDashboardsRequest(), + }); + const savedObjectsSerializer = new SavedObjectsSerializer( + requestHandlerContext.savedObjects.typeRegistry + ); + const getSavedObject = (savedObject: Partial) => { + const payload: SavedObject = { + references: [], + id: '', + type: 'dashboard', + attributes: {}, + ...savedObject, + }; + + return payload; + }; + wrapperInstance.setSerializer(savedObjectsSerializer); + describe('createWithWorkspaceConflictCheck', () => { + it(`Should reserve the workspace params when overwrite with empty workspaces`, async () => { + mockedClient.get.mockResolvedValueOnce( + getSavedObject({ + id: 'dashboard:foo', + workspaces: ['foo'], + }) + ); + + await wrapperClient.create( + 'dashboard', + { + name: 'foo', + }, + { + id: 'dashboard:foo', + overwrite: true, + workspaces: [], + } + ); + + expect(mockedClient.create).toBeCalledWith( + expect.anything(), + expect.anything(), + expect.objectContaining({ + workspaces: ['foo'], + }) + ); + }); + + it(`Should return error when overwrite with conflict workspaces`, async () => { + mockedClient.get.mockResolvedValueOnce( + getSavedObject({ + id: 'dashboard:foo', + workspaces: ['foo'], + }) + ); + + await expect( + wrapperClient.create( + 'dashboard', + { + name: 'foo', + }, + { + id: 'dashboard:foo', + overwrite: true, + workspaces: ['bar'], + } + ) + ).rejects.toThrowError('Saved object [dashboard/dashboard:foo] conflict'); + }); + }); + + describe('bulkCreateWithWorkspaceConflictCheck', () => { + beforeEach(() => { + mockedClient.bulkCreate.mockClear(); + }); + it(`Should create objects when no workspaces and id present`, async () => { + mockedClient.bulkCreate.mockResolvedValueOnce({ + saved_objects: [], + }); + await wrapperClient.bulkCreate([ + getSavedObject({ + id: 'foo', + }), + ]); + + expect(mockedClient.bulkGet).not.toBeCalled(); + expect(mockedClient.bulkCreate).toBeCalledWith( + [{ attributes: {}, id: 'foo', references: [], type: 'dashboard' }], + {} + ); + }); + + it(`Should create objects when not overwrite`, async () => { + mockedClient.bulkCreate.mockResolvedValueOnce({ + saved_objects: [], + }); + await wrapperClient.bulkCreate([ + getSavedObject({ + id: 'foo', + workspaces: ['foo'], + }), + ]); + + expect(mockedClient.bulkGet).not.toBeCalled(); + expect(mockedClient.bulkCreate).toBeCalledWith( + [{ attributes: {}, id: 'foo', references: [], type: 'dashboard', workspaces: ['foo'] }], + {} + ); + }); + + it(`Should check conflict on workspace when overwrite`, async () => { + mockedClient.bulkCreate.mockResolvedValueOnce({ + saved_objects: [ + getSavedObject({ + id: 'foo', + workspaces: ['foo'], + }), + getSavedObject({ + id: 'bar', + workspaces: ['foo', 'bar'], + }), + ], + }); + mockedClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + getSavedObject({ + id: 'foo', + workspaces: ['foo'], + }), + getSavedObject({ + id: 'bar', + workspaces: ['foo', 'bar'], + }), + getSavedObject({ + id: 'baz', + workspaces: ['baz'], + }), + getSavedObject({ + id: 'qux', + error: { + statusCode: 404, + message: 'object not found', + error: 'object not found', + }, + }), + ], + }); + const result = await wrapperClient.bulkCreate( + [ + getSavedObject({ + id: 'foo', + }), + getSavedObject({ + id: 'bar', + }), + getSavedObject({ + id: 'baz', + }), + getSavedObject({ + id: 'qux', + }), + ], + { + overwrite: true, + workspaces: ['foo'], + } + ); + + expect(mockedClient.bulkGet).toBeCalled(); + expect(mockedClient.bulkCreate).toBeCalledWith( + [ + { attributes: {}, id: 'foo', references: [], type: 'dashboard', workspaces: ['foo'] }, + { + attributes: {}, + id: 'bar', + references: [], + type: 'dashboard', + workspaces: ['foo', 'bar'], + }, + { + attributes: {}, + id: 'qux', + references: [], + type: 'dashboard', + }, + ], + { + overwrite: true, + workspaces: ['foo'], + } + ); + expect(result).toMatchInlineSnapshot(` + Object { + "saved_objects": Array [ + Object { + "attributes": Object {}, + "error": Object { + "error": "Conflict", + "message": "Saved object [dashboard/baz] conflict", + "metadata": Object { + "isNotOverwritable": true, + }, + "statusCode": 409, + }, + "id": "baz", + "references": Array [], + "type": "dashboard", + }, + Object { + "attributes": Object {}, + "id": "foo", + "references": Array [], + "type": "dashboard", + "workspaces": Array [ + "foo", + ], + }, + Object { + "attributes": Object {}, + "id": "bar", + "references": Array [], + "type": "dashboard", + "workspaces": Array [ + "foo", + "bar", + ], + }, + ], + } + `); + }); + }); + + describe('checkConflictWithWorkspaceConflictCheck', () => { + beforeEach(() => { + mockedClient.bulkGet.mockClear(); + }); + + it(`Return early when no objects`, async () => { + const result = await wrapperClient.checkConflicts([]); + expect(result.errors).toEqual([]); + expect(mockedClient.bulkGet).not.toBeCalled(); + }); + + it(`Should filter out workspace conflict objects`, async () => { + mockedClient.checkConflicts.mockResolvedValueOnce({ + errors: [], + }); + mockedClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + getSavedObject({ + id: 'foo', + workspaces: ['foo'], + }), + getSavedObject({ + id: 'bar', + workspaces: ['foo', 'bar'], + }), + getSavedObject({ + id: 'baz', + workspaces: ['baz'], + }), + getSavedObject({ + id: 'qux', + error: { + statusCode: 404, + message: 'object not found', + error: 'object not found', + }, + }), + ], + }); + const result = await wrapperClient.checkConflicts( + [ + getSavedObject({ + id: 'foo', + }), + getSavedObject({ + id: 'bar', + }), + getSavedObject({ + id: 'baz', + }), + getSavedObject({ + id: 'qux', + }), + ], + { + workspaces: ['foo'], + } + ); + + expect(mockedClient.bulkGet).toBeCalled(); + expect(mockedClient.checkConflicts).toBeCalledWith( + [ + { attributes: {}, id: 'foo', references: [], type: 'dashboard' }, + { + attributes: {}, + id: 'bar', + references: [], + type: 'dashboard', + }, + { + attributes: {}, + id: 'qux', + references: [], + type: 'dashboard', + }, + ], + { + workspaces: ['foo'], + } + ); + expect(result).toMatchInlineSnapshot(` + Object { + "errors": Array [ + Object { + "error": Object { + "error": "Conflict", + "message": "Saved object [dashboard/baz] conflict", + "metadata": Object { + "isNotOverwritable": true, + }, + "statusCode": 409, + }, + "id": "baz", + "type": "dashboard", + }, + ], + } + `); + }); + }); +}); diff --git a/src/plugins/workspace/server/saved_objects/saved_objects_wrapper_for_check_workspace_conflict.ts b/src/plugins/workspace/server/saved_objects/saved_objects_wrapper_for_check_workspace_conflict.ts new file mode 100644 index 000000000000..f2d0eb2c732c --- /dev/null +++ b/src/plugins/workspace/server/saved_objects/saved_objects_wrapper_for_check_workspace_conflict.ts @@ -0,0 +1,310 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import Boom from '@hapi/boom'; +import { + SavedObject, + SavedObjectsBaseOptions, + SavedObjectsBulkCreateObject, + SavedObjectsBulkResponse, + SavedObjectsClientWrapperFactory, + SavedObjectsCreateOptions, + SavedObjectsErrorHelpers, + SavedObjectsUtils, + SavedObjectsSerializer, + SavedObjectsCheckConflictsObject, + SavedObjectsCheckConflictsResponse, +} from '../../../../core/server'; + +const errorContent = (error: Boom.Boom) => error.output.payload; + +const filterWorkspacesAccordingToSourceWorkspaces = ( + targetWorkspaces?: string[], + baseWorkspaces?: string[] +): string[] => targetWorkspaces?.filter((item) => !baseWorkspaces?.includes(item)) || []; + +export class WorkspaceConflictSavedObjectsClientWrapper { + private _serializer?: SavedObjectsSerializer; + public setSerializer(serializer: SavedObjectsSerializer) { + this._serializer = serializer; + } + private getRawId(props: { namespace?: string; id: string; type: string }) { + return ( + this._serializer?.generateRawId(props.namespace, props.type, props.id) || + `${props.type}:${props.id}` + ); + } + public wrapperFactory: SavedObjectsClientWrapperFactory = (wrapperOptions) => { + const createWithWorkspaceConflictCheck = async ( + type: string, + attributes: T, + options: SavedObjectsCreateOptions = {} + ) => { + const { workspaces, id, overwrite } = options; + let savedObjectWorkspaces = options?.workspaces; + + /** + * Check if overwrite with id + * If so, need to reserve the workspace params + */ + if (id && overwrite) { + let currentItem; + try { + currentItem = await wrapperOptions.client.get(type, id); + } catch (e) { + // If item can not be found, supress the error and create the object + } + if (currentItem) { + if ( + filterWorkspacesAccordingToSourceWorkspaces(workspaces, currentItem.workspaces).length + ) { + throw SavedObjectsErrorHelpers.createConflictError(type, id); + } else { + savedObjectWorkspaces = currentItem.workspaces; + } + } + } + + return await wrapperOptions.client.create(type, attributes, { + ...options, + workspaces: savedObjectWorkspaces, + }); + }; + + const bulkCreateWithWorkspaceConflictCheck = async ( + objects: Array>, + options: SavedObjectsCreateOptions = {} + ): Promise> => { + const { overwrite, namespace } = options; + /** + * When overwrite, filter out all the objects that have ids + */ + const bulkGetDocs = overwrite + ? objects + .filter((object) => !!object.id) + .map((object) => { + const { type, id } = object; + /** + * It requires a check when overwriting objects to target workspaces + */ + return { + type, + id: id as string, + fields: ['id', 'workspaces'], + }; + }) + : []; + const objectsConflictWithWorkspace: SavedObject[] = []; + const objectsMapWorkspaces: Record = {}; + if (bulkGetDocs.length) { + /** + * Get latest status of objects + */ + const bulkGetResult = await wrapperOptions.client.bulkGet(bulkGetDocs); + + bulkGetResult.saved_objects.forEach((object) => { + /** + * Skip the items with error, wrapperOptions.client will handle the error + */ + if (!object.error && object.id) { + /** + * When it is about to overwrite a object into options.workspace. + * We need to check if the options.workspaces is the subset of object.workspaces, + * Or it will be treated as a conflict + */ + const filteredWorkspaces = filterWorkspacesAccordingToSourceWorkspaces( + options.workspaces, + object.workspaces + ); + const { id, type } = object; + if (filteredWorkspaces.length) { + /** + * options.workspaces is not a subset of object.workspaces, + * Add the item into conflict array. + */ + objectsConflictWithWorkspace.push({ + id, + type, + attributes: {}, + references: [], + error: { + ...errorContent(SavedObjectsErrorHelpers.createConflictError(type, id)), + metadata: { isNotOverwritable: true }, + }, + }); + } else { + /** + * options.workspaces is a subset of object's workspaces + * Add the workspaces status into a objectId -> workspaces pairs for later use. + */ + objectsMapWorkspaces[this.getRawId({ namespace, type, id })] = object.workspaces; + } + } + }); + } + + /** + * Get all the objects that do not conflict on workspaces + */ + const objectsNoWorkspaceConflictError = objects.filter( + (item) => + !objectsConflictWithWorkspace.find( + (errorItems) => + this.getRawId({ namespace, type: errorItems.type, id: errorItems.id }) === + this.getRawId({ namespace, type: item.type, id: item.id as string }) + ) + ); + + /** + * Add the workspaces params back based on objects' workspaces value in index. + */ + const objectsPayload = objectsNoWorkspaceConflictError.map((item) => { + if (item.id) { + const workspacesParamsInIndex = + objectsMapWorkspaces[ + this.getRawId({ + namespace, + id: item.id, + type: item.type, + }) + ]; + if (workspacesParamsInIndex) { + item.workspaces = workspacesParamsInIndex; + } + } + + return item; + }); + + /** + * Bypass those objects that are not conflict on workspaces check. + */ + const realBulkCreateResult = await wrapperOptions.client.bulkCreate(objectsPayload, options); + + /** + * Merge the workspaceConflict result and real client bulkCreate result. + */ + return { + ...realBulkCreateResult, + saved_objects: [ + ...objectsConflictWithWorkspace, + ...(realBulkCreateResult?.saved_objects || []), + ], + } as SavedObjectsBulkResponse; + }; + + const checkConflictWithWorkspaceConflictCheck = async ( + objects: SavedObjectsCheckConflictsObject[] = [], + options: SavedObjectsBaseOptions = {} + ) => { + const objectsConflictWithWorkspace: SavedObjectsCheckConflictsResponse['errors'] = []; + /** + * Fail early when no objects + */ + if (objects.length === 0) { + return { errors: [] }; + } + + /** + * Workspace conflict only happens when target workspaces params present. + */ + if (options.workspaces) { + const bulkGetDocs: any[] = objects.map((object) => { + const { type, id } = object; + + return { + type, + id, + fields: ['id', 'workspaces'], + }; + }); + + if (bulkGetDocs.length) { + const bulkGetResult = await wrapperOptions.client.bulkGet(bulkGetDocs); + + bulkGetResult.saved_objects.forEach((object) => { + const { id, type } = object; + /** + * Skip the error ones, real checkConflict in repository will handle that. + */ + if (!object.error) { + let workspaceConflict = false; + const filteredWorkspaces = filterWorkspacesAccordingToSourceWorkspaces( + options.workspaces, + object.workspaces + ); + if (filteredWorkspaces.length) { + workspaceConflict = true; + } + if (workspaceConflict) { + objectsConflictWithWorkspace.push({ + id, + type, + error: { + ...errorContent(SavedObjectsErrorHelpers.createConflictError(type, id)), + metadata: { isNotOverwritable: true }, + }, + }); + } + } + }); + } + } + + const objectsNoWorkspaceConflictError = objects.filter( + (item) => + !objectsConflictWithWorkspace.find( + (errorItems) => + this.getRawId({ + namespace: options.namespace, + type: errorItems.type, + id: errorItems.id, + }) === + this.getRawId({ + namespace: options.namespace, + type: item.type, + id: item.id as string, + }) + ) + ); + + /** + * Bypass those objects that are not conflict on workspaces + */ + const realBulkCreateResult = await wrapperOptions.client.checkConflicts( + objectsNoWorkspaceConflictError, + options + ); + + /** + * Merge results from two conflict check. + */ + const result: SavedObjectsCheckConflictsResponse = { + ...realBulkCreateResult, + errors: [...objectsConflictWithWorkspace, ...realBulkCreateResult.errors], + }; + + return result; + }; + + return { + ...wrapperOptions.client, + create: createWithWorkspaceConflictCheck, + bulkCreate: bulkCreateWithWorkspaceConflictCheck, + checkConflicts: checkConflictWithWorkspaceConflictCheck, + delete: wrapperOptions.client.delete, + find: wrapperOptions.client.find, + bulkGet: wrapperOptions.client.bulkGet, + get: wrapperOptions.client.get, + update: wrapperOptions.client.update, + bulkUpdate: wrapperOptions.client.bulkUpdate, + errors: wrapperOptions.client.errors, + addToNamespaces: wrapperOptions.client.addToNamespaces, + deleteFromNamespaces: wrapperOptions.client.deleteFromNamespaces, + }; + }; + + constructor() {} +}