From 278d345c5fed4e6fcd94c94032572932d1523557 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Sat, 25 Apr 2020 14:19:15 -0400 Subject: [PATCH 01/55] Add error attribute to SavedObject.error type, update unit tests SavedObject.error type did not specify the error attribute and did not reflect what is actually returned by SavedObjectsRepository. Updated this for consistency, and updated relevant saved object unit tests to utilize the SavedObjectsErrorHelper functions instead of manually mocking error objects. --- .../inject_nested_depdendencies.test.ts | 7 ++-- .../import/extract_errors.test.ts | 12 ++---- .../import/import_saved_objects.test.ts | 18 ++++----- .../import/resolve_import_errors.test.ts | 20 ++++------ .../import/validate_references.test.ts | 40 ++++++------------- .../routes/integration_tests/import.test.ts | 15 ++++--- src/core/types/saved_objects.ts | 1 + 7 files changed, 42 insertions(+), 71 deletions(-) diff --git a/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts b/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts index a571f62e3d1c1..1d5ce5625bf48 100644 --- a/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts +++ b/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts @@ -20,6 +20,7 @@ import { SavedObject } from '../types'; import { savedObjectsClientMock } from '../../mocks'; import { getObjectReferencesToFetch, fetchNestedDependencies } from './inject_nested_depdendencies'; +import { SavedObjectsErrorHelpers } from '..'; describe('getObjectReferencesToFetch()', () => { test('works with no saved objects', () => { @@ -475,10 +476,8 @@ describe('injectNestedDependencies', () => { { id: '1', type: 'index-pattern', - error: { - statusCode: 404, - message: 'Not found', - }, + error: SavedObjectsErrorHelpers.createGenericNotFoundError('index-pattern', '1').output + .payload, attributes: {}, references: [], }, diff --git a/src/core/server/saved_objects/import/extract_errors.test.ts b/src/core/server/saved_objects/import/extract_errors.test.ts index f97cc661c0bca..a20d14e6809e2 100644 --- a/src/core/server/saved_objects/import/extract_errors.test.ts +++ b/src/core/server/saved_objects/import/extract_errors.test.ts @@ -19,6 +19,7 @@ import { SavedObject } from '../types'; import { extractErrors } from './extract_errors'; +import { SavedObjectsErrorHelpers } from '..'; describe('extractErrors()', () => { test('returns empty array when no errors exist', () => { @@ -44,10 +45,7 @@ describe('extractErrors()', () => { title: 'My Dashboard 2', }, references: [], - error: { - statusCode: 409, - message: 'Conflict', - }, + error: SavedObjectsErrorHelpers.createConflictError('dashboard', '2').output.payload, }, { id: '3', @@ -56,10 +54,7 @@ describe('extractErrors()', () => { title: 'My Dashboard 3', }, references: [], - error: { - statusCode: 400, - message: 'Bad Request', - }, + error: SavedObjectsErrorHelpers.createBadRequestError().output.payload, }, ]; const result = extractErrors(savedObjects, savedObjects); @@ -75,6 +70,7 @@ Array [ }, Object { "error": Object { + "error": "Bad Request", "message": "Bad Request", "statusCode": 400, "type": "unknown", 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 e204cd7bddfc7..dd84f59cdf464 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 @@ -21,6 +21,7 @@ import { Readable } from 'stream'; import { SavedObject } from '../types'; import { importSavedObjectsFromStream } from './import_saved_objects'; import { savedObjectsClientMock } from '../../mocks'; +import { SavedObjectsErrorHelpers } from '..'; const emptyResponse = { saved_objects: [], @@ -351,13 +352,10 @@ describe('importSavedObjects()', () => { }); savedObjectsClient.find.mockResolvedValueOnce(emptyResponse); savedObjectsClient.bulkCreate.mockResolvedValue({ - saved_objects: savedObjects.map((savedObject) => ({ - type: savedObject.type, - id: savedObject.id, - error: { - statusCode: 409, - message: 'conflict', - }, + saved_objects: savedObjects.map(({ type, id }) => ({ + type, + id, + error: SavedObjectsErrorHelpers.createConflictError(type, id).output.payload, attributes: {}, references: [], })), @@ -451,10 +449,8 @@ describe('importSavedObjects()', () => { { type: 'index-pattern', id: '2', - error: { - statusCode: 404, - message: 'Not found', - }, + error: SavedObjectsErrorHelpers.createGenericNotFoundError('index-pattern', '2').output + .payload, attributes: {}, references: [], }, 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 54ebecc7dca70..d40df6c9eef00 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 @@ -21,6 +21,7 @@ import { Readable } from 'stream'; import { SavedObject } from '../types'; import { resolveSavedObjectsImportErrors } from './resolve_import_errors'; import { savedObjectsClientMock } from '../../mocks'; +import { SavedObjectsErrorHelpers } from '..'; describe('resolveImportErrors()', () => { const savedObjects: SavedObject[] = [ @@ -219,7 +220,7 @@ describe('resolveImportErrors()', () => { `); }); - test('works wtih replaceReferences', async () => { + test('works with replaceReferences', async () => { const readStream = new Readable({ objectMode: true, read() { @@ -301,13 +302,10 @@ describe('resolveImportErrors()', () => { }, }); savedObjectsClient.bulkCreate.mockResolvedValue({ - saved_objects: savedObjects.map((savedObject) => ({ - type: savedObject.type, - id: savedObject.id, - error: { - statusCode: 409, - message: 'conflict', - }, + saved_objects: savedObjects.map(({ type, id }) => ({ + type, + id, + error: SavedObjectsErrorHelpers.createConflictError(type, id).output.payload, attributes: {}, references: [], })), @@ -406,10 +404,8 @@ describe('resolveImportErrors()', () => { { type: 'index-pattern', id: '2', - error: { - statusCode: 404, - message: 'Not found', - }, + error: SavedObjectsErrorHelpers.createGenericNotFoundError('index-pattern', '2').output + .payload, attributes: {}, references: [], }, diff --git a/src/core/server/saved_objects/import/validate_references.test.ts b/src/core/server/saved_objects/import/validate_references.test.ts index 6642cf149eda9..59b0eb33079c7 100644 --- a/src/core/server/saved_objects/import/validate_references.test.ts +++ b/src/core/server/saved_objects/import/validate_references.test.ts @@ -19,6 +19,7 @@ import { getNonExistingReferenceAsKeys, validateReferences } from './validate_references'; import { savedObjectsClientMock } from '../../mocks'; +import { SavedObjectsErrorHelpers } from '..'; describe('getNonExistingReferenceAsKeys()', () => { const savedObjectsClient = savedObjectsClientMock.create(); @@ -164,20 +165,15 @@ describe('getNonExistingReferenceAsKeys()', () => { { id: '1', type: 'index-pattern', - error: { - statusCode: 404, - message: 'Not found', - }, + error: SavedObjectsErrorHelpers.createGenericNotFoundError('index-pattern', '1').output + .payload, attributes: {}, references: [], }, { id: '3', type: 'search', - error: { - statusCode: 404, - message: 'Not found', - }, + error: SavedObjectsErrorHelpers.createGenericNotFoundError('search', '3').output.payload, attributes: {}, references: [], }, @@ -245,40 +241,31 @@ Object { { type: 'index-pattern', id: '3', - error: { - statusCode: 404, - message: 'Not found', - }, + error: SavedObjectsErrorHelpers.createGenericNotFoundError('index-pattern', '3').output + .payload, attributes: {}, references: [], }, { type: 'index-pattern', id: '5', - error: { - statusCode: 404, - message: 'Not found', - }, + error: SavedObjectsErrorHelpers.createGenericNotFoundError('index-pattern', '5').output + .payload, attributes: {}, references: [], }, { type: 'index-pattern', id: '6', - error: { - statusCode: 404, - message: 'Not found', - }, + error: SavedObjectsErrorHelpers.createGenericNotFoundError('index-pattern', '6').output + .payload, attributes: {}, references: [], }, { type: 'search', id: '7', - error: { - statusCode: 404, - message: 'Not found', - }, + error: SavedObjectsErrorHelpers.createGenericNotFoundError('search', '7').output.payload, attributes: {}, references: [], }, @@ -602,10 +589,7 @@ Object { { id: '1', type: 'index-pattern', - error: { - statusCode: 400, - message: 'Error', - }, + error: SavedObjectsErrorHelpers.createBadRequestError().output.payload, attributes: {}, references: [], }, diff --git a/src/core/server/saved_objects/routes/integration_tests/import.test.ts b/src/core/server/saved_objects/routes/integration_tests/import.test.ts index c4a03a0e2e7d2..5ab7baba90586 100644 --- a/src/core/server/saved_objects/routes/integration_tests/import.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/import.test.ts @@ -23,6 +23,7 @@ import { registerImportRoute } from '../import'; import { savedObjectsClientMock } from '../../../../../core/server/mocks'; import { SavedObjectConfig } from '../../saved_objects_config'; import { setupServer, createExportableType } from './test_utils'; +import { SavedObjectsErrorHelpers } from '../..'; type setupServerReturn = UnwrapPromise>; @@ -185,10 +186,8 @@ describe('POST /internal/saved_objects/_import', () => { id: 'my-pattern', attributes: {}, references: [], - error: { - statusCode: 409, - message: 'Saved object [index-pattern/my-pattern] conflict', - }, + error: SavedObjectsErrorHelpers.createConflictError('index-pattern', 'my-pattern').output + .payload, }, { type: 'dashboard', @@ -241,10 +240,10 @@ describe('POST /internal/saved_objects/_import', () => { { id: 'my-pattern-*', type: 'index-pattern', - error: { - statusCode: 404, - message: 'Not found', - }, + error: SavedObjectsErrorHelpers.createGenericNotFoundError( + 'index-pattern', + 'my-pattern-*' + ).output.payload, references: [], attributes: {}, }, diff --git a/src/core/types/saved_objects.ts b/src/core/types/saved_objects.ts index 04aaacc3cf31a..d9cd7baa9cff8 100644 --- a/src/core/types/saved_objects.ts +++ b/src/core/types/saved_objects.ts @@ -87,6 +87,7 @@ export interface SavedObject { /** Timestamp of the last time this document had been updated. */ updated_at?: string; error?: { + error: string; message: string; statusCode: number; }; From 9ee94b2f966bace0161b1fb070a6c214bb14264d Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Sat, 25 Apr 2020 14:20:49 -0400 Subject: [PATCH 02/55] Modify saved object import function contracts to use type registry `importSavedObjectsFromStream` and `resolveSavedObjectImportErrors` previously expected an array of supported saved object types. Updated these functions to use the SavedObjectTypeRegistry directly to obtain the list of importable/exportable types. This simplifies logic for other consumers (e.g., Spaces), and it lays the groundwork for regenerating IDs of multi-namespace saved objects upon import. --- .../import/import_saved_objects.test.ts | 29 ++++-- .../import/import_saved_objects.ts | 3 +- .../import/resolve_import_errors.test.ts | 30 ++++-- .../import/resolve_import_errors.ts | 3 +- src/core/server/saved_objects/import/types.ts | 9 +- .../server/saved_objects/routes/import.ts | 6 +- .../routes/resolve_import_errors.ts | 6 +- .../lib/copy_to_spaces/copy_to_spaces.test.ts | 62 +++++------- .../lib/copy_to_spaces/copy_to_spaces.ts | 5 +- .../resolve_copy_conflicts.test.ts | 62 +++++------- .../copy_to_spaces/resolve_copy_conflicts.ts | 5 +- .../__fixtures__/create_mock_so_service.ts | 35 ------- .../routes/api/external/copy_to_space.test.ts | 96 +------------------ 13 files changed, 109 insertions(+), 242 deletions(-) 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 dd84f59cdf464..eb3c4a98e2386 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 @@ -22,6 +22,7 @@ import { SavedObject } from '../types'; import { importSavedObjectsFromStream } from './import_saved_objects'; import { savedObjectsClientMock } from '../../mocks'; import { SavedObjectsErrorHelpers } from '..'; +import { typeRegistryMock } from '../saved_objects_type_registry.mock'; const emptyResponse = { saved_objects: [], @@ -29,6 +30,19 @@ const emptyResponse = { per_page: 0, page: 0, }; + +const createTypeRegistryMock = (supportedTypes: string[]) => { + const typeRegistry = typeRegistryMock.create(); + const types = supportedTypes.map((name) => ({ + name, + hidden: false, + namespaceType: 'single' as 'single', + mappings: { properties: {} }, + })); + typeRegistry.getImportableAndExportableTypes.mockReturnValue(types); + return typeRegistry; +}; + describe('importSavedObjects()', () => { const savedObjects: SavedObject[] = [ { @@ -65,6 +79,7 @@ describe('importSavedObjects()', () => { }, ]; const savedObjectsClient = savedObjectsClientMock.create(); + const supportedTypes = ['index-pattern', 'search', 'visualization', 'dashboard']; beforeEach(() => { jest.resetAllMocks(); @@ -82,7 +97,7 @@ describe('importSavedObjects()', () => { objectLimit: 1, overwrite: false, savedObjectsClient, - supportedTypes: [], + typeRegistry: createTypeRegistryMock([]), }); expect(result).toMatchInlineSnapshot(` Object { @@ -109,7 +124,7 @@ describe('importSavedObjects()', () => { objectLimit: 4, overwrite: false, savedObjectsClient, - supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'], + typeRegistry: createTypeRegistryMock(supportedTypes), }); expect(result).toMatchInlineSnapshot(` Object { @@ -192,7 +207,7 @@ describe('importSavedObjects()', () => { objectLimit: 4, overwrite: false, savedObjectsClient, - supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'], + typeRegistry: createTypeRegistryMock(supportedTypes), namespace: 'foo', }); expect(result).toMatchInlineSnapshot(` @@ -276,7 +291,7 @@ describe('importSavedObjects()', () => { objectLimit: 4, overwrite: true, savedObjectsClient, - supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'], + typeRegistry: createTypeRegistryMock(supportedTypes), }); expect(result).toMatchInlineSnapshot(` Object { @@ -365,7 +380,7 @@ describe('importSavedObjects()', () => { objectLimit: 4, overwrite: false, savedObjectsClient, - supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'], + typeRegistry: createTypeRegistryMock(supportedTypes), }); expect(result).toMatchInlineSnapshot(` Object { @@ -461,7 +476,7 @@ describe('importSavedObjects()', () => { objectLimit: 4, overwrite: false, savedObjectsClient, - supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'], + typeRegistry: createTypeRegistryMock(supportedTypes), }); expect(result).toMatchInlineSnapshot(` Object { @@ -537,7 +552,7 @@ describe('importSavedObjects()', () => { objectLimit: 5, overwrite: false, savedObjectsClient, - supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'], + typeRegistry: createTypeRegistryMock(supportedTypes), }); expect(result).toMatchInlineSnapshot(` Object { 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 6065e03fb1628..d274ece506759 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.ts @@ -37,10 +37,11 @@ export async function importSavedObjectsFromStream({ objectLimit, overwrite, savedObjectsClient, - supportedTypes, + typeRegistry, namespace, }: SavedObjectsImportOptions): Promise { let errorAccumulator: SavedObjectsImportError[] = []; + const supportedTypes = typeRegistry.getImportableAndExportableTypes().map((type) => type.name); // Get the objects to import const { 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 d40df6c9eef00..6c29d8399d87c 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 @@ -22,6 +22,19 @@ import { SavedObject } from '../types'; import { resolveSavedObjectsImportErrors } from './resolve_import_errors'; import { savedObjectsClientMock } from '../../mocks'; import { SavedObjectsErrorHelpers } from '..'; +import { typeRegistryMock } from '../saved_objects_type_registry.mock'; + +const createTypeRegistryMock = (supportedTypes: string[]) => { + const typeRegistry = typeRegistryMock.create(); + const types = supportedTypes.map((name) => ({ + name, + hidden: false, + namespaceType: 'single' as 'single', + mappings: { properties: {} }, + })); + typeRegistry.getImportableAndExportableTypes.mockReturnValue(types); + return typeRegistry; +}; describe('resolveImportErrors()', () => { const savedObjects: SavedObject[] = [ @@ -65,6 +78,7 @@ describe('resolveImportErrors()', () => { }, ]; const savedObjectsClient = savedObjectsClientMock.create(); + const supportedTypes = ['index-pattern', 'search', 'visualization', 'dashboard']; beforeEach(() => { jest.resetAllMocks(); @@ -86,7 +100,7 @@ describe('resolveImportErrors()', () => { objectLimit: 4, retries: [], savedObjectsClient, - supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'], + typeRegistry: createTypeRegistryMock(supportedTypes), }); expect(result).toMatchInlineSnapshot(` Object { @@ -120,7 +134,7 @@ describe('resolveImportErrors()', () => { }, ], savedObjectsClient, - supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'], + typeRegistry: createTypeRegistryMock(supportedTypes), }); expect(result).toMatchInlineSnapshot(` Object { @@ -181,7 +195,7 @@ describe('resolveImportErrors()', () => { }, ], savedObjectsClient, - supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'], + typeRegistry: createTypeRegistryMock(supportedTypes), }); expect(result).toMatchInlineSnapshot(` Object { @@ -249,7 +263,7 @@ describe('resolveImportErrors()', () => { }, ], savedObjectsClient, - supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'], + typeRegistry: createTypeRegistryMock(supportedTypes), }); expect(result).toMatchInlineSnapshot(` Object { @@ -320,7 +334,7 @@ describe('resolveImportErrors()', () => { replaceReferences: [], })), savedObjectsClient, - supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'], + typeRegistry: createTypeRegistryMock(supportedTypes), }); expect(result).toMatchInlineSnapshot(` Object { @@ -429,7 +443,7 @@ describe('resolveImportErrors()', () => { }, ], savedObjectsClient, - supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'], + typeRegistry: createTypeRegistryMock(supportedTypes), }); expect(result).toMatchInlineSnapshot(` Object { @@ -511,7 +525,7 @@ describe('resolveImportErrors()', () => { }, ], savedObjectsClient, - supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'], + typeRegistry: createTypeRegistryMock(supportedTypes), }); expect(result).toMatchInlineSnapshot(` Object { @@ -555,7 +569,7 @@ describe('resolveImportErrors()', () => { }, ], savedObjectsClient, - supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'], + typeRegistry: createTypeRegistryMock(supportedTypes), namespace: 'foo', }); expect(result).toMatchInlineSnapshot(` 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 a5175aa080598..47e5e5cd8ebf5 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.ts @@ -38,11 +38,12 @@ export async function resolveSavedObjectsImportErrors({ objectLimit, retries, savedObjectsClient, - supportedTypes, + typeRegistry, namespace, }: SavedObjectsResolveImportErrorsOptions): Promise { let successCount = 0; let errorAccumulator: SavedObjectsImportError[] = []; + const supportedTypes = typeRegistry.getImportableAndExportableTypes().map((type) => type.name); const filter = createObjectsFilter(retries); // Get the objects to resolve errors diff --git a/src/core/server/saved_objects/import/types.ts b/src/core/server/saved_objects/import/types.ts index 067579f54edac..f8cfa8c5ac015 100644 --- a/src/core/server/saved_objects/import/types.ts +++ b/src/core/server/saved_objects/import/types.ts @@ -19,6 +19,7 @@ import { Readable } from 'stream'; import { SavedObjectsClientContract } from '../types'; +import { ISavedObjectTypeRegistry } from '..'; /** * Describes a retry operation for importing a saved object. @@ -115,8 +116,8 @@ export interface SavedObjectsImportOptions { overwrite: boolean; /** {@link SavedObjectsClientContract | client} to use to perform the import operation */ savedObjectsClient: SavedObjectsClientContract; - /** the list of allowed types to import */ - supportedTypes: string[]; + /** The registry of all known saved object types */ + typeRegistry: ISavedObjectTypeRegistry; /** if specified, will import in given namespace, else will import as global object */ namespace?: string; } @@ -132,10 +133,10 @@ export interface SavedObjectsResolveImportErrorsOptions { objectLimit: number; /** client to use to perform the import operation */ savedObjectsClient: SavedObjectsClientContract; + /** The registry of all known saved object types */ + typeRegistry: ISavedObjectTypeRegistry; /** saved object import references to retry */ retries: SavedObjectsImportRetry[]; - /** the list of allowed types to import */ - supportedTypes: string[]; /** if specified, will import in given namespace */ namespace?: string; } diff --git a/src/core/server/saved_objects/routes/import.ts b/src/core/server/saved_objects/routes/import.ts index 8fce6f49fb850..6f15ca505558e 100644 --- a/src/core/server/saved_objects/routes/import.ts +++ b/src/core/server/saved_objects/routes/import.ts @@ -61,13 +61,9 @@ export const registerImportRoute = (router: IRouter, config: SavedObjectConfig) return res.badRequest({ body: `Invalid file extension ${fileExtension}` }); } - const supportedTypes = context.core.savedObjects.typeRegistry - .getImportableAndExportableTypes() - .map((type) => type.name); - const result = await importSavedObjectsFromStream({ - supportedTypes, savedObjectsClient: context.core.savedObjects.client, + typeRegistry: context.core.savedObjects.typeRegistry, readStream: createSavedObjectsStreamFromNdJson(file), objectLimit: maxImportExportSize, overwrite, diff --git a/src/core/server/saved_objects/routes/resolve_import_errors.ts b/src/core/server/saved_objects/routes/resolve_import_errors.ts index 3458e601e0fe6..361b6ec862a8a 100644 --- a/src/core/server/saved_objects/routes/resolve_import_errors.ts +++ b/src/core/server/saved_objects/routes/resolve_import_errors.ts @@ -72,12 +72,8 @@ export const registerResolveImportErrorsRoute = (router: IRouter, config: SavedO return res.badRequest({ body: `Invalid file extension ${fileExtension}` }); } - const supportedTypes = context.core.savedObjects.typeRegistry - .getImportableAndExportableTypes() - .map((type) => type.name); - const result = await resolveSavedObjectsImportErrors({ - supportedTypes, + typeRegistry: context.core.savedObjects.typeRegistry, savedObjectsClient: context.core.savedObjects.client, readStream: createSavedObjectsStreamFromNdJson(file), retries: req.body.retries, diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts index 9679dd8c52523..7c45c4596b09d 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts @@ -10,7 +10,7 @@ import { } from 'src/core/server'; import { copySavedObjectsToSpacesFactory } from './copy_to_spaces'; import { Readable } from 'stream'; -import { coreMock, savedObjectsTypeRegistryMock, httpServerMock } from 'src/core/server/mocks'; +import { coreMock, httpServerMock } from 'src/core/server/mocks'; jest.mock('../../../../../../src/core/server', () => { return { @@ -53,34 +53,6 @@ describe('copySavedObjectsToSpaces', () => { const setup = (setupOpts: SetupOpts) => { const coreStart = coreMock.createStart(); - const typeRegistry = savedObjectsTypeRegistryMock.create(); - typeRegistry.getAllTypes.mockReturnValue([ - { - name: 'dashboard', - namespaceType: 'single', - hidden: false, - mappings: { properties: {} }, - }, - { - name: 'visualization', - namespaceType: 'single', - hidden: false, - mappings: { properties: {} }, - }, - { - name: 'globaltype', - namespaceType: 'agnostic', - hidden: false, - mappings: { properties: {} }, - }, - ]); - - typeRegistry.isNamespaceAgnostic.mockImplementation((type: string) => - typeRegistry.getAllTypes().some((t) => t.name === type && t.namespaceType === 'agnostic') - ); - - coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); - (exportSavedObjectsToStream as jest.Mock).mockImplementation( async (opts: SavedObjectsExportOptions) => { return ( @@ -266,10 +238,18 @@ describe('copySavedObjectsToSpaces', () => { "get": [MockFunction], "update": [MockFunction], }, - "supportedTypes": Array [ - "dashboard", - "visualization", - ], + "typeRegistry": Object { + "getAllTypes": [MockFunction], + "getImportableAndExportableTypes": [MockFunction], + "getIndex": [MockFunction], + "getType": [MockFunction], + "isHidden": [MockFunction], + "isImportableAndExportable": [MockFunction], + "isMultiNamespace": [MockFunction], + "isNamespaceAgnostic": [MockFunction], + "isSingleNamespace": [MockFunction], + "registerType": [MockFunction], + }, }, ], Array [ @@ -331,10 +311,18 @@ describe('copySavedObjectsToSpaces', () => { "get": [MockFunction], "update": [MockFunction], }, - "supportedTypes": Array [ - "dashboard", - "visualization", - ], + "typeRegistry": Object { + "getAllTypes": [MockFunction], + "getImportableAndExportableTypes": [MockFunction], + "getIndex": [MockFunction], + "getType": [MockFunction], + "isHidden": [MockFunction], + "isImportableAndExportable": [MockFunction], + "isMultiNamespace": [MockFunction], + "isNamespaceAgnostic": [MockFunction], + "isSingleNamespace": [MockFunction], + "registerType": [MockFunction], + }, }, ], ] diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts index dca6f2a6206ab..cd1d119cd0623 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts @@ -12,7 +12,6 @@ import { } from '../../../../../../src/core/server'; import { spaceIdToNamespace } from '../utils/namespace'; import { CopyOptions, CopyResponse } from './types'; -import { getEligibleTypes } from './lib/get_eligible_types'; import { createReadableStreamFromArray } from './lib/readable_stream_from_array'; import { createEmptyFailureResponse } from './lib/create_empty_failure_response'; import { readStreamToCompletion } from './lib/read_stream_to_completion'; @@ -27,8 +26,6 @@ export function copySavedObjectsToSpacesFactory( const savedObjectsClient = getScopedClient(request, COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS); - const eligibleTypes = getEligibleTypes(getTypeRegistry()); - const exportRequestedObjects = async ( sourceSpaceId: string, options: Pick @@ -56,7 +53,7 @@ export function copySavedObjectsToSpacesFactory( objectLimit: getImportExportObjectLimit(), overwrite: options.overwrite, savedObjectsClient, - supportedTypes: eligibleTypes, + typeRegistry: getTypeRegistry(), readStream: objectsStream, }); diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts index 7bb4c61ed51a0..e4a30e79436ac 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts @@ -8,7 +8,7 @@ import { SavedObjectsResolveImportErrorsOptions, SavedObjectsExportOptions, } from 'src/core/server'; -import { coreMock, savedObjectsTypeRegistryMock, httpServerMock } from 'src/core/server/mocks'; +import { coreMock, httpServerMock } from 'src/core/server/mocks'; import { Readable } from 'stream'; import { resolveCopySavedObjectsToSpacesConflictsFactory } from './resolve_copy_conflicts'; @@ -53,34 +53,6 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { const setup = (setupOpts: SetupOpts) => { const coreStart = coreMock.createStart(); - const typeRegistry = savedObjectsTypeRegistryMock.create(); - typeRegistry.getAllTypes.mockReturnValue([ - { - name: 'dashboard', - namespaceType: 'single', - hidden: false, - mappings: { properties: {} }, - }, - { - name: 'visualization', - namespaceType: 'single', - hidden: false, - mappings: { properties: {} }, - }, - { - name: 'globaltype', - namespaceType: 'agnostic', - hidden: false, - mappings: { properties: {} }, - }, - ]); - - typeRegistry.isNamespaceAgnostic.mockImplementation((type: string) => - typeRegistry.getAllTypes().some((t) => t.name === type && t.namespaceType === 'agnostic') - ); - - coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); - (exportSavedObjectsToStream as jest.Mock).mockImplementation( async (opts: SavedObjectsExportOptions) => { return ( @@ -289,10 +261,18 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { "get": [MockFunction], "update": [MockFunction], }, - "supportedTypes": Array [ - "dashboard", - "visualization", - ], + "typeRegistry": Object { + "getAllTypes": [MockFunction], + "getImportableAndExportableTypes": [MockFunction], + "getIndex": [MockFunction], + "getType": [MockFunction], + "isHidden": [MockFunction], + "isImportableAndExportable": [MockFunction], + "isMultiNamespace": [MockFunction], + "isNamespaceAgnostic": [MockFunction], + "isSingleNamespace": [MockFunction], + "registerType": [MockFunction], + }, }, ], Array [ @@ -361,10 +341,18 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { "get": [MockFunction], "update": [MockFunction], }, - "supportedTypes": Array [ - "dashboard", - "visualization", - ], + "typeRegistry": Object { + "getAllTypes": [MockFunction], + "getImportableAndExportableTypes": [MockFunction], + "getIndex": [MockFunction], + "getType": [MockFunction], + "isHidden": [MockFunction], + "isImportableAndExportable": [MockFunction], + "isMultiNamespace": [MockFunction], + "isNamespaceAgnostic": [MockFunction], + "isSingleNamespace": [MockFunction], + "registerType": [MockFunction], + }, }, ], ] diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts index a355d19b305a3..33b0f7cabd594 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts @@ -12,7 +12,6 @@ import { } from '../../../../../../src/core/server'; import { spaceIdToNamespace } from '../utils/namespace'; import { CopyOptions, ResolveConflictsOptions, CopyResponse } from './types'; -import { getEligibleTypes } from './lib/get_eligible_types'; import { createEmptyFailureResponse } from './lib/create_empty_failure_response'; import { readStreamToCompletion } from './lib/read_stream_to_completion'; import { createReadableStreamFromArray } from './lib/readable_stream_from_array'; @@ -27,8 +26,6 @@ export function resolveCopySavedObjectsToSpacesConflictsFactory( const savedObjectsClient = getScopedClient(request, COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS); - const eligibleTypes = getEligibleTypes(getTypeRegistry()); - const exportRequestedObjects = async ( sourceSpaceId: string, options: Pick @@ -59,7 +56,7 @@ export function resolveCopySavedObjectsToSpacesConflictsFactory( namespace: spaceIdToNamespace(spaceId), objectLimit: getImportExportObjectLimit(), savedObjectsClient, - supportedTypes: eligibleTypes, + typeRegistry: getTypeRegistry(), readStream: objectsStream, retries, }); diff --git a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_mock_so_service.ts b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_mock_so_service.ts index 034d212a33035..ce93591f492f1 100644 --- a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_mock_so_service.ts +++ b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_mock_so_service.ts @@ -43,41 +43,6 @@ export const createMockSavedObjectsService = (spaces: any[] = []) => { const { savedObjects } = coreMock.createStart(); const typeRegistry = savedObjectsTypeRegistryMock.create(); - typeRegistry.getAllTypes.mockReturnValue([ - { - name: 'visualization', - namespaceType: 'single', - hidden: false, - mappings: { properties: {} }, - }, - { - name: 'dashboard', - namespaceType: 'single', - hidden: false, - mappings: { properties: {} }, - }, - { - name: 'index-pattern', - namespaceType: 'single', - hidden: false, - mappings: { properties: {} }, - }, - { - name: 'globalType', - namespaceType: 'agnostic', - hidden: false, - mappings: { properties: {} }, - }, - { - name: 'space', - namespaceType: 'agnostic', - hidden: true, - mappings: { properties: {} }, - }, - ]); - typeRegistry.isNamespaceAgnostic.mockImplementation((type: string) => - typeRegistry.getAllTypes().some((t) => t.name === type && t.namespaceType === 'agnostic') - ); savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); savedObjects.getScopedClient.mockReturnValue(mockSavedObjectsClientContract); diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts index 632e64156291c..f7879b61df123 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts @@ -207,40 +207,6 @@ describe('copy to space', () => { ).toThrowErrorMatchingInlineSnapshot(`"[objects]: duplicate objects are not allowed"`); }); - it('does not allow namespace agnostic types to be copied (via "supportedTypes" property)', async () => { - const payload = { - spaces: ['a-space'], - objects: [ - { type: 'globalType', id: 'bar' }, - { type: 'visualization', id: 'bar' }, - ], - }; - - const { copyToSpace } = await setup(); - - const request = httpServerMock.createKibanaRequest({ - body: payload, - method: 'post', - }); - - const response = await copyToSpace.routeHandler( - mockRouteContext, - request, - kibanaResponseFactory - ); - - const { status } = response; - - expect(status).toEqual(200); - expect(importSavedObjectsFromStream).toHaveBeenCalledTimes(1); - const [importCallOptions] = (importSavedObjectsFromStream as jest.Mock).mock.calls[0]; - - expect(importCallOptions).toMatchObject({ - namespace: 'a-space', - supportedTypes: ['visualization', 'dashboard', 'index-pattern'], - }); - }); - it('copies to multiple spaces', async () => { const payload = { spaces: ['a-space', 'b-space'], @@ -365,58 +331,6 @@ describe('copy to space', () => { ); }); - it('does not allow namespace agnostic types to be copied (via "supportedTypes" property)', async () => { - const payload = { - retries: { - ['a-space']: [ - { - type: 'visualization', - id: 'bar', - overwrite: true, - }, - { - type: 'globalType', - id: 'bar', - overwrite: true, - }, - ], - }, - objects: [ - { - type: 'globalType', - id: 'bar', - }, - { type: 'visualization', id: 'bar' }, - ], - }; - - const { resolveConflicts } = await setup(); - - const request = httpServerMock.createKibanaRequest({ - body: payload, - method: 'post', - }); - - const response = await resolveConflicts.routeHandler( - mockRouteContext, - request, - kibanaResponseFactory - ); - - const { status } = response; - - expect(status).toEqual(200); - expect(resolveSavedObjectsImportErrors).toHaveBeenCalledTimes(1); - const [ - resolveImportErrorsCallOptions, - ] = (resolveSavedObjectsImportErrors as jest.Mock).mock.calls[0]; - - expect(resolveImportErrorsCallOptions).toMatchObject({ - namespace: 'a-space', - supportedTypes: ['visualization', 'dashboard', 'index-pattern'], - }); - }); - it('resolves conflicts for multiple spaces', async () => { const payload = { objects: [{ type: 'visualization', id: 'bar' }], @@ -459,19 +373,13 @@ describe('copy to space', () => { resolveImportErrorsFirstCallOptions, ] = (resolveSavedObjectsImportErrors as jest.Mock).mock.calls[0]; - expect(resolveImportErrorsFirstCallOptions).toMatchObject({ - namespace: 'a-space', - supportedTypes: ['visualization', 'dashboard', 'index-pattern'], - }); + expect(resolveImportErrorsFirstCallOptions).toMatchObject({ namespace: 'a-space' }); const [ resolveImportErrorsSecondCallOptions, ] = (resolveSavedObjectsImportErrors as jest.Mock).mock.calls[1]; - expect(resolveImportErrorsSecondCallOptions).toMatchObject({ - namespace: 'b-space', - supportedTypes: ['visualization', 'dashboard', 'index-pattern'], - }); + expect(resolveImportErrorsSecondCallOptions).toMatchObject({ namespace: 'b-space' }); }); }); }); From 8c4ec1635160b10991498748a37695b6498e8b84 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Sat, 25 Apr 2020 14:22:02 -0400 Subject: [PATCH 03/55] Add optional `originId` field to saved objects This is only intended to be set during migration or create operations. It is in place to support future changes for migrating and importing multi-namespace saved objects, so IDs can be regenerated deterministically. This will allow saved objects to be overwritten consistently if they are imported multiple times into a given space. --- .../serialization/serializer.test.ts | 43 ++++++ .../saved_objects/serialization/serializer.ts | 5 +- .../saved_objects/serialization/types.ts | 2 + .../service/lib/included_fields.test.ts | 30 +++-- .../service/lib/included_fields.ts | 1 + .../service/lib/repository.test.js | 124 ++++++++++++++---- .../saved_objects/service/lib/repository.ts | 39 ++++-- .../service/saved_objects_client.ts | 4 + src/core/types/saved_objects.ts | 7 + .../services/epm/packages/get_objects.ts | 6 +- 10 files changed, 213 insertions(+), 48 deletions(-) diff --git a/src/core/server/saved_objects/serialization/serializer.test.ts b/src/core/server/saved_objects/serialization/serializer.test.ts index 1a7dfdd2d130e..e5f0e8abd3b71 100644 --- a/src/core/server/saved_objects/serialization/serializer.test.ts +++ b/src/core/server/saved_objects/serialization/serializer.test.ts @@ -214,6 +214,28 @@ describe('#rawToSavedObject', () => { expect(actual).not.toHaveProperty('updated_at'); }); + test('if specified it copies the _source.originId property to originId', () => { + const originId = 'baz'; + const actual = singleNamespaceSerializer.rawToSavedObject({ + _id: 'foo:bar', + _source: { + type: 'foo', + originId, + }, + }); + expect(actual).toHaveProperty('originId', originId); + }); + + test(`if _source.originId is unspecified it doesn't set originId`, () => { + const actual = singleNamespaceSerializer.rawToSavedObject({ + _id: 'foo:bar', + _source: { + type: 'foo', + }, + }); + expect(actual).not.toHaveProperty('originId'); + }); + test('it does not pass unknown properties through', () => { const actual = singleNamespaceSerializer.rawToSavedObject({ _id: 'universe', @@ -280,6 +302,7 @@ describe('#rawToSavedObject', () => { namespace: 'foo-namespace', updated_at: String(new Date()), references: [], + originId: 'baz', }, }; @@ -458,6 +481,26 @@ describe('#savedObjectToRaw', () => { expect(actual._source).not.toHaveProperty('updated_at'); }); + test('if specified it copies the originId property to _source.originId', () => { + const originId = 'baz'; + const actual = singleNamespaceSerializer.savedObjectToRaw({ + type: '', + attributes: {}, + originId, + } as any); + + expect(actual._source).toHaveProperty('originId', originId); + }); + + test(`if unspecified it doesn't add originId property to _source`, () => { + const actual = singleNamespaceSerializer.savedObjectToRaw({ + type: '', + attributes: {}, + } as any); + + expect(actual._source).not.toHaveProperty('originId'); + }); + test('it copies the migrationVersion property to _source.migrationVersion', () => { const actual = singleNamespaceSerializer.savedObjectToRaw({ type: '', diff --git a/src/core/server/saved_objects/serialization/serializer.ts b/src/core/server/saved_objects/serialization/serializer.ts index 3b19d494d8ecf..11bfba6e3bfb4 100644 --- a/src/core/server/saved_objects/serialization/serializer.ts +++ b/src/core/server/saved_objects/serialization/serializer.ts @@ -64,7 +64,7 @@ export class SavedObjectsSerializer { */ public rawToSavedObject(doc: SavedObjectsRawDoc): SavedObjectSanitizedDoc { const { _id, _source, _seq_no, _primary_term } = doc; - const { type, namespace, namespaces } = _source; + const { type, namespace, namespaces, originId } = _source; const version = _seq_no != null || _primary_term != null @@ -76,6 +76,7 @@ export class SavedObjectsSerializer { id: this.trimIdPrefix(namespace, type, _id), ...(namespace && this.registry.isSingleNamespace(type) && { namespace }), ...(namespaces && this.registry.isMultiNamespace(type) && { namespaces }), + ...(originId && { originId }), attributes: _source[type], references: _source.references || [], ...(_source.migrationVersion && { migrationVersion: _source.migrationVersion }), @@ -95,6 +96,7 @@ export class SavedObjectsSerializer { type, namespace, namespaces, + originId, attributes, migrationVersion, updated_at, @@ -107,6 +109,7 @@ export class SavedObjectsSerializer { references, ...(namespace && this.registry.isSingleNamespace(type) && { namespace }), ...(namespaces && this.registry.isMultiNamespace(type) && { namespaces }), + ...(originId && { originId }), ...(migrationVersion && { migrationVersion }), ...(updated_at && { updated_at }), }; diff --git a/src/core/server/saved_objects/serialization/types.ts b/src/core/server/saved_objects/serialization/types.ts index acd2c7b5284aa..8b3eebceb2c5a 100644 --- a/src/core/server/saved_objects/serialization/types.ts +++ b/src/core/server/saved_objects/serialization/types.ts @@ -40,6 +40,7 @@ export interface SavedObjectsRawDocSource { migrationVersion?: SavedObjectsMigrationVersion; updated_at?: string; references?: SavedObjectReference[]; + originId?: string; [typeMapping: string]: any; } @@ -56,6 +57,7 @@ interface SavedObjectDoc { migrationVersion?: SavedObjectsMigrationVersion; version?: string; updated_at?: string; + originId?: string; } interface Referencable { diff --git a/src/core/server/saved_objects/service/lib/included_fields.test.ts b/src/core/server/saved_objects/service/lib/included_fields.test.ts index ced99361f1ea0..356ffff398343 100644 --- a/src/core/server/saved_objects/service/lib/included_fields.test.ts +++ b/src/core/server/saved_objects/service/lib/included_fields.test.ts @@ -19,6 +19,8 @@ import { includedFields } from './included_fields'; +const BASE_FIELD_COUNT = 9; + describe('includedFields', () => { it('returns undefined if fields are not provided', () => { expect(includedFields()).toBe(undefined); @@ -26,7 +28,7 @@ describe('includedFields', () => { it('accepts type string', () => { const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(8); + expect(fields).toHaveLength(BASE_FIELD_COUNT); expect(fields).toContain('type'); }); @@ -42,6 +44,7 @@ Array [ "references", "migrationVersion", "updated_at", + "originId", "foo", ] `); @@ -49,14 +52,14 @@ Array [ it('accepts field as string', () => { const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(8); + expect(fields).toHaveLength(BASE_FIELD_COUNT); expect(fields).toContain('config.foo'); }); it('accepts fields as an array', () => { const fields = includedFields('config', ['foo', 'bar']); - expect(fields).toHaveLength(10); + expect(fields).toHaveLength(BASE_FIELD_COUNT + 2); expect(fields).toContain('config.foo'); expect(fields).toContain('config.bar'); }); @@ -75,6 +78,7 @@ Array [ "references", "migrationVersion", "updated_at", + "originId", "foo", "bar", ] @@ -83,37 +87,43 @@ Array [ it('includes namespace', () => { const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(8); + expect(fields).toHaveLength(BASE_FIELD_COUNT); expect(fields).toContain('namespace'); }); it('includes namespaces', () => { const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(8); + expect(fields).toHaveLength(BASE_FIELD_COUNT); expect(fields).toContain('namespaces'); }); it('includes references', () => { const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(8); + expect(fields).toHaveLength(BASE_FIELD_COUNT); expect(fields).toContain('references'); }); it('includes migrationVersion', () => { const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(8); + expect(fields).toHaveLength(BASE_FIELD_COUNT); expect(fields).toContain('migrationVersion'); }); it('includes updated_at', () => { const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(8); + expect(fields).toHaveLength(BASE_FIELD_COUNT); expect(fields).toContain('updated_at'); }); + it('includes originId', () => { + const fields = includedFields('config', 'foo'); + expect(fields).toHaveLength(BASE_FIELD_COUNT); + expect(fields).toContain('originId'); + }); + it('uses wildcard when type is not provided', () => { const fields = includedFields(undefined, 'foo'); - expect(fields).toHaveLength(8); + expect(fields).toHaveLength(BASE_FIELD_COUNT); expect(fields).toContain('*.foo'); }); @@ -121,7 +131,7 @@ Array [ it('includes legacy field path', () => { const fields = includedFields('config', ['foo', 'bar']); - expect(fields).toHaveLength(10); + expect(fields).toHaveLength(BASE_FIELD_COUNT + 2); expect(fields).toContain('foo'); expect(fields).toContain('bar'); }); diff --git a/src/core/server/saved_objects/service/lib/included_fields.ts b/src/core/server/saved_objects/service/lib/included_fields.ts index 33bca49e3fc58..63d8f184ed2f2 100644 --- a/src/core/server/saved_objects/service/lib/included_fields.ts +++ b/src/core/server/saved_objects/service/lib/included_fields.ts @@ -42,5 +42,6 @@ export function includedFields(type: string | string[] = '*', fields?: string[] .concat('references') .concat('migrationVersion') .concat('updated_at') + .concat('originId') .concat(fields); // v5 compatibility } 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 83e037fb2da66..bf0741c3e91c0 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -153,7 +153,7 @@ describe('SavedObjectsRepository', () => { validateDoc: jest.fn(), }); - const getMockGetResponse = ({ type, id, references, namespace }) => ({ + const getMockGetResponse = ({ type, id, references, namespace, originId }) => ({ // NOTE: Elasticsearch returns more fields (_index, _type) but the SavedObjectsRepository method ignores these found: true, _id: `${registry.isSingleNamespace(type) && namespace ? `${namespace}:` : ''}${type}:${id}`, @@ -161,6 +161,7 @@ describe('SavedObjectsRepository', () => { _source: { ...(registry.isSingleNamespace(type) && { namespace }), ...(registry.isMultiNamespace(type) && { namespaces: [namespace ?? 'default'] }), + ...(originId && { originId }), type, [type]: { title: 'Testing' }, references, @@ -423,6 +424,7 @@ describe('SavedObjectsRepository', () => { id: '6.0.0-alpha1', attributes: { title: 'Test One' }, references: [{ name: 'ref_0', type: 'test', id: '1' }], + originId: 'some-origin-id', // only one of the object args has an originId, this is intentional to test both a positive and negative case }; const obj2 = { type: 'index-pattern', @@ -434,13 +436,14 @@ describe('SavedObjectsRepository', () => { const getMockBulkCreateResponse = (objects, namespace) => { return { - items: objects.map(({ type, id, attributes, references, migrationVersion }) => ({ + items: objects.map(({ type, id, originId, attributes, references, migrationVersion }) => ({ create: { _id: `${namespace ? `${namespace}:` : ''}${type}:${id}`, _source: { [type]: attributes, type, namespace, + ...(originId && { originId }), references, ...mockTimestampFields, migrationVersion: migrationVersion || { [type]: '1.1.1' }, @@ -851,6 +854,7 @@ describe('SavedObjectsRepository', () => { id: '1', }, ], + originId: 'some-origin-id', // only one of the results has an originId, this is intentional to test both a positive and negative case }; const obj2 = { type: 'index-pattern', @@ -986,6 +990,7 @@ describe('SavedObjectsRepository', () => { type, id, ...(doc._source.namespaces && { namespaces: doc._source.namespaces }), + ...(doc._source.originId && { originId: doc._source.originId }), ...(doc._source.updated_at && { updated_at: doc._source.updated_at }), version: encodeHitVersion(doc), attributes: doc._source[type], @@ -1052,27 +1057,35 @@ describe('SavedObjectsRepository', () => { attributes: { title: 'Test Two' }, }; const references = [{ name: 'ref_0', type: 'test', id: '1' }]; + const originId = 'some-origin-id'; const namespace = 'foo-namespace'; - const getMockBulkUpdateResponse = (objects, options) => ({ + const getMockBulkUpdateResponse = (objects, options, includeOriginId) => ({ items: objects.map(({ type, id }) => ({ update: { _id: `${ registry.isSingleNamespace(type) && options?.namespace ? `${options?.namespace}:` : '' }${type}:${id}`, ...mockVersionProps, + get: { + _source: { + // "includeOriginId" is not an option for the operation; however, if the existing saved object contains an originId attribute, the + // operation will return it in the result. This flag is just used for test purposes to modify the mock cluster call response. + ...(includeOriginId && { originId }), + }, + }, result: 'updated', }, })), }); - const bulkUpdateSuccess = async (objects, options) => { + const bulkUpdateSuccess = async (objects, options, includeOriginId) => { const multiNamespaceObjects = objects.filter(({ type }) => registry.isMultiNamespace(type)); if (multiNamespaceObjects?.length) { const response = getMockMgetResponse(multiNamespaceObjects, options?.namespace); callAdminCluster.mockResolvedValueOnce(response); // this._callCluster('mget', ...) } - const response = getMockBulkUpdateResponse(objects, options?.namespace); + const response = getMockBulkUpdateResponse(objects, options?.namespace, includeOriginId); callAdminCluster.mockResolvedValue(response); // this._writeToCluster('bulk', ...) const result = await savedObjectsRepository.bulkUpdate(objects, options); expect(callAdminCluster).toHaveBeenCalledTimes(multiNamespaceObjects?.length ? 2 : 1); @@ -1350,9 +1363,10 @@ describe('SavedObjectsRepository', () => { }); describe('returns', () => { - const expectSuccessResult = ({ type, id, attributes, references }) => ({ + const expectSuccessResult = ({ type, id, attributes, references, originId }) => ({ type, id, + originId, attributes, references, version: mockVersion, @@ -1399,6 +1413,17 @@ describe('SavedObjectsRepository', () => { ], }); }); + + it(`includes originId property if present in cluster call response`, async () => { + const obj = { type: MULTI_NAMESPACE_TYPE, id: 'three' }; + const result = await bulkUpdateSuccess([obj1, obj], {}, true); + expect(result).toEqual({ + saved_objects: [ + expect.objectContaining({ originId }), + expect.objectContaining({ originId }), + ], + }); + }); }); }); @@ -1414,6 +1439,7 @@ describe('SavedObjectsRepository', () => { const attributes = { title: 'Logstash' }; const id = 'logstash-*'; const namespace = 'foo-namespace'; + const originId = 'some-origin-id'; const references = [ { name: 'ref_0', @@ -1490,6 +1516,20 @@ describe('SavedObjectsRepository', () => { await test(null); }); + it(`defaults to no originId`, async () => { + await createSuccess(type, attributes, { id }); + expectClusterCallArgs({ + body: expect.not.objectContaining({ originId: expect.anything() }), + }); + }); + + it(`accepts custom originId`, async () => { + await createSuccess(type, attributes, { id, originId }); + expectClusterCallArgs({ + body: expect.objectContaining({ originId }), + }); + }); + it(`defaults to a refresh setting of wait_for`, async () => { await createSuccess(type, attributes); expectClusterCallArgs({ refresh: 'wait_for' }); @@ -1643,10 +1683,16 @@ describe('SavedObjectsRepository', () => { describe('returns', () => { it(`formats the ES response`, async () => { - const result = await createSuccess(type, attributes, { id, namespace, references }); + const result = await createSuccess(type, attributes, { + id, + namespace, + references, + originId, + }); expect(result).toEqual({ type, id, + originId, ...mockTimestampFields, version: mockVersion, attributes, @@ -1927,6 +1973,7 @@ describe('SavedObjectsRepository', () => { ...mockVersionProps, _source: { namespace, + originId: 'some-origin-id', // only one of the results has an originId, this is intentional to test both a positive and negative case type: 'index-pattern', ...mockTimestampFields, 'index-pattern': { @@ -2028,6 +2075,7 @@ describe('SavedObjectsRepository', () => { 'references', 'migrationVersion', 'updated_at', + 'originId', 'title', ], }); @@ -2124,6 +2172,7 @@ describe('SavedObjectsRepository', () => { expect(response.saved_objects[i]).toEqual({ id: doc._id.replace(/(index-pattern|config|globalType)\:/, ''), type: doc._source.type, + originId: doc._source.originId, ...mockTimestampFields, version: mockVersion, attributes: doc._source[doc._source.type], @@ -2146,6 +2195,7 @@ describe('SavedObjectsRepository', () => { expect(response.saved_objects[i]).toEqual({ id: doc._id.replace(/(foo-namespace\:)?(index-pattern|config|globalType)\:/, ''), type: doc._source.type, + originId: doc._source.originId, ...mockTimestampFields, version: mockVersion, attributes: doc._source[doc._source.type], @@ -2273,9 +2323,17 @@ describe('SavedObjectsRepository', () => { const type = 'index-pattern'; const id = 'logstash-*'; const namespace = 'foo-namespace'; + const originId = 'some-origin-id'; - const getSuccess = async (type, id, options) => { - const response = getMockGetResponse({ type, id, namespace: options?.namespace }); + const getSuccess = async (type, id, options, includeOriginId) => { + const response = getMockGetResponse({ + type, + id, + namespace: options?.namespace, + // "includeOriginId" is not an option for the operation; however, if the existing saved object contains an originId attribute, the + // operation will return it in the result. This flag is just used for test purposes to modify the mock cluster call response. + ...(includeOriginId && { originId }), + }); callAdminCluster.mockResolvedValue(response); const result = await savedObjectsRepository.get(type, id, options); expect(callAdminCluster).toHaveBeenCalledTimes(1); @@ -2383,6 +2441,11 @@ describe('SavedObjectsRepository', () => { namespaces: expect.anything(), }); }); + + it(`includes originId property if present in cluster call response`, async () => { + const result = await getSuccess(type, id, {}, true); + expect(result).toMatchObject({ originId }); + }); }); }); @@ -2391,6 +2454,7 @@ describe('SavedObjectsRepository', () => { const id = 'one'; const field = 'buildNum'; const namespace = 'foo-namespace'; + const originId = 'some-origin-id'; const incrementCounterSuccess = async (type, id, field, options) => { const isMultiNamespace = registry.isMultiNamespace(type); @@ -2562,6 +2626,7 @@ describe('SavedObjectsRepository', () => { buildNum: 8468, defaultIndex: 'logstash-*', }, + originId, }, }, })); @@ -2584,6 +2649,7 @@ describe('SavedObjectsRepository', () => { buildNum: 8468, defaultIndex: 'logstash-*', }, + originId, }); }); }); @@ -2891,8 +2957,9 @@ describe('SavedObjectsRepository', () => { id: '1', }, ]; + const originId = 'some-origin-id'; - const updateSuccess = async (type, id, attributes, options) => { + const updateSuccess = async (type, id, attributes, options, includeOriginId) => { if (registry.isMultiNamespace(type)) { const mockGetResponse = getMockGetResponse({ type, id, namespace: options?.namespace }); callAdminCluster.mockResolvedValueOnce(mockGetResponse); // this._callCluster('get', ...) @@ -2901,10 +2968,17 @@ describe('SavedObjectsRepository', () => { _id: `${type}:${id}`, ...mockVersionProps, result: 'updated', - ...(registry.isMultiNamespace(type) && { - // don't need the rest of the source for test purposes, just the namespaces attribute - get: { _source: { namespaces: [options?.namespace ?? 'default'] } }, - }), + get: { + _source: { + // don't need the rest of the source for test purposes, just the namespaces attribute + ...(registry.isMultiNamespace(type) && { + namespaces: [options?.namespace ?? 'default'], + }), + // "includeOriginId" is not an option for the operation; however, if the existing saved object contains an originId attribute, the + // operation will return it in the result. This flag is just used for test purposes to modify the mock cluster call response. + ...(includeOriginId && { originId }), + }, + }, }); // this._writeToCluster('update', ...) const result = await savedObjectsRepository.update(type, id, attributes, options); expect(callAdminCluster).toHaveBeenCalledTimes(registry.isMultiNamespace(type) ? 2 : 1); @@ -3002,19 +3076,14 @@ describe('SavedObjectsRepository', () => { expectClusterCallArgs({ id: expect.stringMatching(`${MULTI_NAMESPACE_TYPE}:${id}`) }, 2); }); - it(`includes _sourceIncludes when type is multi-namespace`, async () => { - await updateSuccess(MULTI_NAMESPACE_TYPE, id, attributes); - expectClusterCallArgs({ _sourceIncludes: ['namespaces'] }, 2); + it(`uses default _sourceIncludes when type is not multi-namespace`, async () => { + await updateSuccess(type, id, attributes); + expectClusterCallArgs({ _sourceIncludes: ['originId'] }); }); - it(`doesn't include _sourceIncludes when type is not multi-namespace`, async () => { - await updateSuccess(type, id, attributes); - expect(callAdminCluster).toHaveBeenLastCalledWith( - expect.any(String), - expect.not.objectContaining({ - _sourceIncludes: expect.anything(), - }) - ); + it(`adds to _sourceIncludes when type is multi-namespace`, async () => { + await updateSuccess(MULTI_NAMESPACE_TYPE, id, attributes); + expectClusterCallArgs({ _sourceIncludes: ['originId', 'namespaces'] }, 2); }); }); @@ -3102,6 +3171,11 @@ describe('SavedObjectsRepository', () => { namespaces: expect.anything(), }); }); + + it(`includes originId property if present in cluster call response`, async () => { + const result = await updateSuccess(type, id, attributes, {}, true); + expect(result).toMatchObject({ originId }); + }); }); }); }); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index e23f8dec5927c..87ed704b46910 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -219,6 +219,7 @@ export class SavedObjectsRepository { overwrite = false, references = [], refresh = DEFAULT_REFRESH_SETTING, + originId, } = options; if (!this._allowedTypes.includes(type)) { @@ -245,6 +246,7 @@ export class SavedObjectsRepository { type, ...(savedObjectNamespace && { namespace: savedObjectNamespace }), ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), + originId, attributes, migrationVersion, updated_at: time, @@ -376,6 +378,7 @@ export class SavedObjectsRepository { ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), updated_at: time, references: object.references || [], + originId: object.originId, }) as SavedObjectSanitizedDoc ), }; @@ -760,12 +763,14 @@ export class SavedObjectsRepository { } as any) as SavedObject; } - const time = doc._source.updated_at; + const { namespaces, originId, updated_at: updatedAt } = doc._source; + return { id, type, - ...(doc._source.namespaces && { namespaces: doc._source.namespaces }), - ...(time && { updated_at: time }), + ...(namespaces && { namespaces }), + ...(originId && { originId }), + ...(updatedAt && { updated_at: updatedAt }), version: encodeHitVersion(doc), attributes: doc._source[type], references: doc._source.references || [], @@ -808,12 +813,13 @@ export class SavedObjectsRepository { throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } - const { updated_at: updatedAt } = response._source; + const { namespaces, originId, updated_at: updatedAt } = response._source; return { id, type, - ...(response._source.namespaces && { namespaces: response._source.namespaces }), + ...(namespaces && { namespaces }), + ...(originId && { originId }), ...(updatedAt && { updated_at: updatedAt }), version: encodeHitVersion(response), attributes: response._source[type], @@ -858,6 +864,10 @@ export class SavedObjectsRepository { ...(Array.isArray(references) && { references }), }; + const _sourceIncludes = ['originId']; + if (this._registry.isMultiNamespace(type)) { + _sourceIncludes.push('namespaces'); + } const updateResponse = await this._writeToCluster('update', { id: this._serializer.generateRawId(namespace, type, id), index: this.getIndexForType(type), @@ -867,7 +877,7 @@ export class SavedObjectsRepository { body: { doc, }, - ...(this._registry.isMultiNamespace(type) && { _sourceIncludes: ['namespaces'] }), + _sourceIncludes, }); if (updateResponse.status === 404) { @@ -875,14 +885,14 @@ export class SavedObjectsRepository { throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } + const { namespaces, originId } = updateResponse.get._source; return { id, type, + ...(namespaces && { namespaces }), + ...(originId && { originId }), updated_at: time, version: encodeHitVersion(updateResponse), - ...(this._registry.isMultiNamespace(type) && { - namespaces: updateResponse.get._source.namespaces, - }), references, attributes, }; @@ -1169,6 +1179,7 @@ export class SavedObjectsRepository { ? await this._writeToCluster('bulk', { refresh, body: bulkUpdateParams, + _sourceIncludes: ['originId'], }) : {}; @@ -1180,7 +1191,7 @@ export class SavedObjectsRepository { const { type, id, namespaces, documentToSave, esRequestIndex } = expectedResult.value; const response = bulkUpdateResponse.items[esRequestIndex]; - const { error, _seq_no: seqNo, _primary_term: primaryTerm } = Object.values( + const { error, _seq_no: seqNo, _primary_term: primaryTerm, get } = Object.values( response )[0] as any; @@ -1192,10 +1203,13 @@ export class SavedObjectsRepository { error: getBulkOperationError(error, type, id), }; } + + const { originId } = get._source; return { id, type, ...(namespaces && { namespaces }), + ...(originId && { originId }), updated_at, version: encodeVersion(seqNo, primaryTerm), attributes, @@ -1220,7 +1234,7 @@ export class SavedObjectsRepository { id: string, counterFieldName: string, options: SavedObjectsIncrementCounterOptions = {} - ) { + ): Promise { if (typeof type !== 'string') { throw new Error('"type" argument must be a string'); } @@ -1283,9 +1297,12 @@ export class SavedObjectsRepository { }, }); + const { originId } = response.get._source; return { id, type, + ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), + ...(originId && { originId }), updated_at: time, references: response.get._source.references, version: encodeHitVersion(response), 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 8780f07cc3091..2c7f3fdf7c84b 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -42,6 +42,8 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions { references?: SavedObjectReference[]; /** The Elasticsearch Refresh setting for this operation */ refresh?: MutatingOperationRefreshSetting; + /** Optional ID of the original saved object, if this object's `id` was regenerated */ + originId?: string; } /** @@ -55,6 +57,8 @@ export interface SavedObjectsBulkCreateObject { references?: SavedObjectReference[]; /** {@inheritDoc SavedObjectsMigrationVersion} */ migrationVersion?: SavedObjectsMigrationVersion; + /** Optional ID of the original saved object, if this object's `id` was regenerated */ + originId?: string; } /** diff --git a/src/core/types/saved_objects.ts b/src/core/types/saved_objects.ts index d9cd7baa9cff8..8df722a3d908f 100644 --- a/src/core/types/saved_objects.ts +++ b/src/core/types/saved_objects.ts @@ -99,4 +99,11 @@ export interface SavedObject { migrationVersion?: SavedObjectsMigrationVersion; /** Namespace(s) that this saved object exists in. This attribute is only used for multi-namespace saved object types. */ namespaces?: string[]; + /** + * The ID of the saved object this originated from. This is set if this object's `id` was regenerated; that can happen during migration + * from a legacy single-namespace type, or during import. It is only set during migration or create operations. This is used during import + * to ensure that ID regeneration is deterministic, so saved objects will be overwritten if they are imported multiple times into a given + * space. + */ + originId?: string; } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts index b623295c5e060..f1c22b7b38194 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts @@ -9,7 +9,11 @@ import { AssetType } from '../../../types'; import * as Registry from '../registry'; type ArchiveAsset = Pick; -type SavedObjectToBe = Required & { type: AssetType }; +type SavedObjectToBe = Required< + Pick +> & { + type: AssetType; +}; export async function getObject(key: string) { const buffer = Registry.getAsset(key); From c4fa88a4d6bc9c8d680010765f96a3024a35950d Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Sat, 25 Apr 2020 14:22:34 -0400 Subject: [PATCH 04/55] Change saved object export to omit `namespaces` field Single-namespace saved objects do not include the `namespace` field after deserialization, hence it was never exported. Multi-namespace saved objects do include the `namespaces` field, but this should be omitted when the object is exported from Kibana. --- .../get_sorted_objects_for_export.test.ts | 29 +++++++++++++++++++ .../export/get_sorted_objects_for_export.ts | 9 ++++-- 2 files changed, 36 insertions(+), 2 deletions(-) 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 32485f461f59b..4a171eff5da17 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 @@ -560,6 +560,35 @@ describe('getSortedObjectsForExport()', () => { `); }); + test('modifies return results to redact `namespaces` attribute', async () => { + const createSavedObject = (obj: any) => ({ ...obj, attributes: {}, references: [] }); + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + createSavedObject({ type: 'multi', id: '1', namespaces: ['foo'] }), + createSavedObject({ type: 'multi', id: '2', namespaces: ['bar'] }), + createSavedObject({ type: 'other', id: '3' }), + ], + }); + const exportStream = await exportSavedObjectsToStream({ + exportSizeLimit: 10000, + savedObjectsClient, + objects: [ + { type: 'multi', id: '1' }, + { type: 'multi', id: '2' }, + { type: 'other', id: '3' }, + ], + }); + const response = await readStreamToCompletion(exportStream); + expect(response).toEqual( + expect.arrayContaining([ + expect.not.objectContaining({ id: '1', namespaces: expect.anything() }), + expect.not.objectContaining({ id: '2', namespaces: expect.anything() }), + expect.not.objectContaining({ id: '3', namespaces: expect.anything() }), + expect.objectContaining({ exportedCount: 3 }), + ]) + ); + }); + test('includes nested dependencies when passed in', async () => { savedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ 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 cafaa5a3147db..80f878aa54dbe 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 @@ -148,7 +148,7 @@ export async function exportSavedObjectsToStream({ exportSizeLimit, namespace, }); - let exportedObjects = []; + let exportedObjects: Array> = []; let missingReferences: SavedObjectsExportResultDetails['missingReferences'] = []; if (includeReferencesDeep) { @@ -159,10 +159,15 @@ export async function exportSavedObjectsToStream({ exportedObjects = sortObjects(rootObjects); } + // redact attributes that should not be exported + const redactedObjects = exportedObjects.map>( + ({ namespaces, ...object }) => object + ); + const exportDetails: SavedObjectsExportResultDetails = { exportedCount: exportedObjects.length, missingRefCount: missingReferences.length, missingReferences, }; - return createListStream([...exportedObjects, ...(excludeExportDetails ? [] : [exportDetails])]); + return createListStream([...redactedObjects, ...(excludeExportDetails ? [] : [exportDetails])]); } From c1735a61e6c697d700c4a1eb317da4537bf0a480 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Sat, 25 Apr 2020 14:23:23 -0400 Subject: [PATCH 05/55] Modify `bulkCreate` to differentiate between conflict types bulkCreate now attaches error metadata to indicate when an unresolvable conflict occurred. --- .../service/lib/repository.test.js | 40 +++++++++++-------- .../saved_objects/service/lib/repository.ts | 8 ++-- src/core/types/saved_objects.ts | 1 + 3 files changed, 29 insertions(+), 20 deletions(-) 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 bf0741c3e91c0..72e1bc29cd547 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -202,13 +202,17 @@ describe('SavedObjectsRepository', () => { }); const expectSuccess = ({ type, id }) => expect.toBeDocumentWithoutError(type, id); const expectError = ({ type, id }) => ({ type, id, error: expect.any(Object) }); - const expectErrorResult = ({ type, id }, error) => ({ type, id, error }); - const expectErrorNotFound = (obj) => - expectErrorResult(obj, createGenericNotFoundError(obj.type, obj.id)); - const expectErrorConflict = (obj) => - expectErrorResult(obj, createConflictError(obj.type, obj.id)); - const expectErrorInvalidType = (obj) => - expectErrorResult(obj, createUnsupportedTypeError(obj.type, obj.id)); + const expectErrorResult = ({ type, id }, error, overrides = {}) => ({ + type, + id, + error: { ...error, ...overrides }, + }); + const expectErrorNotFound = (obj, overrides) => + expectErrorResult(obj, createGenericNotFoundError(obj.type, obj.id), overrides); + const expectErrorConflict = (obj, overrides) => + expectErrorResult(obj, createConflictError(obj.type, obj.id), overrides); + const expectErrorInvalidType = (obj, overrides) => + expectErrorResult(obj, createUnsupportedTypeError(obj.type, obj.id), overrides); const expectMigrationArgs = (args, contains = true, n = 1) => { const obj = contains ? expect.objectContaining(args) : expect.not.objectContaining(args); @@ -455,9 +459,9 @@ describe('SavedObjectsRepository', () => { }; const bulkCreateSuccess = async (objects, options) => { - const multiNamespaceObjects = - options?.overwrite && - objects.filter(({ type, id }) => registry.isMultiNamespace(type) && id); + const multiNamespaceObjects = objects.filter( + ({ type, id }) => registry.isMultiNamespace(type) && id + ); if (multiNamespaceObjects?.length) { const response = getMockMgetResponse(multiNamespaceObjects, options?.namespace); callAdminCluster.mockResolvedValueOnce(response); // this._callCluster('mget', ...) @@ -472,14 +476,15 @@ describe('SavedObjectsRepository', () => { // bulk create calls have two objects for each source -- the action, and the source const expectClusterCallArgsAction = ( objects, - { method, _index = expect.any(String), getId = () => expect.any(String) } + { method, _index = expect.any(String), getId = () => expect.any(String) }, + n ) => { const body = []; for (const { type, id } of objects) { body.push({ [method]: { _index, _id: getId(type, id) } }); body.push(expect.any(Object)); } - expectClusterCallArgs({ body }); + expectClusterCallArgs({ body }, n); }; const expectObjArgs = ({ type, attributes, references }, overrides) => [ @@ -506,9 +511,9 @@ describe('SavedObjectsRepository', () => { expectClusterCalls('bulk'); }); - it(`should use the ES mget action before bulk action for any types that are multi-namespace, when overwrite=true`, async () => { + it(`should use the ES mget action before bulk action for any types that are multi-namespace, when id is defined`, async () => { const objects = [obj1, { ...obj2, type: MULTI_NAMESPACE_TYPE }]; - await bulkCreateSuccess(objects, { overwrite: true }); + await bulkCreateSuccess(objects); expectClusterCalls('mget', 'bulk'); const docs = [expect.objectContaining({ _id: `${MULTI_NAMESPACE_TYPE}:${obj2.id}` })]; expectClusterCallArgs({ body: { docs } }, 1); @@ -557,7 +562,7 @@ describe('SavedObjectsRepository', () => { await bulkCreateSuccess(objects, { namespace }); const expected = expect.not.objectContaining({ namespace: expect.anything() }); const body = [expect.any(Object), expected, expect.any(Object), expected]; - expectClusterCallArgs({ body }); + expectClusterCallArgs({ body }, 2); }); it(`adds namespaces to request body for any types that are multi-namespace`, async () => { @@ -627,7 +632,7 @@ describe('SavedObjectsRepository', () => { { ...obj2, type: MULTI_NAMESPACE_TYPE }, ]; await bulkCreateSuccess(objects, { namespace }); - expectClusterCallArgsAction(objects, { method: 'create', getId }); + expectClusterCallArgsAction(objects, { method: 'create', getId }, 2); }); }); @@ -695,8 +700,9 @@ describe('SavedObjectsRepository', () => { expectClusterCallArgs({ body: body1 }, 1); const body2 = [...expectObjArgs(obj1), ...expectObjArgs(obj2)]; expectClusterCallArgs({ body: body2 }, 2); + const expectedError = expectErrorConflict(obj, { metadata: { isNotOverwritable: true } }); expect(result).toEqual({ - saved_objects: [expectSuccess(obj1), expectErrorConflict(obj), expectSuccess(obj2)], + saved_objects: [expectSuccess(obj1), expectedError, expectSuccess(obj2)], }); }); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 87ed704b46910..a7fda7ed853d3 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -299,8 +299,7 @@ export class SavedObjectsRepository { } const method = object.id && overwrite ? 'index' : 'create'; - const requiresNamespacesCheck = - method === 'index' && this._registry.isMultiNamespace(object.type); + const requiresNamespacesCheck = object.id && this._registry.isMultiNamespace(object.type); if (object.id == null) object.id = uuid.v1(); @@ -352,7 +351,10 @@ export class SavedObjectsRepository { error: { id, type, - error: SavedObjectsErrorHelpers.createConflictError(type, id).output.payload, + error: { + ...SavedObjectsErrorHelpers.createConflictError(type, id).output.payload, + metadata: { isNotOverwritable: true }, + }, }, }; } diff --git a/src/core/types/saved_objects.ts b/src/core/types/saved_objects.ts index 8df722a3d908f..c43e7e9f8ac9d 100644 --- a/src/core/types/saved_objects.ts +++ b/src/core/types/saved_objects.ts @@ -90,6 +90,7 @@ export interface SavedObject { error: string; message: string; statusCode: number; + metadata?: Record; }; /** {@inheritdoc SavedObjectAttributes} */ attributes: T; From 519c260b6162c9dd81f54771dc4c5394ed50457c Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Sat, 25 Apr 2020 14:33:39 -0400 Subject: [PATCH 06/55] Add `rawSearchFields` option to `SavedObjectsClient.find` function When attempting to search with a simple_query_string term, the `searchFields` option only allows for selecting type-specific attributes (e.g., the fields in the `attributes` object). Every element in `searchFields` is prefixed by the object type before being added to the simple_query_string term. This commit adds a `rawSearchFields` option which is not prefixed by the object type -- this way, searches can use fields that are not specific to the object type (such as `_id` or `originId`). --- .../saved_objects/saved_objects_client.ts | 5 ++- .../saved_objects/service/lib/repository.ts | 2 + .../lib/search_dsl/query_params.test.ts | 45 +++++++++++++++---- .../service/lib/search_dsl/query_params.ts | 16 ++++--- .../service/lib/search_dsl/search_dsl.test.ts | 4 +- .../service/lib/search_dsl/search_dsl.ts | 3 ++ src/core/server/saved_objects/types.ts | 3 ++ 7 files changed, 62 insertions(+), 16 deletions(-) diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index cdc113871c447..9eada98210474 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -31,7 +31,10 @@ import { import { SimpleSavedObject } from './simple_saved_object'; import { HttpFetchOptions, HttpSetup } from '../http'; -type SavedObjectsFindOptions = Omit; +type SavedObjectsFindOptions = Omit< + SavedObjectFindOptionsServer, + 'namespace' | 'sortOrder' | 'rawSearchFields' +>; type PromiseType> = T extends Promise ? U : never; diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index a7fda7ed853d3..1118fa2c737c4 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -587,6 +587,7 @@ export class SavedObjectsRepository { search, defaultSearchOperator = 'OR', searchFields, + rawSearchFields, hasReference, page = 1, perPage = 20, @@ -649,6 +650,7 @@ export class SavedObjectsRepository { search, defaultSearchOperator, searchFields, + rawSearchFields, type: allowedTypes, sortField, sortOrder, 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 a0ffa91f53671..14967a785c14b 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 @@ -277,13 +277,19 @@ describe('#getQueryParams', () => { }); }); - describe('`searchFields` parameter', () => { + describe('`searchFields` and `rawSearchFields` parameters', () => { const getExpectedFields = (searchFields: string[], typeOrTypes: string | string[]) => { const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes]; return searchFields.map((x) => types.map((y) => `${y}.${x}`)).flat(); }; - const test = (searchFields: string[]) => { + const test = ({ + searchFields, + rawSearchFields, + }: { + searchFields?: string[]; + rawSearchFields?: string[]; + }) => { for (const typeOrTypes of ALL_TYPE_SUBSETS) { const result = getQueryParams({ mappings, @@ -291,8 +297,12 @@ describe('#getQueryParams', () => { type: typeOrTypes, search, searchFields, + rawSearchFields, }); - const fields = getExpectedFields(searchFields, typeOrTypes); + let fields = rawSearchFields || []; + if (searchFields) { + fields = fields.concat(getExpectedFields(searchFields, typeOrTypes)); + } expectResult(result, expect.objectContaining({ fields })); } // also test with no specified type/s @@ -302,31 +312,48 @@ describe('#getQueryParams', () => { type: undefined, search, searchFields, + rawSearchFields, }); - const fields = getExpectedFields(searchFields, ALL_TYPES); + let fields = rawSearchFields || []; + if (searchFields) { + fields = fields.concat(getExpectedFields(searchFields, ALL_TYPES)); + } expectResult(result, expect.objectContaining({ fields })); }; - it('includes lenient flag and all fields when `searchFields` is not specified', () => { + it('includes lenient flag and all fields when `searchFields` and `rawSearchFields` are not specified', () => { const result = getQueryParams({ mappings, registry, search, searchFields: undefined, + rawSearchFields: undefined, }); expectResult(result, expect.objectContaining({ lenient: true, fields: ['*'] })); }); it('includes specified search fields for appropriate type/s', () => { - test(['title']); + test({ searchFields: ['title'] }); }); it('supports boosting', () => { - test(['title^3']); + test({ searchFields: ['title^3'] }); + }); + + it('supports multiple search fields', () => { + test({ searchFields: ['title, title.raw'] }); + }); + + it('includes specified raw search fields', () => { + test({ rawSearchFields: ['_id'] }); + }); + + it('supports multiple raw search fields', () => { + test({ rawSearchFields: ['_id', 'originId'] }); }); - it('supports multiple fields', () => { - test(['title, title.raw']); + it('supports search fields and raw search fields', () => { + test({ searchFields: ['title'], rawSearchFields: ['_id'] }); }); }); diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts index 40485564176a6..269fc8899ea2e 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts @@ -39,17 +39,21 @@ function getTypes(mappings: IndexMapping, type?: string | string[]) { } /** - * Get the field params based on the types and searchFields + * Get the field params based on the types, searchFields, and rawSearchFields */ -function getFieldsForTypes(types: string[], searchFields?: string[]) { - if (!searchFields || !searchFields.length) { +function getFieldsForTypes( + types: string[], + searchFields: string[] = [], + rawSearchFields: string[] = [] +) { + if (!searchFields.length && !rawSearchFields.length) { return { lenient: true, fields: ['*'], }; } - let fields: string[] = []; + let fields: string[] = rawSearchFields; for (const field of searchFields) { fields = fields.concat(types.map((prefix) => `${prefix}.${field}`)); } @@ -102,6 +106,7 @@ interface QueryParams { type?: string | string[]; search?: string; searchFields?: string[]; + rawSearchFields?: string[]; defaultSearchOperator?: string; hasReference?: HasReferenceQueryParams; kueryNode?: KueryNode; @@ -117,6 +122,7 @@ export function getQueryParams({ type, search, searchFields, + rawSearchFields, defaultSearchOperator, hasReference, kueryNode, @@ -164,7 +170,7 @@ export function getQueryParams({ { simple_query_string: { query: search, - ...getFieldsForTypes(types, searchFields), + ...getFieldsForTypes(types, searchFields, rawSearchFields), ...(defaultSearchOperator ? { default_operator: defaultSearchOperator } : {}), }, }, diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts index 95b7ffd117ee9..226cf8e187f22 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts @@ -57,12 +57,13 @@ describe('getSearchDsl', () => { }); describe('passes control', () => { - it('passes (mappings, schema, namespace, type, search, searchFields, hasReference) to getQueryParams', () => { + it('passes (mappings, schema, namespace, type, search, searchFields, rawSearchFields, hasReference) to getQueryParams', () => { const opts = { namespace: 'foo-namespace', type: 'foo', search: 'bar', searchFields: ['baz'], + rawSearchFields: ['qux'], defaultSearchOperator: 'AND', hasReference: { type: 'bar', @@ -79,6 +80,7 @@ describe('getSearchDsl', () => { type: opts.type, search: opts.search, searchFields: opts.searchFields, + rawSearchFields: opts.rawSearchFields, defaultSearchOperator: opts.defaultSearchOperator, hasReference: opts.hasReference, }); diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts index 74c25491aff8b..d612e1006d2f3 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts @@ -31,6 +31,7 @@ interface GetSearchDslOptions { search?: string; defaultSearchOperator?: string; searchFields?: string[]; + rawSearchFields?: string[]; sortField?: string; sortOrder?: string; namespace?: string; @@ -51,6 +52,7 @@ export function getSearchDsl( search, defaultSearchOperator, searchFields, + rawSearchFields, sortField, sortOrder, namespace, @@ -74,6 +76,7 @@ export function getSearchDsl( type, search, searchFields, + rawSearchFields, defaultSearchOperator, hasReference, kueryNode, diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index 43b7663491711..9ff5cc9deb12c 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -79,6 +79,9 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { search?: string; /** The fields to perform the parsed query against. See Elasticsearch Simple Query String `fields` argument for more information */ searchFields?: string[]; + /** The fields to perform the parsed query against. Unlike the `searchFields` argument, these are expected to be raw and will not be + * modified. If used in conjunction with `searchFields`, both are concatenated together. */ + rawSearchFields?: string[]; hasReference?: { type: string; id: string }; defaultSearchOperator?: 'AND' | 'OR'; filter?: string; From 783583314d22b8a540fc5675773b974976d9381f Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 5 May 2020 18:18:26 -0400 Subject: [PATCH 07/55] Validate import objects and retries for uniqueness Kibana does not officially support importing multiple objects with the same type/ID simultaneously. While this will not fail, it will result in a confusing outcome, because these objects would conflict with each other (and overwrite each other if that flag is used). This commit simply adds validation to throw a descriptive error if this situation is encountered. It should never happen in normal usage of Kibana, only if someone is tampering with an exported NDJSON file or otherwise misusing the API. --- .../import/collect_saved_objects.test.ts | 260 +++++++++++------- .../import/collect_saved_objects.ts | 11 + .../import/resolve_import_errors.ts | 4 + .../saved_objects/import/utilities.test.ts | 38 +++ .../server/saved_objects/import/utilities.ts | 35 +++ .../import/validate_retries.test.ts | 96 +++++++ .../saved_objects/import/validate_retries.ts | 37 +++ 7 files changed, 383 insertions(+), 98 deletions(-) create mode 100644 src/core/server/saved_objects/import/utilities.test.ts create mode 100644 src/core/server/saved_objects/import/utilities.ts create mode 100644 src/core/server/saved_objects/import/validate_retries.test.ts create mode 100644 src/core/server/saved_objects/import/validate_retries.ts diff --git a/src/core/server/saved_objects/import/collect_saved_objects.test.ts b/src/core/server/saved_objects/import/collect_saved_objects.test.ts index 9cccc3942f655..b2c62471b13fc 100644 --- a/src/core/server/saved_objects/import/collect_saved_objects.test.ts +++ b/src/core/server/saved_objects/import/collect_saved_objects.test.ts @@ -17,121 +17,185 @@ * under the License. */ -import { Readable } from 'stream'; +import { Readable, PassThrough } from 'stream'; import { collectSavedObjects } from './collect_saved_objects'; +import { createLimitStream } from './create_limit_stream'; +import { getNonUniqueEntries } from './utilities'; + +jest.mock('./create_limit_stream'); +jest.mock('./utilities'); + +const getMockFn = any, U>(fn: (...args: Parameters) => U) => + fn as jest.MockedFunction<(...args: Parameters) => U>; + +let limitStreamPush: jest.SpyInstance; + +beforeEach(() => { + jest.clearAllMocks(); + const stream = new PassThrough({ objectMode: true }); + limitStreamPush = jest.spyOn(stream, 'push'); + getMockFn(createLimitStream).mockReturnValue(stream); + getMockFn(getNonUniqueEntries).mockReturnValue([]); +}); describe('collectSavedObjects()', () => { - test('collects nothing when stream is empty', async () => { - const readStream = new Readable({ + const objectLimit = 10; + const createReadStream = (...args: any[]) => + new Readable({ objectMode: true, read() { + args.forEach((arg) => this.push(arg)); this.push(null); }, }); - const result = await collectSavedObjects({ readStream, objectLimit: 10, supportedTypes: [] }); - expect(result).toMatchInlineSnapshot(` -Object { - "collectedObjects": Array [], - "errors": Array [], -} -`); - }); - test('collects objects from stream', async () => { - const readStream = new Readable({ - objectMode: true, - read() { - this.push({ foo: true, type: 'a' }); - this.push(null); - }, + const obj1 = { type: 'a', id: '1', attributes: { title: 'my title 1' } }; + const obj2 = { type: 'b', id: '2', attributes: { title: 'my title 2' } }; + + describe('module calls', () => { + test('limit stream with empty input stream is called with null', async () => { + const readStream = createReadStream(); + await collectSavedObjects({ readStream, supportedTypes: [], objectLimit }); + + expect(createLimitStream).toHaveBeenCalledWith(objectLimit); + expect(limitStreamPush).toHaveBeenCalledTimes(1); + expect(limitStreamPush).toHaveBeenLastCalledWith(null); }); - const result = await collectSavedObjects({ - readStream, - objectLimit: 1, - supportedTypes: ['a'], + + test('limit stream with non-empty input stream is called with all objects', async () => { + const readStream = createReadStream(obj1, obj2); + const supportedTypes = [obj2.type]; + await collectSavedObjects({ readStream, supportedTypes, objectLimit }); + + expect(createLimitStream).toHaveBeenCalledWith(objectLimit); + expect(limitStreamPush).toHaveBeenCalledTimes(3); + expect(limitStreamPush).toHaveBeenNthCalledWith(1, obj1); + expect(limitStreamPush).toHaveBeenNthCalledWith(2, obj2); + expect(limitStreamPush).toHaveBeenLastCalledWith(null); }); - expect(result).toMatchInlineSnapshot(` -Object { - "collectedObjects": Array [ - Object { - "foo": true, - "migrationVersion": Object {}, - "type": "a", - }, - ], - "errors": Array [], -} -`); - }); - test('throws error when object limit is reached', async () => { - const readStream = new Readable({ - objectMode: true, - read() { - this.push({ foo: true, type: 'a' }); - this.push({ bar: true, type: 'a' }); - this.push(null); - }, + test('get non-unique entries with empty input stream is called with empty array', async () => { + const readStream = createReadStream(); + await collectSavedObjects({ readStream, supportedTypes: [], objectLimit }); + + expect(getNonUniqueEntries).toHaveBeenCalledWith([]); }); - await expect( - collectSavedObjects({ - readStream, - objectLimit: 1, - supportedTypes: ['a'], - }) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"Can't import more than 1 objects"`); - }); - test('unsupported types return as import errors', async () => { - const readStream = new Readable({ - objectMode: true, - read() { - this.push({ id: '1', type: 'a', attributes: { title: 'my title' } }); - this.push({ id: '2', type: 'b', attributes: { title: 'my title 2' } }); - this.push(null); - }, + test('get non-unique entries with non-empty input stream is called with all entries', async () => { + const readStream = createReadStream(obj1, obj2); + const supportedTypes = [obj2.type]; + await collectSavedObjects({ readStream, supportedTypes, objectLimit }); + + expect(getNonUniqueEntries).toHaveBeenCalledWith([ + { type: obj1.type, id: obj1.id }, + { type: obj2.type, id: obj2.id }, + ]); + }); + + test('filter with empty input stream is not called', async () => { + const readStream = createReadStream(); + const filter = jest.fn(); + await collectSavedObjects({ readStream, supportedTypes: [], objectLimit, filter }); + + expect(filter).not.toHaveBeenCalled(); + }); + + test('filter with non-empty input stream is called with all objects of supported types', async () => { + const readStream = createReadStream(obj1, obj2); + const filter = jest.fn(); + const supportedTypes = [obj2.type]; + await collectSavedObjects({ readStream, supportedTypes, objectLimit, filter }); + + expect(filter).toHaveBeenCalledTimes(1); + expect(filter).toHaveBeenCalledWith(obj2); }); - const result = await collectSavedObjects({ readStream, objectLimit: 2, supportedTypes: ['1'] }); - expect(result).toMatchInlineSnapshot(` -Object { - "collectedObjects": Array [], - "errors": Array [ - Object { - "error": Object { - "type": "unsupported_type", - }, - "id": "1", - "title": "my title", - "type": "a", - }, - Object { - "error": Object { - "type": "unsupported_type", - }, - "id": "2", - "title": "my title 2", - "type": "b", - }, - ], -} -`); }); - test('unsupported types still count towards object limit', async () => { - const readStream = new Readable({ - objectMode: true, - read() { - this.push({ foo: true, type: 'a' }); - this.push({ bar: true, type: 'b' }); - this.push(null); - }, + describe('results', () => { + test('throws Boom error if any import objects are not unique', async () => { + getMockFn(getNonUniqueEntries).mockReturnValue(['type1:id1', 'type2:id2']); + const readStream = createReadStream(); + expect.assertions(2); + try { + await collectSavedObjects({ readStream, supportedTypes: [], objectLimit }); + } catch ({ isBoom, message }) { + expect(isBoom).toBe(true); + expect(message).toMatchInlineSnapshot( + `"Non-unique import objects detected: [type1:id1,type2:id2]"` + ); + } + }); + + test('collects nothing when stream is empty', async () => { + const readStream = createReadStream(); + const result = await collectSavedObjects({ readStream, supportedTypes: [], objectLimit }); + + expect(result).toEqual({ collectedObjects: [], errors: [] }); + }); + + test('collects objects from stream', async () => { + const readStream = createReadStream(obj1); + const supportedTypes = [obj1.type]; + const result = await collectSavedObjects({ readStream, supportedTypes, objectLimit }); + + const collectedObjects = [{ ...obj1, migrationVersion: {} }]; + expect(result).toEqual({ collectedObjects, errors: [] }); + }); + + test('unsupported types return as import errors', async () => { + const readStream = createReadStream(obj1); + const supportedTypes = ['not-obj1-type']; + const result = await collectSavedObjects({ readStream, supportedTypes, objectLimit }); + + const error = { type: 'unsupported_type' }; + const errors = [{ error, type: obj1.type, id: obj1.id, title: obj1.attributes.title }]; + expect(result).toEqual({ collectedObjects: [], errors }); + }); + + test('returns mixed results', async () => { + const readStream = createReadStream(obj1, obj2); + const supportedTypes = [obj2.type]; + const result = await collectSavedObjects({ readStream, supportedTypes, objectLimit }); + + const collectedObjects = [{ ...obj2, migrationVersion: {} }]; + const error = { type: 'unsupported_type' }; + const errors = [{ error, type: obj1.type, id: obj1.id, title: obj1.attributes.title }]; + expect(result).toEqual({ collectedObjects, errors }); + }); + + describe('with optional filter', () => { + test('filters out objects when result === false', async () => { + const readStream = createReadStream(obj1, obj2); + const filter = jest.fn().mockReturnValue(false); + const supportedTypes = [obj2.type]; + const result = await collectSavedObjects({ + readStream, + supportedTypes, + objectLimit, + filter, + }); + + const error = { type: 'unsupported_type' }; + const errors = [{ error, type: obj1.type, id: obj1.id, title: obj1.attributes.title }]; + expect(result).toEqual({ collectedObjects: [], errors }); + }); + + test('does not filter out objects when result === true', async () => { + const readStream = createReadStream(obj1, obj2); + const filter = jest.fn().mockReturnValue(true); + const supportedTypes = [obj2.type]; + const result = await collectSavedObjects({ + readStream, + supportedTypes, + objectLimit, + filter, + }); + + const collectedObjects = [{ ...obj2, migrationVersion: {} }]; + const error = { type: 'unsupported_type' }; + const errors = [{ error, type: obj1.type, id: obj1.id, title: obj1.attributes.title }]; + expect(result).toEqual({ collectedObjects, errors }); + }); }); - await expect( - collectSavedObjects({ - readStream, - objectLimit: 1, - supportedTypes: ['a'], - }) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"Can't import more than 1 objects"`); }); }); diff --git a/src/core/server/saved_objects/import/collect_saved_objects.ts b/src/core/server/saved_objects/import/collect_saved_objects.ts index 1b787c7d9dc10..ef635bf27e7c8 100644 --- a/src/core/server/saved_objects/import/collect_saved_objects.ts +++ b/src/core/server/saved_objects/import/collect_saved_objects.ts @@ -17,6 +17,7 @@ * under the License. */ +import Boom from 'boom'; import { Readable } from 'stream'; import { createConcatStream, @@ -27,6 +28,7 @@ import { import { SavedObject } from '../types'; import { createLimitStream } from './create_limit_stream'; import { SavedObjectsImportError } from './types'; +import { getNonUniqueEntries } from './utilities'; interface CollectSavedObjectsOptions { readStream: Readable; @@ -42,10 +44,12 @@ export async function collectSavedObjects({ supportedTypes, }: CollectSavedObjectsOptions) { const errors: SavedObjectsImportError[] = []; + const entries: Array<{ type: string; id: string }> = []; const collectedObjects: Array> = await createPromiseFromStreams([ readStream, createLimitStream(objectLimit), createFilterStream>((obj) => { + entries.push({ type: obj.type, id: obj.id }); if (supportedTypes.includes(obj.type)) { return true; } @@ -66,6 +70,13 @@ export async function collectSavedObjects({ }), createConcatStream([]), ]); + + // throw a BadRequest error if we see the same import object type/id more than once + const nonUniqueEntries = getNonUniqueEntries(entries); + if (nonUniqueEntries.length > 0) { + throw Boom.badRequest(`Non-unique import objects detected: [${nonUniqueEntries.join()}]`); + } + return { errors, collectedObjects, 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 47e5e5cd8ebf5..4a8e015bffac7 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.ts @@ -26,6 +26,7 @@ import { SavedObjectsResolveImportErrorsOptions, } from './types'; import { validateReferences } from './validate_references'; +import { validateRetries } from './validate_retries'; /** * Resolve and return saved object import errors. @@ -41,6 +42,9 @@ export async function resolveSavedObjectsImportErrors({ typeRegistry, namespace, }: SavedObjectsResolveImportErrorsOptions): Promise { + // throw a BadRequest error if we see invalid retries + validateRetries(retries); + let successCount = 0; let errorAccumulator: SavedObjectsImportError[] = []; const supportedTypes = typeRegistry.getImportableAndExportableTypes().map((type) => type.name); diff --git a/src/core/server/saved_objects/import/utilities.test.ts b/src/core/server/saved_objects/import/utilities.test.ts new file mode 100644 index 0000000000000..ccd40fa6a5621 --- /dev/null +++ b/src/core/server/saved_objects/import/utilities.test.ts @@ -0,0 +1,38 @@ +/* + * 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 { getNonUniqueEntries } from './utilities'; + +const foo1 = { type: 'foo', id: '1' }; +const foo2 = { type: 'foo', id: '2' }; // same type as foo1, different ID +const bar1 = { type: 'bar', id: '1' }; // same ID as foo1, different type + +describe('#getNonUniqueEntries', () => { + test('returns empty array if entries are unique', () => { + const result = getNonUniqueEntries([foo1, foo2, bar1]); + expect(result).toEqual([]); + }); + + test('returns non-empty array for non-unique results', () => { + const result1 = getNonUniqueEntries([foo1, foo2, foo1]); + const result2 = getNonUniqueEntries([foo1, foo2, foo1, foo2]); + expect(result1).toEqual([`${foo1.type}:${foo1.id}`]); + expect(result2).toEqual([`${foo1.type}:${foo1.id}`, `${foo2.type}:${foo2.id}`]); + }); +}); diff --git a/src/core/server/saved_objects/import/utilities.ts b/src/core/server/saved_objects/import/utilities.ts new file mode 100644 index 0000000000000..468bf73d9b2db --- /dev/null +++ b/src/core/server/saved_objects/import/utilities.ts @@ -0,0 +1,35 @@ +/* + * 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. + */ + +type Entries = Array<{ type: string; id: string }>; + +export const getNonUniqueEntries = (objects: Entries) => { + const idCountMap = objects.reduce((acc, { type, id }) => { + const key = `${type}:${id}`; + const val = acc.get(key) ?? 0; + return acc.set(key, val + 1); + }, new Map()); + const nonUniqueEntries: string[] = []; + idCountMap.forEach((value, key) => { + if (value >= 2) { + nonUniqueEntries.push(key); + } + }); + return nonUniqueEntries; +}; diff --git a/src/core/server/saved_objects/import/validate_retries.test.ts b/src/core/server/saved_objects/import/validate_retries.test.ts new file mode 100644 index 0000000000000..4272c6a33cb2d --- /dev/null +++ b/src/core/server/saved_objects/import/validate_retries.test.ts @@ -0,0 +1,96 @@ +/* + * 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 { validateRetries } from './validate_retries'; +import { SavedObjectsImportRetry } from '.'; + +import { getNonUniqueEntries } from './utilities'; +jest.mock('./utilities'); +const mockGetNonUniqueEntries = getNonUniqueEntries as jest.MockedFunction< + typeof getNonUniqueEntries +>; + +beforeEach(() => { + jest.clearAllMocks(); + mockGetNonUniqueEntries.mockReturnValue([]); +}); + +describe('#validateRetries', () => { + const createRetry = (object: unknown) => object as SavedObjectsImportRetry; + + describe('module calls', () => { + test('empty retries', () => { + validateRetries([]); + expect(getNonUniqueEntries).toHaveBeenCalledTimes(2); + expect(getNonUniqueEntries).toHaveBeenNthCalledWith(1, []); + expect(getNonUniqueEntries).toHaveBeenNthCalledWith(2, []); + }); + + test('non-empty retries', () => { + const retry1 = createRetry({ type: 'foo', id: '1' }); + const retry2 = createRetry({ type: 'foo', id: '2', overwrite: true }); + const retry3 = createRetry({ type: 'foo', id: '3', idToOverwrite: 'a' }); // this is not a valid retry but we test it for posterity + const retry4 = createRetry({ type: 'foo', id: '4', overwrite: true, idToOverwrite: 'b' }); + const retry5 = createRetry({ type: 'foo', id: '5', overwrite: true, idToOverwrite: 'c' }); + const retries = [retry1, retry2, retry3, retry4, retry5]; + validateRetries(retries); + expect(getNonUniqueEntries).toHaveBeenCalledTimes(2); + // check all retry objects for non-unique entries + expect(getNonUniqueEntries).toHaveBeenNthCalledWith(1, retries); + // check only retry overwrites with `overwrite` === true and `idToOverwrite` !== undefined for non-unique entries + const retryOverwriteEntries = [ + { type: retry4.type, id: retry4.idToOverwrite }, + { type: retry5.type, id: retry5.idToOverwrite }, + ]; + expect(getNonUniqueEntries).toHaveBeenNthCalledWith(2, retryOverwriteEntries); + }); + }); + + describe('results', () => { + test('throws Boom error if any retry objects are not unique', () => { + mockGetNonUniqueEntries.mockReturnValue(['type1:id1', 'type2:id2']); + expect.assertions(2); + try { + validateRetries([]); + } catch ({ isBoom, message }) { + expect(isBoom).toBe(true); + expect(message).toMatchInlineSnapshot(`"Non-unique retry objects: [type1:id1,type2:id2]"`); + } + }); + + test('throws Boom error if any retry overwrites are not unique', () => { + mockGetNonUniqueEntries.mockReturnValueOnce([]); + mockGetNonUniqueEntries.mockReturnValue(['type1:id1', 'type2:id2']); + expect.assertions(2); + try { + validateRetries([]); + } catch ({ isBoom, message }) { + expect(isBoom).toBe(true); + expect(message).toMatchInlineSnapshot( + `"Non-unique retry overwrites: [type1:id1,type2:id2]"` + ); + } + }); + + test('does not throw error if retry objects and retry overwrites are unique', () => { + // no need to mock return value, the mock `getNonUniqueEntries` function returns an empty array by default + expect(() => validateRetries([])).not.toThrowError(); + }); + }); +}); diff --git a/src/core/server/saved_objects/import/validate_retries.ts b/src/core/server/saved_objects/import/validate_retries.ts new file mode 100644 index 0000000000000..3ea143c0711c9 --- /dev/null +++ b/src/core/server/saved_objects/import/validate_retries.ts @@ -0,0 +1,37 @@ +/* + * 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 Boom from 'boom'; +import { SavedObjectsImportRetry } from './types'; +import { getNonUniqueEntries } from './utilities'; + +export const validateRetries = (retries: SavedObjectsImportRetry[]) => { + const nonUniqueRetryObjects = getNonUniqueEntries(retries); + if (nonUniqueRetryObjects.length > 0) { + throw Boom.badRequest(`Non-unique retry objects: [${nonUniqueRetryObjects.join()}]`); + } + + const overwriteEntries = retries + .filter((retry) => retry.overwrite && retry.idToOverwrite !== undefined) + .map(({ type, idToOverwrite }) => ({ type, id: idToOverwrite! })); + const nonUniqueRetryOverwrites = getNonUniqueEntries(overwriteEntries); + if (nonUniqueRetryOverwrites.length > 0) { + throw Boom.badRequest(`Non-unique retry overwrites: [${nonUniqueRetryOverwrites.join()}]`); + } +}; From 9e87a9aaaeb4d09d8e6375debcbf2c05f5942e6f Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 5 May 2020 18:22:14 -0400 Subject: [PATCH 08/55] Server-side support for importing multi-namespace saved objects Imports for single-namespace saved objects ensured that, at most, one "copy" of an object was made in any given space. Re-importing an object into a space would result in a conflict that could be overwritten. This commit ensures that multi-namespace saved objects can be imported, and that they behave in the same way. Primary changes: 1. Pre-flight searches to ensure that conflicts are made with the correct ID, and that ambiguous conflicts are detected. 2. Abstract out behavior of bulkCreate to handle the case of unresolvable conflicts, which shouldn't be exposed to consumers. 3. Support resolving ambiguous_conflict import errors by allowing consumers to select which conflicting object should be overwritten. --- src/core/public/index.ts | 2 + src/core/public/saved_objects/index.ts | 2 + src/core/server/index.ts | 2 + .../saved_objects/import/__mocks__/index.ts | 25 + .../import/check_conflicts.test.ts | 487 +++++++++++ .../saved_objects/import/check_conflicts.ts | 217 +++++ .../import/create_saved_objects.test.ts | 346 ++++++++ .../import/create_saved_objects.ts | 131 +++ .../import/extract_errors.test.ts | 21 +- .../saved_objects/import/extract_errors.ts | 9 +- .../import/import_saved_objects.test.ts | 725 ++++------------ .../import/import_saved_objects.ts | 43 +- src/core/server/saved_objects/import/index.ts | 2 + .../import/resolve_import_errors.test.ts | 776 ++++++------------ .../import/resolve_import_errors.ts | 48 +- src/core/server/saved_objects/import/types.ts | 29 + .../routes/resolve_import_errors.ts | 36 +- src/core/server/saved_objects/types.ts | 2 + .../lib/process_import_response.test.ts | 41 +- .../public/lib/process_import_response.ts | 8 +- .../lib/copy_to_spaces/copy_to_spaces.test.ts | 17 + .../lib/copy_to_spaces/copy_to_spaces.ts | 1 + .../resolve_copy_conflicts.test.ts | 17 + .../copy_to_spaces/resolve_copy_conflicts.ts | 10 +- .../spaces/server/lib/copy_to_spaces/types.ts | 9 +- .../routes/api/external/copy_to_space.test.ts | 24 + .../routes/api/external/copy_to_space.ts | 20 +- 27 files changed, 1838 insertions(+), 1212 deletions(-) create mode 100644 src/core/server/saved_objects/import/__mocks__/index.ts create mode 100644 src/core/server/saved_objects/import/check_conflicts.test.ts create mode 100644 src/core/server/saved_objects/import/check_conflicts.ts create mode 100644 src/core/server/saved_objects/import/create_saved_objects.test.ts create mode 100644 src/core/server/saved_objects/import/create_saved_objects.ts diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 3698fdcfe9512..12dfcb63e9971 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -143,7 +143,9 @@ export { SavedObjectsClient, SimpleSavedObject, SavedObjectsImportResponse, + SavedObjectsImportSuccess, SavedObjectsImportConflictError, + SavedObjectsImportAmbiguousConflictError, SavedObjectsImportUnsupportedTypeError, SavedObjectsImportMissingReferencesError, SavedObjectsImportUnknownError, diff --git a/src/core/public/saved_objects/index.ts b/src/core/public/saved_objects/index.ts index 13b4a12893666..e5e2d2c326af3 100644 --- a/src/core/public/saved_objects/index.ts +++ b/src/core/public/saved_objects/index.ts @@ -36,7 +36,9 @@ export { SavedObjectsFindOptions, SavedObjectsMigrationVersion, SavedObjectsImportResponse, + SavedObjectsImportSuccess, SavedObjectsImportConflictError, + SavedObjectsImportAmbiguousConflictError, SavedObjectsImportUnsupportedTypeError, SavedObjectsImportMissingReferencesError, SavedObjectsImportUnknownError, diff --git a/src/core/server/index.ts b/src/core/server/index.ts index cf999875b18f8..96574888a134d 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -219,11 +219,13 @@ export { SavedObjectsExportResultDetails, SavedObjectsFindResponse, SavedObjectsImportConflictError, + SavedObjectsImportAmbiguousConflictError, SavedObjectsImportError, SavedObjectsImportMissingReferencesError, SavedObjectsImportOptions, SavedObjectsImportResponse, SavedObjectsImportRetry, + SavedObjectsImportSuccess, SavedObjectsImportUnknownError, SavedObjectsImportUnsupportedTypeError, SavedObjectMigrationContext, diff --git a/src/core/server/saved_objects/import/__mocks__/index.ts b/src/core/server/saved_objects/import/__mocks__/index.ts new file mode 100644 index 0000000000000..e2c48ee483ce4 --- /dev/null +++ b/src/core/server/saved_objects/import/__mocks__/index.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +const mockUuidv4 = jest.fn().mockReturnValue('uuidv4'); +jest.mock('uuid', () => ({ + v4: mockUuidv4, +})); + +export { mockUuidv4 }; diff --git a/src/core/server/saved_objects/import/check_conflicts.test.ts b/src/core/server/saved_objects/import/check_conflicts.test.ts new file mode 100644 index 0000000000000..9481292b326ae --- /dev/null +++ b/src/core/server/saved_objects/import/check_conflicts.test.ts @@ -0,0 +1,487 @@ +/* + * 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 { + SavedObjectsClientContract, + SavedObjectReference, + SavedObject, + SavedObjectsImportRetry, + SavedObjectsImportError, +} from '../types'; +import { checkConflicts, getImportIdMapForRetries } from './check_conflicts'; +import { savedObjectsClientMock } from '../../mocks'; +import { typeRegistryMock } from '../saved_objects_type_registry.mock'; +import { ISavedObjectTypeRegistry } from '..'; + +type SavedObjectType = SavedObject<{ title?: string }>; +type CheckConflictsOptions = Parameters[1]; +type GetImportIdMapForRetriesOptions = Parameters[1]; + +/** + * Function to create a realistic-looking import object given a type, ID, and optional originId + */ +const createObject = (type: string, id: string, originId?: string): SavedObjectType => ({ + type, + id, + attributes: { title: `Title for ${type}:${id}` }, + references: (Symbol() as unknown) as SavedObjectReference[], + ...(originId && { originId }), +}); + +const MULTI_NS_TYPE = 'multi'; +const OTHER_TYPE = 'other'; + +describe('#checkConflicts', () => { + let savedObjectsClient: jest.Mocked; + let typeRegistry: jest.Mocked; + let find: typeof savedObjectsClient['find']; + + const getResultMock = (...objects: SavedObjectType[]) => ({ + page: 1, + per_page: 10, + total: objects.length, + saved_objects: objects, + }); + + const setupOptions = (namespace?: string): CheckConflictsOptions => { + savedObjectsClient = savedObjectsClientMock.create(); + find = savedObjectsClient.find; + find.mockResolvedValue(getResultMock()); // mock zero hits response by default + typeRegistry = typeRegistryMock.create(); + typeRegistry.isMultiNamespace.mockImplementation((type) => type === MULTI_NS_TYPE); + return { savedObjectsClient, typeRegistry, namespace }; + }; + + const mockFindResult = (...objects: SavedObjectType[]) => { + find.mockResolvedValueOnce(getResultMock(...objects)); + }; + + describe('cluster calls', () => { + const multiNsObj = createObject(MULTI_NS_TYPE, 'id-1'); + const multiNsObjWithOriginId = createObject(MULTI_NS_TYPE, 'id-2', 'originId-foo'); + const otherObj = createObject(OTHER_TYPE, 'id-3'); + // non-multi-namespace types shouldn't have origin IDs, but we include a test case to ensure it's handled gracefully + const otherObjWithOriginId = createObject(OTHER_TYPE, 'id-4', 'originId-bar'); + + const expectFindArgs = (n: number, object: SavedObject, rawIdPrefix: string) => { + const { type, id, originId } = object; + const search = `"${rawIdPrefix}${type}:${originId || id}" | "${originId || id}"`; // this template works for our basic test cases + const expectedOptions = expect.objectContaining({ type, search }); + // exclude rawSearchFields, page, perPage, and fields attributes from assertion -- these are constant + // exclude namespace from assertion -- a separate test covers that + expect(find).toHaveBeenNthCalledWith(n, expectedOptions); + }; + + test('does not execute searches for non-multi-namespace objects', async () => { + const objects = [otherObj, otherObjWithOriginId]; + const options = setupOptions(); + + await checkConflicts(objects, options); + expect(find).not.toHaveBeenCalled(); + }); + + test('executes searches for multi-namespace objects', async () => { + const objects = [multiNsObj, otherObj, multiNsObjWithOriginId, otherObjWithOriginId]; + const options1 = setupOptions(); + + await checkConflicts(objects, options1); + expect(find).toHaveBeenCalledTimes(2); + expectFindArgs(1, multiNsObj, ''); + expectFindArgs(2, multiNsObjWithOriginId, ''); + + find.mockClear(); + const options2 = setupOptions('some-namespace'); + await checkConflicts(objects, options2); + expect(find).toHaveBeenCalledTimes(2); + expectFindArgs(1, multiNsObj, 'some-namespace:'); + expectFindArgs(2, multiNsObjWithOriginId, 'some-namespace:'); + }); + + test('searches within the current `namespace`', async () => { + const objects = [multiNsObj]; + const namespace = 'some-namespace'; + const options = setupOptions(namespace); + + await checkConflicts(objects, options); + expect(find).toHaveBeenCalledTimes(1); + expect(find).toHaveBeenCalledWith(expect.objectContaining({ namespace })); + }); + + test('search query escapes quote and backslash characters in `id` and/or `originId`', async () => { + const weirdId = `some"weird\\id`; + const objects = [ + createObject(MULTI_NS_TYPE, weirdId), + createObject(MULTI_NS_TYPE, 'some-id', weirdId), + ]; + const options = setupOptions(); + + await checkConflicts(objects, options); + const escapedId = `some\\"weird\\\\id`; + const expectedQuery = `"${MULTI_NS_TYPE}:${escapedId}" | "${escapedId}"`; + expect(find).toHaveBeenCalledTimes(2); + expect(find).toHaveBeenNthCalledWith(1, expect.objectContaining({ search: expectedQuery })); + expect(find).toHaveBeenNthCalledWith(2, expect.objectContaining({ search: expectedQuery })); + }); + }); + + describe('results', () => { + const getAmbiguousConflicts = (objects: SavedObjectType[]) => + objects + .map(({ id, attributes, updated_at: updatedAt }) => ({ + id, + title: attributes?.title, + updatedAt, + })) + .sort((a: { id: string }, b: { id: string }) => (a.id > b.id ? 1 : b.id > a.id ? -1 : 0)); + const createAmbiguousConflictError = ( + object: SavedObjectType, + sources: SavedObjectType[], + destinations: SavedObjectType[] + ): SavedObjectsImportError => ({ + type: object.type, + id: object.id, + title: object.attributes?.title, + error: { + type: 'ambiguous_conflict', + sources: getAmbiguousConflicts(sources), + destinations: getAmbiguousConflicts(destinations), + }, + }); + + describe('object result without a `importIdMap` entry (no match or exact match)', () => { + test('returns object when no match is detected (0 hits)', async () => { + // no objects exist in this space + // try to import obj1, obj2, obj3, and obj4 + const obj1 = createObject(OTHER_TYPE, 'id-1'); // non-multi-namespace types are skipped when searching, so they will never have a match anyway + const obj2 = createObject(OTHER_TYPE, 'id-2', 'originId-foo'); // non-multi-namespace types are skipped when searching, so they will never have a match anyway + const obj3 = createObject(MULTI_NS_TYPE, 'id-3'); + const obj4 = createObject(MULTI_NS_TYPE, 'id-4', 'originId-bar'); + const options = setupOptions(); + + // don't need to mock find results for obj3 and obj4, "no match" is the default find result in this test suite + const checkConflictsResult = await checkConflicts([obj1, obj2, obj3, obj4], options); + + const expectedResult = { + filteredObjects: [obj1, obj2, obj3, obj4], + importIdMap: new Map(), + errors: [], + }; + expect(checkConflictsResult).toEqual(expectedResult); + }); + + test('returns object when an exact match is detected (1 hit)', async () => { + // obj1 and obj2 exist in this space + // try to import obj2 and obj2 + const obj1 = createObject(MULTI_NS_TYPE, 'id-1'); + const obj2 = createObject(MULTI_NS_TYPE, 'id-2', 'originId-foo'); + const options = setupOptions(); + mockFindResult(obj1); // find for obj1: the result is an exact match + mockFindResult(obj2); // find for obj2: the result is an exact match + + const checkConflictsResult = await checkConflicts([obj1, obj2], options); + const expectedResult = { + filteredObjects: [obj1, obj2], + importIdMap: new Map(), + errors: [], + }; + expect(checkConflictsResult).toEqual(expectedResult); + }); + + test('returns object when an exact match is detected (2+ hits)', async () => { + // obj1, obj2, objA, and objB exist in this space + // try to import obj1 and obj2 + const obj1 = createObject(MULTI_NS_TYPE, 'id-1'); + const objA = createObject(MULTI_NS_TYPE, 'id-3', obj1.id); + const obj2 = createObject(MULTI_NS_TYPE, 'id-2', 'originId-foo'); + const objB = createObject(MULTI_NS_TYPE, 'id-4', obj2.originId); + const options = setupOptions(); + mockFindResult(obj1, objA); // find for obj1: the first result is an exact match, so the second result is ignored + mockFindResult(objB, obj2); // find for obj2: the second result is an exact match, so the first result is ignored + + const checkConflictsResult = await checkConflicts([obj1, obj2], options); + const expectedResult = { + filteredObjects: [obj1, obj2], + importIdMap: new Map(), + errors: [], + }; + expect(checkConflictsResult).toEqual(expectedResult); + }); + + test('returns object when an inexact match is detected (1 hit) with a destination that is exactly matched by another object', async () => { + // obj1 and obj3 exist in this space + // try to import obj1, obj2, obj3, and obj4 + // note: this test is only concerned with obj2 and obj4, but obj1 and obj3 must be included to exercise this code path + const obj1 = createObject(MULTI_NS_TYPE, 'id-1'); + const obj2 = createObject(MULTI_NS_TYPE, 'id-2', obj1.id); + const obj3 = createObject(MULTI_NS_TYPE, 'id-3', 'originId-foo'); + const obj4 = createObject(MULTI_NS_TYPE, 'id-4', obj3.originId); + const options = setupOptions(); + mockFindResult(obj1); // find for obj1: the result is an exact match + mockFindResult(obj1); // find for obj2: the result is an inexact match with one destination that is exactly matched by obj1 so it is ignored -- accordingly, obj2 has no match + mockFindResult(obj3); // find for obj3: the result is an exact match + mockFindResult(obj3); // find for obj4: the result is an inexact match with one destination that is exactly matched by obj3 so it is ignored -- accordingly, obj4 has no match + + const checkConflictsResult = await checkConflicts([obj1, obj2, obj3, obj4], options); + const expectedResult = { + filteredObjects: [obj1, obj2, obj3, obj4], + importIdMap: new Map(), + errors: [], + }; + expect(checkConflictsResult).toEqual(expectedResult); + }); + + test('returns object when an inexact match is detected (2+ hits) with destinations that are all exactly matched by another object', async () => { + // obj1 and obj2 exist in this space + // try to import obj1, obj2, and obj3 + const obj1 = createObject(MULTI_NS_TYPE, 'id-1'); + const obj2 = createObject(MULTI_NS_TYPE, 'id-2', obj1.id); + const obj3 = createObject(MULTI_NS_TYPE, 'id-3', obj1.id); + const options = setupOptions(); + mockFindResult(obj1, obj2); // find for obj1: the first result is an exact match, so the second result is ignored + mockFindResult(obj1, obj2); // find for obj2: the second result is an exact match, so the first result is ignored + mockFindResult(obj1, obj2); // find for obj3: the result is an inexact match with two destinations that are exactly matched by obj1 and obj2 so they are ignored -- accordingly, obj3 has no match + + const checkConflictsResult = await checkConflicts([obj1, obj2, obj3], options); + const expectedResult = { + filteredObjects: [obj1, obj2, obj3], + importIdMap: new Map(), + errors: [], + }; + expect(checkConflictsResult).toEqual(expectedResult); + }); + }); + + describe('object result with a `importIdMap` entry (partial match with a single destination)', () => { + test('returns object with a `importIdMap` entry when an inexact match is detected (1 hit)', async () => { + // objA and objB exist in this space + // try to import obj1 and obj2 + const obj1 = createObject(MULTI_NS_TYPE, 'id-1'); + const objA = createObject(MULTI_NS_TYPE, 'id-A', obj1.id); + const obj2 = createObject(MULTI_NS_TYPE, 'id-2', 'originId-foo'); + const objB = createObject(MULTI_NS_TYPE, 'id-B', obj2.originId); + const options = setupOptions(); + mockFindResult(objA); // find for obj1: the result is an inexact match with one destination + mockFindResult(objB); // find for obj2: the result is an inexact match with one destination + + const checkConflictsResult = await checkConflicts([obj1, obj2], options); + const expectedResult = { + filteredObjects: [obj1, obj2], + importIdMap: new Map([ + [`${obj1.type}:${obj1.id}`, { id: objA.id }], + [`${obj2.type}:${obj2.id}`, { id: objB.id }], + ]), + errors: [], + }; + expect(checkConflictsResult).toEqual(expectedResult); + }); + + test('returns object with a `importIdMap` entry when an inexact match is detected (2+ hits), with n-1 destinations that are exactly matched by another object', async () => { + // obj1, obj3, objA, and objB exist in this space + // try to import obj1, obj2, obj3, and obj4 + // note: this test is only concerned with obj2 and obj4, but obj1 and obj3 must be included to exercise this code path + const obj1 = createObject(MULTI_NS_TYPE, 'id-1'); + const obj2 = createObject(MULTI_NS_TYPE, 'id-2', obj1.id); + const objA = createObject(MULTI_NS_TYPE, 'id-A', obj1.id); + const obj3 = createObject(MULTI_NS_TYPE, 'id-3', 'originId-foo'); + const obj4 = createObject(MULTI_NS_TYPE, 'id-4', obj3.originId); + const objB = createObject(MULTI_NS_TYPE, 'id-B', obj3.originId); + const options = setupOptions(); + mockFindResult(obj1, objA); // find for obj1: the first result is an exact match, so the second result is ignored + mockFindResult(obj1, objA); // find for obj2: the result is an inexact match with two destinations, but the first destination is exactly matched by obj1 so it is ignored -- accordingly, obj2 has an inexact match with one destination (objA) + mockFindResult(objB, obj3); // find for obj3: the second result is an exact match, so the first result is ignored + mockFindResult(objB, obj3); // find for obj4: the result is an inexact match with two destinations, but the second destination is exactly matched by obj3 so it is ignored -- accordingly, obj4 has an inexact match with one destination (objB) + + const checkConflictsResult = await checkConflicts([obj1, obj2, obj3, obj4], options); + const expectedResult = { + filteredObjects: [obj1, obj2, obj3, obj4], + importIdMap: new Map([ + [`${obj2.type}:${obj2.id}`, { id: objA.id }], + [`${obj4.type}:${obj4.id}`, { id: objB.id }], + ]), + errors: [], + }; + expect(checkConflictsResult).toEqual(expectedResult); + }); + }); + + describe('error result (ambiguous conflict)', () => { + test('returns ambiguous_conflict error when multiple inexact matches are detected that target the same single destination', async () => { + // objA and objB exist in this space + // try to import obj1, obj2, obj3, and obj4 + const obj1 = createObject(MULTI_NS_TYPE, 'id-1'); + const obj2 = createObject(MULTI_NS_TYPE, 'id-2', obj1.id); + const objA = createObject(MULTI_NS_TYPE, 'id-A', obj1.id); + const obj3 = createObject(MULTI_NS_TYPE, 'id-3', 'originId-foo'); + const obj4 = createObject(MULTI_NS_TYPE, 'id-4', obj3.originId); + const objB = createObject(MULTI_NS_TYPE, 'id-B', obj3.originId); + const options = setupOptions(); + mockFindResult(objA); // find for obj1: the result is an inexact match with one destination + mockFindResult(objA); // find for obj2: the result is an inexact match with one destination + mockFindResult(objB); // find for obj3: the result is an inexact match with one destination + mockFindResult(objB); // find for obj4: the result is an inexact match with one destination + + const checkConflictsResult = await checkConflicts([obj1, obj2, obj3, obj4], options); + const expectedResult = { + filteredObjects: [], + importIdMap: new Map(), + errors: [ + createAmbiguousConflictError(obj1, [obj1, obj2], [objA]), + createAmbiguousConflictError(obj2, [obj1, obj2], [objA]), + createAmbiguousConflictError(obj3, [obj3, obj4], [objB]), + createAmbiguousConflictError(obj4, [obj3, obj4], [objB]), + ], + }; + expect(checkConflictsResult).toEqual(expectedResult); + }); + + test('returns ambiguous_conflict error when an inexact match is detected (2+ hits)', async () => { + // objA, objB, objC, and objD exist in this space + // try to import obj1 and obj2 + const obj1 = createObject(MULTI_NS_TYPE, 'id-1'); + const obj2 = createObject(MULTI_NS_TYPE, 'id-2', 'originId-foo'); + const objA = createObject(MULTI_NS_TYPE, 'id-A', obj1.id); + const objB = createObject(MULTI_NS_TYPE, 'id-B', obj1.id); + const objC = createObject(MULTI_NS_TYPE, 'id-C', obj2.originId); + const objD = createObject(MULTI_NS_TYPE, 'id-D', obj2.originId); + const options = setupOptions(); + mockFindResult(objA, objB); // find for obj1: the result is an inexact match with two destinations + mockFindResult(objC, objD); // find for obj2: the result is an inexact match with two destinations + + const checkConflictsResult = await checkConflicts([obj1, obj2], options); + const expectedResult = { + filteredObjects: [], + importIdMap: new Map(), + errors: [ + createAmbiguousConflictError(obj1, [obj1], [objA, objB]), + createAmbiguousConflictError(obj2, [obj2], [objC, objD]), + ], + }; + expect(checkConflictsResult).toEqual(expectedResult); + }); + + test('returns ambiguous_conflict error when multiple inexact matches are detected that target the same multiple destinations', async () => { + // objA, objB, objC, and objD exist in this space + // try to import obj1, obj2, obj3, and obj4 + const obj1 = createObject(MULTI_NS_TYPE, 'id-1'); + const obj2 = createObject(MULTI_NS_TYPE, 'id-2', obj1.id); + const obj3 = createObject(MULTI_NS_TYPE, 'id-3', 'originId-foo'); + const obj4 = createObject(MULTI_NS_TYPE, 'id-4', obj3.originId); + const objA = createObject(MULTI_NS_TYPE, 'id-A', obj1.id); + const objB = createObject(MULTI_NS_TYPE, 'id-B', obj1.id); + const objC = createObject(MULTI_NS_TYPE, 'id-C', obj3.originId); + const objD = createObject(MULTI_NS_TYPE, 'id-D', obj3.originId); + const options = setupOptions(); + mockFindResult(objA, objB); // find for obj1: the result is an inexact match with two destinations + mockFindResult(objA, objB); // find for obj2: the result is an inexact match with two destinations + mockFindResult(objC, objD); // find for obj3: the result is an inexact match with two destinations + mockFindResult(objC, objD); // find for obj4: the result is an inexact match with two destinations + + const checkConflictsResult = await checkConflicts([obj1, obj2, obj3, obj4], options); + const expectedResult = { + filteredObjects: [], + importIdMap: new Map(), + errors: [ + createAmbiguousConflictError(obj1, [obj1, obj2], [objA, objB]), + createAmbiguousConflictError(obj2, [obj1, obj2], [objA, objB]), + createAmbiguousConflictError(obj3, [obj3, obj4], [objC, objD]), + createAmbiguousConflictError(obj4, [obj3, obj4], [objC, objD]), + ], + }; + expect(checkConflictsResult).toEqual(expectedResult); + }); + }); + + test('returns mixed results', async () => { + // obj3, objA, obB, and objC exist in this space + // try to import obj1, obj2, obj3, obj4, obj5, obj6, and obj7 + // note: this test is non-exhaustive for different permutations of import objects and results, but prior tests exercise these more thoroughly + const obj1 = createObject(OTHER_TYPE, 'id-1'); + const obj2 = createObject(MULTI_NS_TYPE, 'id-2'); + const obj3 = createObject(MULTI_NS_TYPE, 'id-3'); + const obj4 = createObject(MULTI_NS_TYPE, 'id-4', obj3.id); + const obj5 = createObject(MULTI_NS_TYPE, 'id-5'); + const obj6 = createObject(MULTI_NS_TYPE, 'id-6'); + const obj7 = createObject(MULTI_NS_TYPE, 'id-7', obj6.id); + const objA = createObject(MULTI_NS_TYPE, 'id-A', obj5.id); + const objB = createObject(MULTI_NS_TYPE, 'id-B', obj6.id); + const objC = createObject(MULTI_NS_TYPE, 'id-C', obj6.id); + const options = setupOptions(); + // obj1 is a non-multi-namespace type, so it is skipped while searching + mockFindResult(); // find for obj2: the result is no match + mockFindResult(obj3); // find for obj3: the result is an exact match + mockFindResult(obj3); // find for obj4: the result is an inexact match with one destination that is exactly matched by obj3 so it is ignored -- accordingly, obj4 has no match + mockFindResult(objA); // find for obj5: the result is an inexact match with one destination + mockFindResult(objB, objC); // find for obj6: the result is an inexact match with two destinations + mockFindResult(objB, objC); // find for obj7: the result is an inexact match with two destinations + + const objects = [obj1, obj2, obj3, obj4, obj5, obj6, obj7]; + const checkConflictsResult = await checkConflicts(objects, options); + const expectedResult = { + filteredObjects: [obj1, obj2, obj3, obj4, obj5], + importIdMap: new Map([[`${obj5.type}:${obj5.id}`, { id: objA.id }]]), + errors: [ + createAmbiguousConflictError(obj6, [obj6, obj7], [objB, objC]), + createAmbiguousConflictError(obj7, [obj6, obj7], [objB, objC]), + ], + }; + expect(checkConflictsResult).toEqual(expectedResult); + }); + }); +}); + +describe('#getImportIdMapForRetries', () => { + let typeRegistry: jest.Mocked; + + const setupOptions = (retries: SavedObjectsImportRetry[]): GetImportIdMapForRetriesOptions => { + typeRegistry = typeRegistryMock.create(); + typeRegistry.isMultiNamespace.mockImplementation((type) => type === MULTI_NS_TYPE); + return { typeRegistry, retries }; + }; + + const createOverwriteRetry = ( + { type, id }: { type: string; id: string }, + idToOverwrite?: string + ): SavedObjectsImportRetry => { + return { type, id, overwrite: true, idToOverwrite, replaceReferences: [] }; + }; + + test('returns expected results', async () => { + const obj1 = createObject(OTHER_TYPE, 'id-1'); + const obj2 = createObject(OTHER_TYPE, 'id-2'); + const obj3 = createObject(OTHER_TYPE, 'id-3'); + const obj4 = createObject(MULTI_NS_TYPE, 'id-4'); + const obj5 = createObject(MULTI_NS_TYPE, 'id-5'); + const obj6 = createObject(MULTI_NS_TYPE, 'id-6'); + const objects = [obj1, obj2, obj3, obj4, obj5, obj6]; + const retries = [ + // all three overwrite retries for non-multi-namespace types are ignored; + // retries for non-multi-namespace these should not have `idToOverwrite` specified, but we test it here for posterity + createOverwriteRetry(obj1), + createOverwriteRetry(obj2, obj2.id), + createOverwriteRetry(obj3, 'id-X'), + createOverwriteRetry(obj4), // retries that do not have `idToOverwrite` specified are ignored + createOverwriteRetry(obj5, obj5.id), // retries that have `id` that matches `idToOverwrite` are ignored + createOverwriteRetry(obj6, 'id-Y'), // this retry will get added to the `importIdMap`! + ]; + const options = setupOptions(retries); + + const checkConflictsResult = await getImportIdMapForRetries(objects, options); + expect(checkConflictsResult).toEqual(new Map([[`${obj6.type}:${obj6.id}`, { id: 'id-Y' }]])); + }); +}); diff --git a/src/core/server/saved_objects/import/check_conflicts.ts b/src/core/server/saved_objects/import/check_conflicts.ts new file mode 100644 index 0000000000000..26f9795a46e80 --- /dev/null +++ b/src/core/server/saved_objects/import/check_conflicts.ts @@ -0,0 +1,217 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +import { + SavedObject, + SavedObjectsClientContract, + SavedObjectsImportError, + SavedObjectsImportRetry, +} from '../types'; +import { ISavedObjectTypeRegistry } from '..'; + +interface CheckConflictsOptions { + savedObjectsClient: SavedObjectsClientContract; + typeRegistry: ISavedObjectTypeRegistry; + namespace?: string; +} + +interface GetImportIdMapForRetriesOptions { + typeRegistry: ISavedObjectTypeRegistry; + retries: SavedObjectsImportRetry[]; +} + +type InexactMatch = { + object: SavedObject; + destinations: Array<{ id: string; title?: string; updatedAt?: string }>; +}; +type Left = { tag: 'left'; value: InexactMatch }; +type Right = { tag: 'right'; value: SavedObject }; +type Either = Left | Right; +const isLeft = (object: Either): object is Left => object.tag === 'left'; + +const createQueryTerm = (input: string) => input.replace(/\\/g, '\\\\').replace(/\"/g, '\\"'); +const createQuery = (type: string, id: string, rawIdPrefix: string) => + `"${createQueryTerm(`${rawIdPrefix}${type}:${id}`)}" | "${createQueryTerm(id)}"`; +const getAmbiguousConflicts = (objects: Array>) => + objects + .map(({ id, attributes, updated_at: updatedAt }) => ({ + id, + title: attributes?.title, + updatedAt, + })) + .sort((a, b) => (a.id > b.id ? 1 : b.id > a.id ? -1 : 0)); +const getAmbiguousConflictSourceKey = ({ object }: InexactMatch) => + `${object.type}:${object.originId || object.id}`; + +/** + * Make a search request for an import object to check if any objects of this type that match this object's `originId` or `id` exist in the + * specified namespace: + * - A `Right` result indicates that no conflict destinations were found in this namespace ("no match"), *OR* an exact match for this + * object's ID exists in this namespace ("exact match"). + * - A `Left` result indicates that one or more conflict destinations exist in this namespace, none of which exactly match this object's ID + * ("inexact match"). + */ +const checkConflict = async ( + object: SavedObject<{ title?: string }>, + importIds: Set, + options: CheckConflictsOptions +): Promise> => { + const { savedObjectsClient, typeRegistry, namespace } = options; + const { type, originId } = object; + + if (!typeRegistry.isMultiNamespace(type)) { + // Skip the search request for non-multi-namespace types, since by definition they cannot have inexact matches or ambiguous conflicts. + return { tag: 'right', value: object }; + } + + const search = createQuery(type, originId || object.id, namespace ? `${namespace}:` : ''); + const findOptions = { + type, + search, + rawSearchFields: ['_id', 'originId'], + page: 1, + perPage: 10, + fields: ['title'], + namespace, + }; + const findResult = await savedObjectsClient.find<{ title?: string }>(findOptions); + const { total, saved_objects: savedObjects } = findResult; + if (total === 0 || savedObjects.some(({ id }) => id === object.id)) { + return { tag: 'right', value: object }; + } + // This is an "inexact match" so far; filter the conflict destination(s) to exclude any that exactly match other objects we are importing. + const objects = savedObjects.filter((obj) => !importIds.has(`${obj.type}:${obj.id}`)); + const destinations = getAmbiguousConflicts(objects); + if (destinations.length === 0) { + // No conflict destinations remain after filtering, so this is a "no match" result. + return { tag: 'right', value: object }; + } + return { tag: 'left', value: { object, destinations } }; +}; + +/** + * This function takes all objects to import, and checks "multi-namespace" types for potential conflicts. An object with a multi-namespace + * type may include an `originId` field, which means that it should conflict with other objects that originate from the same source. + * Expected behavior of importing saved objects (single-namespace or multi-namespace): + * 1. The object 'foo' is exported from space A and imported to space B -- a new object 'bar' is created. + * 2. Then, the object 'bar' is exported from space B and imported to space C -- a new object 'baz' is created. + * 3. Then, the object 'baz' is exported from space C to space A -- the object conflicts with 'foo', which must be overwritten to continue. + * This behavior originated with "single-namespace" types, and this function was added to ensure importing objects of multi-namespace types + * will behave in the same way. + * + * To achieve this behavior for multi-namespace types, a search request is made for each object to determine if any objects of this type + * that match this object's `originId` or `id` exist in the specified namespace: + * - If this is a `Right` result; return the import object and allow `createSavedObjects` to handle the conflict (if any). + * - If this is a `Left` "partial match" result: + * A. If there is a single source and destination match, add the destination to the importIdMap and return the import object, which + * will allow `createSavedObjects` to modify the ID before creating the object (thus ensuring a conflict during). + * B. Otherwise, this is an "ambiguous conflict" result; return an error. + */ +export async function checkConflicts( + objects: Array>, + options: CheckConflictsOptions +) { + // Check each object for possible destination conflicts. + const importIds = new Set(objects.map(({ type, id }) => `${type}:${id}`)); + const checkConflictResults = await Promise.all( + objects.map((object) => checkConflict(object, importIds, options)) + ); + + // Get a map of all inexact matches that share the same destination(s). + const ambiguousConflictSourcesMap = checkConflictResults.filter(isLeft).reduce((acc, cur) => { + const key = getAmbiguousConflictSourceKey(cur.value); + const value = acc.get(key) ?? []; + return acc.set(key, [...value, cur.value.object]); + }, new Map>>()); + + const errors: SavedObjectsImportError[] = []; + const filteredObjects: Array> = []; + const importIdMap = new Map(); + checkConflictResults.forEach((result) => { + if (!isLeft(result)) { + filteredObjects.push(result.value); + return; + } + const key = getAmbiguousConflictSourceKey(result.value); + const sources = getAmbiguousConflicts(ambiguousConflictSourcesMap.get(key)!); + const { object, destinations } = result.value; + const { type, id, attributes } = object; + if (sources.length === 1 && destinations.length === 1) { + // This is a simple "inexact match" result -- a single import object has a single destination conflict. + importIdMap.set(`${type}:${id}`, { id: destinations[0].id }); + filteredObjects.push(object); + return; + } + // This is an ambiguous conflict error, which is one of the following cases: + // - a single import object has 2+ destination conflicts ("ambiguous destination") + // - 2+ import objects have the same single destination conflict ("ambiguous source") + // - 2+ import objects have the same 2+ destination conflicts ("ambiguous source and destination") + errors.push({ + type, + id, + title: attributes?.title, + error: { + type: 'ambiguous_conflict', + sources, + destinations, + }, + }); + }); + + return { + errors, + filteredObjects, + importIdMap, + }; +} + +/** + * Assume that all objects exist in the `retries` map (due to filtering at the beginnning of `resolveSavedObjectsImportErrors`). + */ +export async function getImportIdMapForRetries( + objects: Array>, + options: GetImportIdMapForRetriesOptions +) { + const { typeRegistry, retries } = options; + + const retryMap = retries.reduce( + (acc, cur) => acc.set(`${cur.type}:${cur.id}`, cur), + new Map() + ); + const importIdMap = new Map(); + + objects.forEach(({ type, id }) => { + const retry = retryMap.get(`${type}:${id}`); + if (retry) { + const { overwrite, idToOverwrite } = retry; + if ( + overwrite && + idToOverwrite && + idToOverwrite !== id && + typeRegistry.isMultiNamespace(type) + ) { + importIdMap.set(`${type}:${id}`, { id: idToOverwrite }); + } + } + }); + + return importIdMap; +} diff --git a/src/core/server/saved_objects/import/create_saved_objects.test.ts b/src/core/server/saved_objects/import/create_saved_objects.test.ts new file mode 100644 index 0000000000000..b1129ec574270 --- /dev/null +++ b/src/core/server/saved_objects/import/create_saved_objects.test.ts @@ -0,0 +1,346 @@ +/* + * 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 { mockUuidv4 } from './__mocks__'; +import { savedObjectsClientMock } from '../../mocks'; +import { createSavedObjects } from './create_saved_objects'; +import { SavedObjectReference } from 'kibana/public'; +import { SavedObjectsClientContract, SavedObject } from '../types'; +import { SavedObjectsErrorHelpers } from '..'; +import { extractErrors } from './extract_errors'; + +type CreateSavedObjectsOptions = Parameters[1]; + +/** + * Function to create a realistic-looking import object given a type, ID, and optional originId + */ +const createObject = (type: string, id: string, originId?: string): SavedObject => ({ + type, + id, + attributes: {}, + references: (Symbol() as unknown) as SavedObjectReference[], + ...(originId && { originId }), +}); + +const MULTI_NS_TYPE = 'multi'; +const OTHER_TYPE = 'other'; +/** + * Create a variety of different objects to exercise different import / result scenarios + */ +const obj1 = createObject(MULTI_NS_TYPE, 'id-1', 'originId-a'); // -> success +const obj2 = createObject(MULTI_NS_TYPE, 'id-2', 'originId-b'); // -> conflict +const obj3 = createObject(MULTI_NS_TYPE, 'id-3', 'originId-c'); // -> conflict (with known importId) +const obj4 = createObject(MULTI_NS_TYPE, 'id-4', 'originId-d'); // -> conflict (with known importId) +const obj5 = createObject(MULTI_NS_TYPE, 'id-5', 'originId-e'); // -> unresolvable conflict +const obj6 = createObject(MULTI_NS_TYPE, 'id-6'); // -> success +const obj7 = createObject(MULTI_NS_TYPE, 'id-7'); // -> conflict +const obj8 = createObject(MULTI_NS_TYPE, 'id-8'); // -> conflict (with known importId) +const obj9 = createObject(MULTI_NS_TYPE, 'id-9'); // -> unresolvable conflict +const obj10 = createObject(OTHER_TYPE, 'id-10', 'originId-f'); // -> success +const obj11 = createObject(OTHER_TYPE, 'id-11', 'originId-g'); // -> conflict +const obj12 = createObject(OTHER_TYPE, 'id-12'); // -> success +const obj13 = createObject(OTHER_TYPE, 'id-13'); // -> conflict +// non-multi-namespace types shouldn't have origin IDs, but we include test cases to ensure it's handled gracefully +// non-multi-namespace types by definition cannot result in an unresolvable conflict, so we don't include test cases for those +const importId3 = 'id-foo'; +const importId4 = 'id-bar'; +const importId8 = 'id-baz'; +const importIdMap = new Map([ + [`${obj3.type}:${obj3.id}`, { id: importId3 }], + [`${obj4.type}:${obj4.id}`, { id: importId4 }], + [`${obj8.type}:${obj8.id}`, { id: importId8 }], +]); + +describe('#createSavedObjects', () => { + let savedObjectsClient: jest.Mocked; + let bulkCreate: typeof savedObjectsClient['bulkCreate']; + + /** + * Creates an options object to be used as an argument for createSavedObjects + * Includes mock savedObjectsClient + */ + const setupOptions = ( + options: { + namespace?: string; + overwrite?: boolean; + } = {} + ): CreateSavedObjectsOptions => { + const { namespace, overwrite } = options; + savedObjectsClient = savedObjectsClientMock.create(); + bulkCreate = savedObjectsClient.bulkCreate; + return { savedObjectsClient, importIdMap, namespace, overwrite }; + }; + + const getExpectedBulkCreateArgsObjects = (objects: SavedObject[], retry?: boolean) => + objects.map(({ type, id, attributes, references, originId }) => ({ + type, + id: retry ? `new-id-for-${id}` : id, // if this was a retry, we regenerated the id -- this is mocked below + attributes, + references, + // if the import object had an originId, and/or if we regenerated the id, expect an originId to be included in the create args + ...((originId || retry) && { originId: originId || id }), + })); + + const expectBulkCreateArgs = { + objects: (n: number, objects: SavedObject[], retry?: boolean) => { + const expectedObjects = getExpectedBulkCreateArgsObjects(objects, retry); + const expectedOptions = expect.any(Object); + expect(bulkCreate).toHaveBeenNthCalledWith(n, expectedObjects, expectedOptions); + }, + options: (n: number, options: CreateSavedObjectsOptions) => { + const expectedObjects = expect.any(Array); + const expectedOptions = { namespace: options.namespace, overwrite: options.overwrite }; + expect(bulkCreate).toHaveBeenNthCalledWith(n, expectedObjects, expectedOptions); + }, + }; + + const getResultMock = { + success: ( + { type, id, attributes, references, originId }: SavedObject, + { namespace }: CreateSavedObjectsOptions + ): SavedObject => ({ + type, + id, + attributes, + references, + ...(originId && { originId }), + version: 'some-version', + updated_at: 'some-date', + namespaces: [namespace ?? 'default'], + }), + conflict: (type: string, id: string) => { + const error = SavedObjectsErrorHelpers.createConflictError(type, id).output.payload; + return ({ type, id, error } as unknown) as SavedObject; + }, + unresolvableConflict: (type: string, id: string) => { + const conflictMock = getResultMock.conflict(type, id); + conflictMock.error!.metadata = { isNotOverwritable: true }; + return conflictMock; + }, + }; + + /** + * Remap the bulkCreate results to ensure that each returned object reflects the ID of the imported object. + * This is needed because createSavedObjects may change the ID of the object to create, but this process is opaque to consumers of the + * API; we have to remap IDs of results so consumers can act upon them, as there is no guarantee that results will be returned in the same + * order as they were imported in. + * For the purposes of this test suite, the objects ARE guaranteed to be in the same order, so we do a simple loop to remap the IDs. + * In addition, extract the errors out of the created objects -- since we are testing with realistic objects/errors, we can use the real + * `extractErrors` module to do so. + */ + const getExpectedResults = (resultObjects: SavedObject[], objects: SavedObject[]) => { + const remappedResults = resultObjects.map((result, i) => ({ ...result, id: objects[i].id })); + return { + createdObjects: remappedResults.filter((obj) => !obj.error), + errors: extractErrors(remappedResults, objects), + }; + }; + + test('exits early if there are no objects to create', async () => { + const options = setupOptions(); + + const createSavedObjectsResult = await createSavedObjects([], options); + expect(bulkCreate).not.toHaveBeenCalled(); + expect(createSavedObjectsResult).toEqual({ createdObjects: [], errors: [] }); + }); + + describe('import without retries', () => { + const objs = [obj1, obj2, obj3, obj4, obj6, obj7, obj8, obj10, obj11, obj12, obj13]; + + const setupMockResults = (options: CreateSavedObjectsOptions) => { + bulkCreate.mockResolvedValue({ + saved_objects: [ + getResultMock.success(obj1, options), + getResultMock.conflict(obj2.type, obj2.id), + getResultMock.conflict(obj3.type, importId3), + getResultMock.conflict(obj4.type, importId4), + // skip obj5, we aren't testing an unresolvable conflict here + getResultMock.success(obj6, options), + getResultMock.conflict(obj7.type, obj7.id), + getResultMock.conflict(obj8.type, importId8), + // skip obj9, we aren't testing an unresolvable conflict here + getResultMock.success(obj10, options), + getResultMock.conflict(obj11.type, obj11.id), + getResultMock.success(obj12, options), + getResultMock.conflict(obj13.type, obj13.id), + ], + }); + }; + + const testBulkCreateObjects = async (namespace?: string) => { + const options = setupOptions({ namespace }); + setupMockResults(options); + + await createSavedObjects(objs, options); + expect(bulkCreate).toHaveBeenCalledTimes(1); + // these three objects are transformed before being created, because they are included in the `importIdMap` + const x3 = { ...obj3, id: importId3 }; // this import object already has an originId + const x4 = { ...obj4, id: importId4 }; // this import object already has an originId + const x8 = { ...obj8, id: importId8, originId: obj8.id }; // this import object doesn't have an originId, so it is set before create + const argObjs = [obj1, obj2, x3, x4, obj6, obj7, x8, obj10, obj11, obj12, obj13]; + expectBulkCreateArgs.objects(1, argObjs); + }; + const testBulkCreateOptions = async (namespace?: string) => { + const overwrite = (Symbol() as unknown) as boolean; + const options = setupOptions({ namespace, overwrite }); + setupMockResults(options); + + await createSavedObjects(objs, options); + expect(bulkCreate).toHaveBeenCalledTimes(1); + expectBulkCreateArgs.options(1, options); + }; + const testReturnValue = async (namespace?: string) => { + const options = setupOptions({ namespace }); + setupMockResults(options); + + const results = await createSavedObjects(objs, options); + const resultSavedObjects = (await bulkCreate.mock.results[0].value).saved_objects; + const [r1, r2, r3, r4, r6, r7, r8, r10, r11, r12, r13] = resultSavedObjects; + // these three results are transformed before being returned, because the bulkCreate attempt used different IDs for them + const [x3, x4, x8] = [r3, r4, r8].map((x: SavedObject) => ({ ...x, newId: x.id })); + const transformedResults = [r1, r2, x3, x4, r6, r7, x8, r10, r11, r12, r13]; + const expectedResults = getExpectedResults(transformedResults, objs); + expect(results).toEqual(expectedResults); + }; + + describe('with an undefined namespace', () => { + test('calls bulkCreate once with input objects', async () => { + await testBulkCreateObjects(); + }); + test('calls bulkCreate once with input options', async () => { + await testBulkCreateOptions(); + }); + test('returns bulkCreate results that are remapped to IDs of imported objects', async () => { + await testReturnValue(); + }); + }); + + describe('with a defined namespace', () => { + const namespace = 'some-namespace'; + test('calls bulkCreate once with input objects', async () => { + await testBulkCreateObjects(namespace); + }); + test('calls bulkCreate once with input options', async () => { + await testBulkCreateOptions(namespace); + }); + test('returns bulkCreate results that are remapped to IDs of imported objects', async () => { + await testReturnValue(namespace); + }); + }); + }); + + describe('import with retries', () => { + const objs = [obj1, obj2, obj3, obj4, obj5, obj6, obj7, obj8, obj9, obj10, obj11, obj12, obj13]; + + const setupMockResults = (options: CreateSavedObjectsOptions) => { + bulkCreate.mockResolvedValueOnce({ + saved_objects: [ + getResultMock.success(obj1, options), + getResultMock.conflict(obj2.type, obj2.id), + getResultMock.conflict(obj3.type, importId3), + getResultMock.conflict(obj4.type, importId4), + getResultMock.unresolvableConflict(obj5.type, obj5.id), // unresolvable conflict will cause a retry + getResultMock.success(obj6, options), + getResultMock.conflict(obj7.type, obj7.id), + getResultMock.conflict(obj8.type, importId8), + getResultMock.unresolvableConflict(obj9.type, obj9.id), // unresolvable conflict will cause a retry + getResultMock.success(obj10, options), + getResultMock.conflict(obj11.type, obj11.id), + getResultMock.success(obj12, options), + getResultMock.conflict(obj13.type, obj13.id), + ], + }); + mockUuidv4.mockReturnValueOnce(`new-id-for-${obj5.id}`); + mockUuidv4.mockReturnValueOnce(`new-id-for-${obj9.id}`); + bulkCreate.mockResolvedValue({ + saved_objects: [ + getResultMock.success({ ...obj5, id: `new-id-for-${obj5.id}` }, options), // retry is a success + getResultMock.success({ ...obj9, id: `new-id-for-${obj9.id}` }, options), // retry is a success + ], + }); + }; + + const testBulkCreateObjects = async (namespace?: string) => { + const options = setupOptions({ namespace }); + setupMockResults(options); + + await createSavedObjects(objs, options); + expect(bulkCreate).toHaveBeenCalledTimes(2); + // these three objects are transformed before being created, because they are included in the `importIdMap` + const x3 = { ...obj3, id: importId3 }; // this import object already has an originId + const x4 = { ...obj4, id: importId4 }; // this import object already has an originId + const x8 = { ...obj8, id: importId8, originId: obj8.id }; // this import object doesn't have an originId, so it is set before create + const argObjs = [obj1, obj2, x3, x4, obj5, obj6, obj7, x8, obj9, obj10, obj11, obj12, obj13]; + expectBulkCreateArgs.objects(1, argObjs); // we expect to first try bulkCreate with all thirteen test cases + expectBulkCreateArgs.objects(2, [obj5, obj9], true); // we only expect to retry bulkCreate with the unresolvable conflicts + }; + const testBulkCreateOptions = async (namespace?: string) => { + const overwrite = (Symbol() as unknown) as boolean; + const options = setupOptions({ namespace, overwrite }); + setupMockResults(options); + + await createSavedObjects(objs, options); + expect(bulkCreate).toHaveBeenCalledTimes(2); + expectBulkCreateArgs.options(1, options); + expectBulkCreateArgs.options(2, options); + }; + const testReturnValue = async (namespace?: string) => { + const options = setupOptions({ namespace }); + setupMockResults(options); + + const createSavedObjectsResult = await createSavedObjects(objs, options); + const resultSavedObjects = (await bulkCreate.mock.results[0].value).saved_objects; + const [r1, r2, r3, r4, , r6, r7, r8, , r10, r11, r12, r13] = resultSavedObjects; + const [r5, r9] = (await bulkCreate.mock.results[1].value).saved_objects; // these two import objects were retried, so the retry results are returned + // these five results are transformed before being returned, because the bulkCreate attempt used different IDs for them + const [x3, x4, x5, x8, x9] = [r3, r4, r5, r8, r9].map((x: SavedObject) => ({ + ...x, + newId: x.id, + })); + const transformedResults = [r1, r2, x3, x4, x5, r6, r7, x8, x9, r10, r11, r12, r13]; + const expectedResults = getExpectedResults(transformedResults, objs); + expect(createSavedObjectsResult).toEqual(expectedResults); + }; + + describe('with an undefined namespace', () => { + test('calls bulkCreate once with input objects, and a second time for unresolvable conflicts', async () => { + await testBulkCreateObjects(); + }); + test('calls bulkCreate once with input options, and a second time with input options', async () => { + await testBulkCreateOptions(); + }); + test('returns bulkCreate results that are merged and remapped to IDs of imported objects', async () => { + await testReturnValue(); + }); + }); + + describe('with a defined namespace', () => { + const namespace = 'some-namespace'; + test('calls bulkCreate once with input objects, and a second time for unresolvable conflicts', async () => { + await testBulkCreateObjects(namespace); + }); + test('calls bulkCreate once with input options, and a second time with input options', async () => { + await testBulkCreateOptions(namespace); + }); + test('returns bulkCreate results that are merged and remapped to IDs of imported objects', async () => { + await testReturnValue(namespace); + }); + }); + }); +}); diff --git a/src/core/server/saved_objects/import/create_saved_objects.ts b/src/core/server/saved_objects/import/create_saved_objects.ts new file mode 100644 index 0000000000000..e847f603e50f2 --- /dev/null +++ b/src/core/server/saved_objects/import/create_saved_objects.ts @@ -0,0 +1,131 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +import { v4 as uuidv4 } from 'uuid'; +import { SavedObject, SavedObjectsClientContract, SavedObjectsImportError } from '../types'; +import { extractErrors } from './extract_errors'; + +interface CreateSavedObjectsOptions { + savedObjectsClient: SavedObjectsClientContract; + importIdMap: Map; + namespace?: string; + overwrite?: boolean; +} +interface CreateSavedObjectsResult { + createdObjects: Array & { newId?: string }>; + errors: SavedObjectsImportError[]; +} + +type UnresolvableConflict = { retryIndex: number; retryObject: SavedObject }; +type Left = { tag: 'left'; value: UnresolvableConflict }; +type Right = { tag: 'right'; value: SavedObject }; +type Either = Left | Right; +const isLeft = (object: Either): object is Left => object.tag === 'left'; +const isRight = (object: Either): object is Right => object.tag === 'right'; + +const isUnresolvableConflict = (object: SavedObject) => + object.error?.statusCode === 409 && object.error?.metadata?.isNotOverwritable; + +/** + * This function abstracts the bulk creation of import objects for two purposes: + * 1. The import ID map that was generated by the `checkConflicts` function should dictate the IDs of the objects we create. + * 2. Any object create attempt that results in an unresolvable conflict should have its ID regenerated and retry create. This way, when an + * object with a "multi-namespace" type is exported from one space and imported to another, it does not result in an error, but instead + * a new object is created. + */ +export const createSavedObjects = async ( + objects: Array>, + options: CreateSavedObjectsOptions +): Promise> => { + // exit early if there are no objects to create + if (objects.length === 0) { + return { createdObjects: [], errors: [] }; + } + + const { savedObjectsClient, importIdMap, namespace, overwrite } = options; + + // generate a map of the raw object IDs + const objectIdMap = objects.reduce( + (map, object) => map.set(`${object.type}:${object.id}`, object), + new Map>() + ); + + // use the import ID map from the `checkConflicts` or `getImportIdMapForRetries` function to ensure that each object is being created with + // the correct ID also, ensure that the `originId` is set on the created object if it did not have one + const objectsToCreate = objects.map((object) => { + const importIdEntry = importIdMap.get(`${object.type}:${object.id}`); + if (importIdEntry) { + objectIdMap.set(`${object.type}:${importIdEntry.id}`, object); + const originId = object.originId ?? object.id; + return { ...object, id: importIdEntry.id, originId }; + } + return object; + }); + const bulkCreateResponse = await savedObjectsClient.bulkCreate(objectsToCreate, { + namespace, + overwrite, + }); + + // retry bulkCreate for multi-namespace saved objects that had an unresolvable conflict + // note: by definition, only multi-namespace saved objects can have an unresolvable conflict + let retryIndexCounter = 0; + const bulkCreateResults: Array> = bulkCreateResponse.saved_objects.map((result) => { + const object = objectIdMap.get(`${result.type}:${result.id}`)!; + if (isUnresolvableConflict(result)) { + const id = uuidv4(); + const originId = object.originId || object.id; + const retryObject = { ...object, id, originId }; + objectIdMap.set(`${retryObject.type}:${retryObject.id}`, object); + return { tag: 'left', value: { retryIndex: retryIndexCounter++, retryObject } }; + } + return { tag: 'right', value: result }; + }); + + // note: this is unrelated to "retries" that are passed into the `resolveSavedObjectsImportErrors` function + const retries = bulkCreateResults.filter(isLeft).map((x) => x.value.retryObject); + const retryResults = + retries.length > 0 + ? (await savedObjectsClient.bulkCreate(retries, { namespace, overwrite })).saved_objects + : []; + + const results: Array> = []; + bulkCreateResults.forEach((result) => { + if (isLeft(result)) { + const { retryIndex } = result.value; + results.push(retryResults[retryIndex]); + } else if (isRight(result)) { + results.push(result.value); + } + }); + + // remap results to reflect the object IDs that were submitted for import + // this ensures that consumers understand the results + const remappedResults = results.map & { newId?: string }>((result) => { + const { id } = objectIdMap.get(`${result.type}:${result.id}`)!; + // also, include a `newId` field if the object create attempt was made with a different ID + return { ...result, id, ...(id !== result.id && { newId: result.id }) }; + }); + + return { + createdObjects: remappedResults.filter((obj) => !obj.error), + errors: extractErrors(remappedResults, objects), + }; +}; diff --git a/src/core/server/saved_objects/import/extract_errors.test.ts b/src/core/server/saved_objects/import/extract_errors.test.ts index a20d14e6809e2..e75dca2626648 100644 --- a/src/core/server/saved_objects/import/extract_errors.test.ts +++ b/src/core/server/saved_objects/import/extract_errors.test.ts @@ -29,7 +29,7 @@ describe('extractErrors()', () => { }); test('extracts errors from saved objects', () => { - const savedObjects: SavedObject[] = [ + const savedObjects: Array = [ { id: '1', type: 'dashboard', @@ -56,6 +56,16 @@ describe('extractErrors()', () => { references: [], error: SavedObjectsErrorHelpers.createBadRequestError().output.payload, }, + { + id: '4', + type: 'dashboard', + attributes: { + title: 'My Dashboard 4', + }, + references: [], + error: SavedObjectsErrorHelpers.createConflictError('dashboard', '4').output.payload, + newId: 'foo', + }, ]; const result = extractErrors(savedObjects, savedObjects); expect(result).toMatchInlineSnapshot(` @@ -79,6 +89,15 @@ Array [ "title": "My Dashboard 3", "type": "dashboard", }, + Object { + "error": Object { + "destinationId": "foo", + "type": "conflict", + }, + "id": "4", + "title": "My Dashboard 4", + "type": "dashboard", + }, ] `); }); diff --git a/src/core/server/saved_objects/import/extract_errors.ts b/src/core/server/saved_objects/import/extract_errors.ts index 5728ce8b7b59f..d1cc912eb67b1 100644 --- a/src/core/server/saved_objects/import/extract_errors.ts +++ b/src/core/server/saved_objects/import/extract_errors.ts @@ -21,7 +21,7 @@ import { SavedObjectsImportError } from './types'; export function extractErrors( // TODO: define saved object type - savedObjectResults: Array>, + savedObjectResults: Array & { newId?: string }>, savedObjectsToImport: Array> ) { const errors: SavedObjectsImportError[] = []; @@ -34,10 +34,8 @@ export function extractErrors( const originalSavedObject = originalSavedObjectsMap.get( `${savedObject.type}:${savedObject.id}` ); - const title = - originalSavedObject && - originalSavedObject.attributes && - originalSavedObject.attributes.title; + const title = originalSavedObject?.attributes?.title; + const { newId } = savedObject; if (savedObject.error.statusCode === 409) { errors.push({ id: savedObject.id, @@ -45,6 +43,7 @@ export function extractErrors( title, error: { type: 'conflict', + ...(newId && { destinationId: newId }), }, }); continue; 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 eb3c4a98e2386..30db655476b8c 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 @@ -18,613 +18,170 @@ */ import { Readable } from 'stream'; -import { SavedObject } from '../types'; -import { importSavedObjectsFromStream } from './import_saved_objects'; +import { v4 as uuidv4 } from 'uuid'; +import { + SavedObjectsClientContract, + SavedObjectsType, + SavedObject, + SavedObjectsImportError, +} from '../types'; import { savedObjectsClientMock } from '../../mocks'; -import { SavedObjectsErrorHelpers } from '..'; +import { SavedObjectsImportOptions, ISavedObjectTypeRegistry } from '..'; import { typeRegistryMock } from '../saved_objects_type_registry.mock'; +import { importSavedObjectsFromStream } from './import_saved_objects'; -const emptyResponse = { - saved_objects: [], - total: 0, - per_page: 0, - page: 0, -}; +import { collectSavedObjects } from './collect_saved_objects'; +import { validateReferences } from './validate_references'; +import { checkConflicts } from './check_conflicts'; +import { createSavedObjects } from './create_saved_objects'; -const createTypeRegistryMock = (supportedTypes: string[]) => { - const typeRegistry = typeRegistryMock.create(); - const types = supportedTypes.map((name) => ({ - name, - hidden: false, - namespaceType: 'single' as 'single', - mappings: { properties: {} }, - })); - typeRegistry.getImportableAndExportableTypes.mockReturnValue(types); - return typeRegistry; -}; +jest.mock('./collect_saved_objects'); +jest.mock('./validate_references'); +jest.mock('./check_conflicts'); +jest.mock('./create_saved_objects'); -describe('importSavedObjects()', () => { - const savedObjects: SavedObject[] = [ - { - id: '1', - type: 'index-pattern', - attributes: { - title: 'My Index Pattern', - }, - references: [], - }, - { - id: '2', - type: 'search', - attributes: { - title: 'My Search', - }, - references: [], - }, - { - id: '3', - type: 'visualization', - attributes: { - title: 'My Visualization', - }, - references: [], - }, - { - id: '4', - type: 'dashboard', - attributes: { - title: 'My Dashboard', - }, - references: [], - }, - ]; - const savedObjectsClient = savedObjectsClientMock.create(); - const supportedTypes = ['index-pattern', 'search', 'visualization', 'dashboard']; +const getMockFn = any, U>(fn: (...args: Parameters) => U) => + fn as jest.MockedFunction<(...args: Parameters) => U>; +describe('#importSavedObjectsFromStream', () => { beforeEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); + // mock empty output of each of these mocked modules so the import doesn't throw an error + getMockFn(collectSavedObjects).mockResolvedValue({ errors: [], collectedObjects: [] }); + getMockFn(validateReferences).mockResolvedValue({ errors: [], filteredObjects: [] }); + getMockFn(checkConflicts).mockResolvedValue({ + errors: [], + filteredObjects: [], + importIdMap: new Map(), + }); + getMockFn(createSavedObjects).mockResolvedValue({ errors: [], createdObjects: [] }); }); - test('returns early when no objects exist', async () => { - const readStream = new Readable({ - objectMode: true, - read() { - this.push(null); - }, - }); - const result = await importSavedObjectsFromStream({ - readStream, - objectLimit: 1, - overwrite: false, - savedObjectsClient, - typeRegistry: createTypeRegistryMock([]), - }); - expect(result).toMatchInlineSnapshot(` - Object { - "success": true, - "successCount": 0, - } - `); - }); + let readStream: Readable; + const objectLimit = 10; + const overwrite = (Symbol() as unknown) as boolean; + let savedObjectsClient: jest.Mocked; + let typeRegistry: jest.Mocked; + const namespace = 'some-namespace'; - test('calls bulkCreate without overwrite', async () => { - const readStream = new Readable({ - objectMode: true, - read() { - savedObjects.forEach((obj) => this.push(obj)); - this.push(null); - }, - }); - savedObjectsClient.find.mockResolvedValueOnce(emptyResponse); - savedObjectsClient.bulkCreate.mockResolvedValue({ - saved_objects: savedObjects, - }); - const result = await importSavedObjectsFromStream({ - readStream, - objectLimit: 4, - overwrite: false, - savedObjectsClient, - typeRegistry: createTypeRegistryMock(supportedTypes), - }); - expect(result).toMatchInlineSnapshot(` - Object { - "success": true, - "successCount": 4, - } - `); - expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(` - [MockFunction] { - "calls": Array [ - Array [ - Array [ - Object { - "attributes": Object { - "title": "My Index Pattern", - }, - "id": "1", - "migrationVersion": Object {}, - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object { - "title": "My Search", - }, - "id": "2", - "migrationVersion": Object {}, - "references": Array [], - "type": "search", - }, - Object { - "attributes": Object { - "title": "My Visualization", - }, - "id": "3", - "migrationVersion": Object {}, - "references": Array [], - "type": "visualization", - }, - Object { - "attributes": Object { - "title": "My Dashboard", - }, - "id": "4", - "migrationVersion": Object {}, - "references": Array [], - "type": "dashboard", - }, - ], - Object { - "namespace": undefined, - "overwrite": false, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], - } - `); - }); + const setupOptions = (): SavedObjectsImportOptions => { + readStream = new Readable(); + savedObjectsClient = savedObjectsClientMock.create(); + typeRegistry = typeRegistryMock.create(); + return { readStream, objectLimit, overwrite, savedObjectsClient, typeRegistry, namespace }; + }; + const createObject = () => { + return ({ type: 'foo-type', id: uuidv4() } as unknown) as SavedObject<{ title: string }>; + }; + const createError = () => { + return ({ type: 'foo-type', id: uuidv4() } as unknown) as SavedObjectsImportError; + }; - test('uses the provided namespace when present', async () => { - const readStream = new Readable({ - objectMode: true, - read() { - savedObjects.forEach((obj) => this.push(obj)); - this.push(null); - }, - }); - savedObjectsClient.find.mockResolvedValueOnce(emptyResponse); - savedObjectsClient.bulkCreate.mockResolvedValue({ - saved_objects: savedObjects, - }); - const result = await importSavedObjectsFromStream({ - readStream, - objectLimit: 4, - overwrite: false, - savedObjectsClient, - typeRegistry: createTypeRegistryMock(supportedTypes), - namespace: 'foo', - }); - expect(result).toMatchInlineSnapshot(` - Object { - "success": true, - "successCount": 4, - } - `); - expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(` - [MockFunction] { - "calls": Array [ - Array [ - Array [ - Object { - "attributes": Object { - "title": "My Index Pattern", - }, - "id": "1", - "migrationVersion": Object {}, - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object { - "title": "My Search", - }, - "id": "2", - "migrationVersion": Object {}, - "references": Array [], - "type": "search", - }, - Object { - "attributes": Object { - "title": "My Visualization", - }, - "id": "3", - "migrationVersion": Object {}, - "references": Array [], - "type": "visualization", - }, - Object { - "attributes": Object { - "title": "My Dashboard", - }, - "id": "4", - "migrationVersion": Object {}, - "references": Array [], - "type": "dashboard", - }, - ], - Object { - "namespace": "foo", - "overwrite": false, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], - } - `); - }); + /** + * These tests use minimal mocks which don't look realistic, but are sufficient to exercise the code paths correctly. For example, for an + * object to be imported successfully it would need to be obtained from `collectSavedObjects`, passed to `validateReferences`, passed to + * `checkConflicts`, passed to `createSavedObjects`, and returned from that. However, for each of the tests below, we skip the + * intermediate steps in the interest of brevity. + */ + describe('module calls', () => { + test('collects saved objects from stream', async () => { + const options = setupOptions(); + const supportedTypes = ['foo-type']; + typeRegistry.getImportableAndExportableTypes.mockReturnValue( + supportedTypes.map((name) => ({ name })) as SavedObjectsType[] + ); - test('calls bulkCreate with overwrite', async () => { - const readStream = new Readable({ - objectMode: true, - read() { - savedObjects.forEach((obj) => this.push(obj)); - this.push(null); - }, - }); - savedObjectsClient.find.mockResolvedValueOnce(emptyResponse); - savedObjectsClient.bulkCreate.mockResolvedValue({ - saved_objects: savedObjects, - }); - const result = await importSavedObjectsFromStream({ - readStream, - objectLimit: 4, - overwrite: true, - savedObjectsClient, - typeRegistry: createTypeRegistryMock(supportedTypes), + await importSavedObjectsFromStream(options); + expect(typeRegistry.getImportableAndExportableTypes).toHaveBeenCalled(); + const collectSavedObjectsOptions = { readStream, objectLimit, supportedTypes }; + expect(collectSavedObjects).toHaveBeenCalledWith(collectSavedObjectsOptions); }); - expect(result).toMatchInlineSnapshot(` - Object { - "success": true, - "successCount": 4, - } - `); - expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(` - [MockFunction] { - "calls": Array [ - Array [ - Array [ - Object { - "attributes": Object { - "title": "My Index Pattern", - }, - "id": "1", - "migrationVersion": Object {}, - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object { - "title": "My Search", - }, - "id": "2", - "migrationVersion": Object {}, - "references": Array [], - "type": "search", - }, - Object { - "attributes": Object { - "title": "My Visualization", - }, - "id": "3", - "migrationVersion": Object {}, - "references": Array [], - "type": "visualization", - }, - Object { - "attributes": Object { - "title": "My Dashboard", - }, - "id": "4", - "migrationVersion": Object {}, - "references": Array [], - "type": "dashboard", - }, - ], - Object { - "namespace": undefined, - "overwrite": true, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], - } - `); - }); - test('extracts errors for conflicts', async () => { - const readStream = new Readable({ - objectMode: true, - read() { - savedObjects.forEach((obj) => this.push(obj)); - this.push(null); - }, + test('validates references', async () => { + const options = setupOptions(); + const collectedObjects = [createObject()]; + getMockFn(collectSavedObjects).mockResolvedValue({ errors: [], collectedObjects }); + + await importSavedObjectsFromStream(options); + expect(validateReferences).toHaveBeenCalledWith( + collectedObjects, + savedObjectsClient, + namespace + ); }); - savedObjectsClient.find.mockResolvedValueOnce(emptyResponse); - savedObjectsClient.bulkCreate.mockResolvedValue({ - saved_objects: savedObjects.map(({ type, id }) => ({ - type, - id, - error: SavedObjectsErrorHelpers.createConflictError(type, id).output.payload, - attributes: {}, - references: [], - })), + + test('checks conflicts', async () => { + const options = setupOptions(); + const filteredObjects = [createObject()]; + getMockFn(validateReferences).mockResolvedValue({ errors: [], filteredObjects }); + + await importSavedObjectsFromStream(options); + const checkConflictsOptions = { savedObjectsClient, typeRegistry, namespace }; + expect(checkConflicts).toHaveBeenCalledWith(filteredObjects, checkConflictsOptions); }); - const result = await importSavedObjectsFromStream({ - readStream, - objectLimit: 4, - overwrite: false, - savedObjectsClient, - typeRegistry: createTypeRegistryMock(supportedTypes), + + test('creates saved objects', async () => { + const options = setupOptions(); + const filteredObjects = [createObject()]; + const importIdMap = new Map(); + getMockFn(checkConflicts).mockResolvedValue({ errors: [], filteredObjects, importIdMap }); + + await importSavedObjectsFromStream(options); + const createSavedObjectsOptions = { savedObjectsClient, importIdMap, overwrite, namespace }; + expect(createSavedObjects).toHaveBeenCalledWith(filteredObjects, createSavedObjectsOptions); }); - expect(result).toMatchInlineSnapshot(` - Object { - "errors": Array [ - Object { - "error": Object { - "type": "conflict", - }, - "id": "1", - "title": "My Index Pattern", - "type": "index-pattern", - }, - Object { - "error": Object { - "type": "conflict", - }, - "id": "2", - "title": "My Search", - "type": "search", - }, - Object { - "error": Object { - "type": "conflict", - }, - "id": "3", - "title": "My Visualization", - "type": "visualization", - }, - Object { - "error": Object { - "type": "conflict", - }, - "id": "4", - "title": "My Dashboard", - "type": "dashboard", - }, - ], - "success": false, - "successCount": 0, - } - `); }); - test('validates references', async () => { - const readStream = new Readable({ - objectMode: true, - read() { - this.push({ - id: '1', - type: 'search', - attributes: { - title: 'My Search', - }, - references: [ - { - name: 'ref_0', - type: 'index-pattern', - id: '2', - }, - ], - }); - this.push({ - id: '3', - type: 'visualization', - attributes: { - title: 'My Visualization', - }, - references: [ - { - name: 'ref_0', - type: 'search', - id: '1', - }, - ], - }); - this.push(null); - }, - }); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - type: 'index-pattern', - id: '2', - error: SavedObjectsErrorHelpers.createGenericNotFoundError('index-pattern', '2').output - .payload, - attributes: {}, - references: [], - }, - ], - }); - const result = await importSavedObjectsFromStream({ - readStream, - objectLimit: 4, - overwrite: false, - savedObjectsClient, - typeRegistry: createTypeRegistryMock(supportedTypes), + describe('results', () => { + test('returns success=true if no errors occurred', async () => { + const options = setupOptions(); + + const result = await importSavedObjectsFromStream(options); + expect(result).toEqual({ success: true, successCount: 0 }); }); - expect(result).toMatchInlineSnapshot(` - Object { - "errors": Array [ - Object { - "error": Object { - "blocking": Array [ - Object { - "id": "3", - "type": "visualization", - }, - ], - "references": Array [ - Object { - "id": "2", - "type": "index-pattern", - }, - ], - "type": "missing_references", - }, - "id": "1", - "title": "My Search", - "type": "search", - }, - ], - "success": false, - "successCount": 0, - } - `); - expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` - [MockFunction] { - "calls": Array [ - Array [ - Array [ - Object { - "fields": Array [ - "id", - ], - "id": "2", - "type": "index-pattern", - }, - ], - Object { - "namespace": undefined, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], - } - `); - }); - test('validates supported types', async () => { - const readStream = new Readable({ - objectMode: true, - read() { - savedObjects.forEach((obj) => this.push(obj)); - this.push({ id: '1', type: 'wigwags', attributes: { title: 'my title' }, references: [] }); - this.push(null); - }, + test('returns success=false if an error occurred', async () => { + const options = setupOptions(); + const errors = [createError()]; + getMockFn(collectSavedObjects).mockResolvedValue({ errors, collectedObjects: [] }); + + const result = await importSavedObjectsFromStream(options); + expect(result).toEqual({ success: false, successCount: 0, errors }); }); - savedObjectsClient.find.mockResolvedValueOnce(emptyResponse); - savedObjectsClient.bulkCreate.mockResolvedValue({ - saved_objects: savedObjects, + + test('handles a mix of successes and errors', async () => { + const options = setupOptions(); + const errors = [createError()]; + const obj1 = createObject(); + const obj2 = { ...createObject(), newId: 'some-newId' }; + getMockFn(createSavedObjects).mockResolvedValue({ errors, createdObjects: [obj1, obj2] }); + + const result = await importSavedObjectsFromStream(options); + // successResults only includes the imported object's type, id, and newId (if a new one was generated) + const successResults = [ + { type: obj1.type, id: obj1.id }, + { type: obj2.type, id: obj2.id, newId: 'some-newId' }, + ]; + expect(result).toEqual({ success: false, successCount: 2, successResults, errors }); }); - const result = await importSavedObjectsFromStream({ - readStream, - objectLimit: 5, - overwrite: false, - savedObjectsClient, - typeRegistry: createTypeRegistryMock(supportedTypes), + + test('accumulates multiple errors', async () => { + const options = setupOptions(); + const errors = [createError(), createError(), createError(), createError()]; + getMockFn(collectSavedObjects).mockResolvedValue({ + errors: [errors[0]], + collectedObjects: [], + }); + getMockFn(validateReferences).mockResolvedValue({ errors: [errors[1]], filteredObjects: [] }); + getMockFn(checkConflicts).mockResolvedValue({ + errors: [errors[2]], + filteredObjects: [], + importIdMap: new Map(), + }); + getMockFn(createSavedObjects).mockResolvedValue({ errors: [errors[3]], createdObjects: [] }); + + const result = await importSavedObjectsFromStream(options); + expect(result).toEqual({ success: false, successCount: 0, errors }); }); - expect(result).toMatchInlineSnapshot(` - Object { - "errors": Array [ - Object { - "error": Object { - "type": "unsupported_type", - }, - "id": "1", - "title": "my title", - "type": "wigwags", - }, - ], - "success": false, - "successCount": 4, - } - `); - expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(` - [MockFunction] { - "calls": Array [ - Array [ - Array [ - Object { - "attributes": Object { - "title": "My Index Pattern", - }, - "id": "1", - "migrationVersion": Object {}, - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object { - "title": "My Search", - }, - "id": "2", - "migrationVersion": Object {}, - "references": Array [], - "type": "search", - }, - Object { - "attributes": Object { - "title": "My Visualization", - }, - "id": "3", - "migrationVersion": Object {}, - "references": Array [], - "type": "visualization", - }, - Object { - "attributes": Object { - "title": "My Dashboard", - }, - "id": "4", - "migrationVersion": Object {}, - "references": Array [], - "type": "dashboard", - }, - ], - Object { - "namespace": undefined, - "overwrite": false, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], - } - `); }); }); 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 d274ece506759..745d170ba0525 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.ts @@ -18,13 +18,14 @@ */ import { collectSavedObjects } from './collect_saved_objects'; -import { extractErrors } from './extract_errors'; import { SavedObjectsImportError, SavedObjectsImportResponse, SavedObjectsImportOptions, } from './types'; import { validateReferences } from './validate_references'; +import { checkConflicts } from './check_conflicts'; +import { createSavedObjects } from './create_saved_objects'; /** * Import saved objects from given stream. See the {@link SavedObjectsImportOptions | options} for more @@ -51,35 +52,37 @@ export async function importSavedObjectsFromStream({ errorAccumulator = [...errorAccumulator, ...collectorErrors]; // Validate references - const { filteredObjects, errors: validationErrors } = await validateReferences( + const validateReferencesResult = await validateReferences( objectsFromStream, savedObjectsClient, namespace ); - errorAccumulator = [...errorAccumulator, ...validationErrors]; + errorAccumulator = [...errorAccumulator, ...validateReferencesResult.errors]; - // Exit early if no objects to import - if (filteredObjects.length === 0) { - return { - success: errorAccumulator.length === 0, - successCount: 0, - ...(errorAccumulator.length ? { errors: errorAccumulator } : {}), - }; - } + // Check multi-namespace object types for regular conflicts and ambiguous conflicts + const checkConflictsOptions = { savedObjectsClient, typeRegistry, namespace }; + const { filteredObjects, errors: conflictErrors, importIdMap } = await checkConflicts( + validateReferencesResult.filteredObjects, + checkConflictsOptions + ); + errorAccumulator = [...errorAccumulator, ...conflictErrors]; // Create objects in bulk - const bulkCreateResult = await savedObjectsClient.bulkCreate(filteredObjects, { - overwrite, - namespace, + const createSavedObjectsOptions = { savedObjectsClient, importIdMap, overwrite, namespace }; + const { createdObjects, errors: bulkCreateErrors } = await createSavedObjects( + filteredObjects, + createSavedObjectsOptions + ); + errorAccumulator = [...errorAccumulator, ...bulkCreateErrors]; + + const successResults = createdObjects.map(({ type, id, newId }) => { + return { type, id, ...(newId && { newId }) }; }); - errorAccumulator = [ - ...errorAccumulator, - ...extractErrors(bulkCreateResult.saved_objects, filteredObjects), - ]; return { + successCount: createdObjects.length, success: errorAccumulator.length === 0, - successCount: bulkCreateResult.saved_objects.filter((obj) => !obj.error).length, - ...(errorAccumulator.length ? { errors: errorAccumulator } : {}), + ...(successResults.length && { successResults }), + ...(errorAccumulator.length && { errors: errorAccumulator }), }; } diff --git a/src/core/server/saved_objects/import/index.ts b/src/core/server/saved_objects/import/index.ts index e268e970b94ac..ab69e4fc44197 100644 --- a/src/core/server/saved_objects/import/index.ts +++ b/src/core/server/saved_objects/import/index.ts @@ -21,9 +21,11 @@ export { importSavedObjectsFromStream } from './import_saved_objects'; export { resolveSavedObjectsImportErrors } from './resolve_import_errors'; export { SavedObjectsImportResponse, + SavedObjectsImportSuccess, SavedObjectsImportError, SavedObjectsImportOptions, SavedObjectsImportConflictError, + SavedObjectsImportAmbiguousConflictError, SavedObjectsImportMissingReferencesError, SavedObjectsImportUnknownError, SavedObjectsImportUnsupportedTypeError, 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 6c29d8399d87c..6f401d10f87db 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 @@ -18,577 +18,263 @@ */ import { Readable } from 'stream'; -import { SavedObject } from '../types'; -import { resolveSavedObjectsImportErrors } from './resolve_import_errors'; +import { v4 as uuidv4 } from 'uuid'; +import { + SavedObjectsClientContract, + SavedObjectsType, + SavedObject, + SavedObjectsImportError, + SavedObjectsImportRetry, + SavedObjectReference, +} from '../types'; import { savedObjectsClientMock } from '../../mocks'; -import { SavedObjectsErrorHelpers } from '..'; +import { SavedObjectsResolveImportErrorsOptions, ISavedObjectTypeRegistry } from '..'; import { typeRegistryMock } from '../saved_objects_type_registry.mock'; +import { resolveSavedObjectsImportErrors } from './resolve_import_errors'; -const createTypeRegistryMock = (supportedTypes: string[]) => { - const typeRegistry = typeRegistryMock.create(); - const types = supportedTypes.map((name) => ({ - name, - hidden: false, - namespaceType: 'single' as 'single', - mappings: { properties: {} }, - })); - typeRegistry.getImportableAndExportableTypes.mockReturnValue(types); - return typeRegistry; -}; - -describe('resolveImportErrors()', () => { - const savedObjects: SavedObject[] = [ - { - id: '1', - type: 'index-pattern', - attributes: { - title: 'My Index Pattern', - }, - references: [], - }, - { - id: '2', - type: 'search', - attributes: { - title: 'My Search', - }, - references: [], - }, - { - id: '3', - type: 'visualization', - attributes: { - title: 'My Visualization', - }, - references: [], - }, - { - id: '4', - type: 'dashboard', - attributes: { - title: 'My Dashboard', - }, - references: [ - { - name: 'panel_0', - type: 'visualization', - id: '3', - }, - ], - }, - ]; - const savedObjectsClient = savedObjectsClientMock.create(); - const supportedTypes = ['index-pattern', 'search', 'visualization', 'dashboard']; +import { validateRetries } from './validate_retries'; +import { collectSavedObjects } from './collect_saved_objects'; +import { validateReferences } from './validate_references'; +import { getImportIdMapForRetries } from './check_conflicts'; +import { splitOverwrites } from './split_overwrites'; +import { createSavedObjects } from './create_saved_objects'; +import { createObjectsFilter } from './create_objects_filter'; - beforeEach(() => { - jest.resetAllMocks(); - }); +jest.mock('./validate_retries'); +jest.mock('./create_objects_filter'); +jest.mock('./collect_saved_objects'); +jest.mock('./validate_references'); +jest.mock('./check_conflicts'); +jest.mock('./split_overwrites'); +jest.mock('./create_saved_objects'); - test('works with empty parameters', async () => { - const readStream = new Readable({ - objectMode: true, - read() { - savedObjects.forEach((obj) => this.push(obj)); - this.push(null); - }, - }); - savedObjectsClient.bulkCreate.mockResolvedValue({ - saved_objects: [], - }); - const result = await resolveSavedObjectsImportErrors({ - readStream, - objectLimit: 4, - retries: [], - savedObjectsClient, - typeRegistry: createTypeRegistryMock(supportedTypes), - }); - expect(result).toMatchInlineSnapshot(` - Object { - "success": true, - "successCount": 0, - } - `); - expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(`[MockFunction]`); - }); +const getMockFn = any, U>(fn: (...args: Parameters) => U) => + fn as jest.MockedFunction<(...args: Parameters) => U>; - test('works with retries', async () => { - const readStream = new Readable({ - objectMode: true, - read() { - savedObjects.forEach((obj) => this.push(obj)); - this.push(null); - }, - }); - savedObjectsClient.bulkCreate.mockResolvedValueOnce({ - saved_objects: savedObjects.filter((obj) => obj.type === 'visualization' && obj.id === '3'), - }); - const result = await resolveSavedObjectsImportErrors({ - readStream, - objectLimit: 4, - retries: [ - { - type: 'visualization', - id: '3', - replaceReferences: [], - overwrite: false, - }, - ], - savedObjectsClient, - typeRegistry: createTypeRegistryMock(supportedTypes), +describe('#importSavedObjectsFromStream', () => { + beforeEach(() => { + jest.clearAllMocks(); + // mock empty output of each of these mocked modules so the import doesn't throw an error + getMockFn(createObjectsFilter).mockReturnValue(() => false); + getMockFn(collectSavedObjects).mockResolvedValue({ errors: [], collectedObjects: [] }); + getMockFn(validateReferences).mockResolvedValue({ errors: [], filteredObjects: [] }); + getMockFn(getImportIdMapForRetries).mockResolvedValue(new Map()); + getMockFn(splitOverwrites).mockReturnValue({ + objectsToOverwrite: [], + objectsToNotOverwrite: [], }); - expect(result).toMatchInlineSnapshot(` - Object { - "success": true, - "successCount": 1, - } - `); - expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(` - [MockFunction] { - "calls": Array [ - Array [ - Array [ - Object { - "attributes": Object { - "title": "My Visualization", - }, - "id": "3", - "migrationVersion": Object {}, - "references": Array [], - "type": "visualization", - }, - ], - Object { - "namespace": undefined, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], - } - `); + getMockFn(createSavedObjects).mockResolvedValue({ errors: [], createdObjects: [] }); }); - test('works with overwrites', async () => { - const readStream = new Readable({ - objectMode: true, - read() { - savedObjects.forEach((obj) => this.push(obj)); - this.push(null); - }, - }); - savedObjectsClient.bulkCreate.mockResolvedValue({ - saved_objects: savedObjects.filter((obj) => obj.type === 'index-pattern' && obj.id === '1'), - }); - const result = await resolveSavedObjectsImportErrors({ - readStream, - objectLimit: 4, - retries: [ - { - type: 'index-pattern', - id: '1', - overwrite: true, - replaceReferences: [], - }, - ], - savedObjectsClient, - typeRegistry: createTypeRegistryMock(supportedTypes), - }); - expect(result).toMatchInlineSnapshot(` - Object { - "success": true, - "successCount": 1, - } - `); - expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(` - [MockFunction] { - "calls": Array [ - Array [ - Array [ - Object { - "attributes": Object { - "title": "My Index Pattern", - }, - "id": "1", - "migrationVersion": Object {}, - "references": Array [], - "type": "index-pattern", - }, - ], - Object { - "namespace": undefined, - "overwrite": true, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], - } - `); - }); + let readStream: Readable; + const objectLimit = 10; + let savedObjectsClient: jest.Mocked; + let typeRegistry: jest.Mocked; + const namespace = 'some-namespace'; - test('works with replaceReferences', async () => { - const readStream = new Readable({ - objectMode: true, - read() { - savedObjects.forEach((obj) => this.push(obj)); - this.push(null); - }, - }); - savedObjectsClient.bulkCreate.mockResolvedValue({ - saved_objects: savedObjects.filter((obj) => obj.type === 'dashboard' && obj.id === '4'), + const setupOptions = ( + retries: SavedObjectsImportRetry[] = [] + ): SavedObjectsResolveImportErrorsOptions => { + readStream = new Readable(); + savedObjectsClient = savedObjectsClientMock.create(); + typeRegistry = typeRegistryMock.create(); + return { readStream, objectLimit, retries, savedObjectsClient, typeRegistry, namespace }; + }; + + const createRetry = (options?: { + id?: string; + overwrite?: boolean; + replaceReferences?: SavedObjectsImportRetry['replaceReferences']; + }) => { + const { id = uuidv4(), overwrite = false, replaceReferences = [] } = options ?? {}; + return { type: 'foo-type', id, overwrite, replaceReferences }; + }; + const createObject = (references?: SavedObjectReference[]) => { + return ({ type: 'foo-type', id: uuidv4(), references } as unknown) as SavedObject<{ + title: string; + }>; + }; + const createError = () => { + return ({ type: 'foo-type', id: uuidv4() } as unknown) as SavedObjectsImportError; + }; + + /** + * These tests use minimal mocks which don't look realistic, but are sufficient to exercise the code paths correctly. For example, for an + * object to be imported successfully it would need to be obtained from `collectSavedObjects`, passed to `validateReferences`, passed to + * `checkConflicts`, passed to `createSavedObjects`, and returned from that. However, for each of the tests below, we skip the + * intermediate steps in the interest of brevity. + */ + describe('module calls', () => { + test('validates retries', async () => { + const retry = createRetry(); + const options = setupOptions([retry]); + + await resolveSavedObjectsImportErrors(options); + expect(validateRetries).toHaveBeenCalledWith([retry]); }); - const result = await resolveSavedObjectsImportErrors({ - readStream, - objectLimit: 4, - retries: [ - { - type: 'dashboard', - id: '4', - overwrite: false, - replaceReferences: [ - { - type: 'visualization', - from: '3', - to: '13', - }, - ], - }, - ], - savedObjectsClient, - typeRegistry: createTypeRegistryMock(supportedTypes), + + test('creates objects filter', async () => { + const retry = createRetry(); + const options = setupOptions([retry]); + + await resolveSavedObjectsImportErrors(options); + expect(createObjectsFilter).toHaveBeenCalledWith([retry]); }); - expect(result).toMatchInlineSnapshot(` - Object { - "success": true, - "successCount": 1, - } - `); - expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(` - [MockFunction] { - "calls": Array [ - Array [ - Array [ - Object { - "attributes": Object { - "title": "My Dashboard", - }, - "id": "4", - "migrationVersion": Object {}, - "references": Array [ - Object { - "id": "13", - "name": "panel_0", - "type": "visualization", - }, - ], - "type": "dashboard", - }, - ], - Object { - "namespace": undefined, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], - } - `); - }); - test('extracts errors for conflicts', async () => { - const readStream = new Readable({ - objectMode: true, - read() { - savedObjects.forEach((obj) => this.push(obj)); - this.push(null); - }, + test('collects saved objects from stream', async () => { + const options = setupOptions(); + const supportedTypes = ['foo']; + typeRegistry.getImportableAndExportableTypes.mockReturnValue( + supportedTypes.map((name) => ({ name })) as SavedObjectsType[] + ); + + await resolveSavedObjectsImportErrors(options); + expect(typeRegistry.getImportableAndExportableTypes).toHaveBeenCalled(); + // expect(createObjectsFilter).toHaveBeenCalled(); + const filter = getMockFn(createObjectsFilter).mock.results[0].value; + const collectSavedObjectsOptions = { readStream, objectLimit, filter, supportedTypes }; + expect(collectSavedObjects).toHaveBeenCalledWith(collectSavedObjectsOptions); }); - savedObjectsClient.bulkCreate.mockResolvedValue({ - saved_objects: savedObjects.map(({ type, id }) => ({ - type, - id, - error: SavedObjectsErrorHelpers.createConflictError(type, id).output.payload, - attributes: {}, - references: [], - })), + + test('validates references', async () => { + const options = setupOptions(); + const collectedObjects = [createObject()]; + getMockFn(collectSavedObjects).mockResolvedValue({ errors: [], collectedObjects }); + + await resolveSavedObjectsImportErrors(options); + expect(validateReferences).toHaveBeenCalledWith( + collectedObjects, + savedObjectsClient, + namespace + ); }); - const result = await resolveSavedObjectsImportErrors({ - readStream, - objectLimit: 4, - retries: savedObjects.map((obj) => ({ - type: obj.type, - id: obj.id, - overwrite: false, - replaceReferences: [], - })), - savedObjectsClient, - typeRegistry: createTypeRegistryMock(supportedTypes), + + test('uses `retries` to replace references of collected objects before validating', async () => { + const object = createObject([{ type: 'bar-type', id: 'abc', name: 'some name' }]); + const retry = createRetry({ + id: object.id, + replaceReferences: [{ type: 'bar-type', from: 'abc', to: 'def' }], + }); + const options = setupOptions([retry]); + getMockFn(collectSavedObjects).mockResolvedValue({ errors: [], collectedObjects: [object] }); + + await resolveSavedObjectsImportErrors(options); + const objectWithReplacedReferences = { + ...object, + references: [{ ...object.references[0], id: 'def' }], + }; + expect(validateReferences).toHaveBeenCalledWith( + [objectWithReplacedReferences], + savedObjectsClient, + namespace + ); }); - expect(result).toMatchInlineSnapshot(` - Object { - "errors": Array [ - Object { - "error": Object { - "type": "conflict", - }, - "id": "1", - "title": "My Index Pattern", - "type": "index-pattern", - }, - Object { - "error": Object { - "type": "conflict", - }, - "id": "2", - "title": "My Search", - "type": "search", - }, - Object { - "error": Object { - "type": "conflict", - }, - "id": "3", - "title": "My Visualization", - "type": "visualization", - }, - Object { - "error": Object { - "type": "conflict", - }, - "id": "4", - "title": "My Dashboard", - "type": "dashboard", - }, - ], - "success": false, - "successCount": 0, - } - `); - }); - test('validates references', async () => { - const readStream = new Readable({ - objectMode: true, - read() { - this.push({ - id: '1', - type: 'search', - attributes: { - title: 'My Search', - }, - references: [ - { - name: 'ref_0', - type: 'index-pattern', - id: '2', - }, - ], - }); - this.push({ - id: '3', - type: 'visualization', - attributes: { - title: 'My Visualization', - }, - references: [ - { - name: 'ref_0', - type: 'search', - id: '1', - }, - ], - }); - this.push(null); - }, + test('checks conflicts', async () => { + const retries = [createRetry()]; + const options = setupOptions(retries); + const filteredObjects = [createObject()]; + getMockFn(validateReferences).mockResolvedValue({ errors: [], filteredObjects }); + + await resolveSavedObjectsImportErrors(options); + const opts = { typeRegistry, retries }; + expect(getImportIdMapForRetries).toHaveBeenCalledWith(filteredObjects, opts); }); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - type: 'index-pattern', - id: '2', - error: SavedObjectsErrorHelpers.createGenericNotFoundError('index-pattern', '2').output - .payload, - attributes: {}, - references: [], - }, - ], + + test('splits objects to ovewrite from those not to overwrite', async () => { + const retries = [createRetry()]; + const options = setupOptions(retries); + const filteredObjects = [createObject()]; + getMockFn(validateReferences).mockResolvedValue({ + errors: [], + filteredObjects, + }); + + await resolveSavedObjectsImportErrors(options); + expect(splitOverwrites).toHaveBeenCalledWith(filteredObjects, retries); }); - const result = await resolveSavedObjectsImportErrors({ - readStream, - objectLimit: 2, - retries: [ - { - type: 'search', - id: '1', - overwrite: false, - replaceReferences: [], - }, - { - type: 'visualization', - id: '3', - overwrite: false, - replaceReferences: [], - }, - ], - savedObjectsClient, - typeRegistry: createTypeRegistryMock(supportedTypes), + + test('creates saved objects', async () => { + const options = setupOptions(); + const importIdMap = new Map(); + getMockFn(getImportIdMapForRetries).mockResolvedValue(importIdMap); + const objectsToOverwrite = [createObject()]; + const objectsToNotOverwrite = [createObject()]; + getMockFn(splitOverwrites).mockReturnValue({ objectsToOverwrite, objectsToNotOverwrite }); + + await resolveSavedObjectsImportErrors(options); + const createSavedObjectsOptions = { savedObjectsClient, importIdMap, namespace }; + expect(createSavedObjects).toHaveBeenNthCalledWith(1, objectsToOverwrite, { + ...createSavedObjectsOptions, + overwrite: true, + }); + expect(createSavedObjects).toHaveBeenNthCalledWith( + 2, + objectsToNotOverwrite, + createSavedObjectsOptions + ); }); - expect(result).toMatchInlineSnapshot(` - Object { - "errors": Array [ - Object { - "error": Object { - "blocking": Array [ - Object { - "id": "3", - "type": "visualization", - }, - ], - "references": Array [ - Object { - "id": "2", - "type": "index-pattern", - }, - ], - "type": "missing_references", - }, - "id": "1", - "title": "My Search", - "type": "search", - }, - ], - "success": false, - "successCount": 0, - } - `); - expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` - [MockFunction] { - "calls": Array [ - Array [ - Array [ - Object { - "fields": Array [ - "id", - ], - "id": "2", - "type": "index-pattern", - }, - ], - Object { - "namespace": undefined, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], - } - `); }); - test('validates object types', async () => { - const readStream = new Readable({ - objectMode: true, - read() { - savedObjects.forEach((obj) => this.push(obj)); - this.push({ id: '1', type: 'wigwags', attributes: { title: 'my title' }, references: [] }); - this.push(null); - }, - }); - savedObjectsClient.bulkCreate.mockResolvedValue({ - saved_objects: [], - }); - const result = await resolveSavedObjectsImportErrors({ - readStream, - objectLimit: 5, - retries: [ - { - id: 'i', - type: 'wigwags', - overwrite: false, - replaceReferences: [], - }, - ], - savedObjectsClient, - typeRegistry: createTypeRegistryMock(supportedTypes), + describe('results', () => { + test('returns success=true if no errors occurred', async () => { + const options = setupOptions(); + + const result = await resolveSavedObjectsImportErrors(options); + expect(result).toEqual({ success: true, successCount: 0 }); }); - expect(result).toMatchInlineSnapshot(` - Object { - "errors": Array [ - Object { - "error": Object { - "type": "unsupported_type", - }, - "id": "1", - "title": "my title", - "type": "wigwags", - }, - ], - "success": false, - "successCount": 0, - } - `); - expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(`[MockFunction]`); - }); - test('uses namespace when provided', async () => { - const readStream = new Readable({ - objectMode: true, - read() { - savedObjects.forEach((obj) => this.push(obj)); - this.push(null); - }, + test('returns success=false if an error occurred', async () => { + const options = setupOptions(); + const errors = [createError()]; + getMockFn(collectSavedObjects).mockResolvedValue({ errors, collectedObjects: [] }); + + const result = await resolveSavedObjectsImportErrors(options); + expect(result).toEqual({ success: false, successCount: 0, errors }); }); - savedObjectsClient.bulkCreate.mockResolvedValue({ - saved_objects: savedObjects.filter((obj) => obj.type === 'index-pattern' && obj.id === '1'), + + test('handles a mix of successes and errors', async () => { + const options = setupOptions(); + const errors = [createError()]; + const obj1 = createObject(); + const obj2 = { ...createObject(), newId: 'some-newId' }; + getMockFn(createSavedObjects).mockResolvedValueOnce({ + errors, + createdObjects: [obj1], + }); + getMockFn(createSavedObjects).mockResolvedValueOnce({ + errors: [], + createdObjects: [obj2], + }); + + const result = await resolveSavedObjectsImportErrors(options); + // successResults only includes the imported object's type, id, and newId (if a new one was generated) + const successResults = [ + { type: obj1.type, id: obj1.id }, + { type: obj2.type, id: obj2.id, newId: obj2.newId }, + ]; + expect(result).toEqual({ success: false, successCount: 2, successResults, errors }); }); - const result = await resolveSavedObjectsImportErrors({ - readStream, - objectLimit: 4, - retries: [ - { - type: 'index-pattern', - id: '1', - overwrite: true, - replaceReferences: [], - }, - ], - savedObjectsClient, - typeRegistry: createTypeRegistryMock(supportedTypes), - namespace: 'foo', + + test('accumulates multiple errors', async () => { + const options = setupOptions(); + const errors = [createError(), createError(), createError(), createError()]; + getMockFn(collectSavedObjects).mockResolvedValue({ + errors: [errors[0]], + collectedObjects: [], + }); + getMockFn(validateReferences).mockResolvedValue({ errors: [errors[1]], filteredObjects: [] }); + getMockFn(createSavedObjects).mockResolvedValueOnce({ + errors: [errors[2]], + createdObjects: [], + }); + getMockFn(createSavedObjects).mockResolvedValueOnce({ + errors: [errors[3]], + createdObjects: [], + }); + + const result = await resolveSavedObjectsImportErrors(options); + expect(result).toEqual({ success: false, successCount: 0, errors }); }); - expect(result).toMatchInlineSnapshot(` - Object { - "success": true, - "successCount": 1, - } - `); - expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( - [ - { - attributes: { title: 'My Index Pattern' }, - id: '1', - migrationVersion: {}, - references: [], - type: 'index-pattern', - }, - ], - { namespace: 'foo', overwrite: true } - ); }); }); 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 4a8e015bffac7..bfecd04a4c9d3 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.ts @@ -18,7 +18,6 @@ */ import { collectSavedObjects } from './collect_saved_objects'; import { createObjectsFilter } from './create_objects_filter'; -import { extractErrors } from './extract_errors'; import { splitOverwrites } from './split_overwrites'; import { SavedObjectsImportError, @@ -27,6 +26,9 @@ import { } from './types'; import { validateReferences } from './validate_references'; import { validateRetries } from './validate_retries'; +import { createSavedObjects } from './create_saved_objects'; +import { getImportIdMapForRetries } from './check_conflicts'; +import { SavedObject } from '../types'; /** * Resolve and return saved object import errors. @@ -86,40 +88,36 @@ export async function resolveSavedObjectsImportErrors({ } // Validate references - const { filteredObjects, errors: validationErrors } = await validateReferences( + const { filteredObjects, errors: referenceErrors } = await validateReferences( objectsToResolve, savedObjectsClient, namespace ); - errorAccumulator = [...errorAccumulator, ...validationErrors]; + errorAccumulator = [...errorAccumulator, ...referenceErrors]; + + // Check multi-namespace object types for regular conflicts and ambiguous conflicts + const importIdMap = await getImportIdMapForRetries(filteredObjects, { typeRegistry, retries }); // Bulk create in two batches, overwrites and non-overwrites - const { objectsToOverwrite, objectsToNotOverwrite } = splitOverwrites(filteredObjects, retries); - if (objectsToOverwrite.length) { - const bulkCreateResult = await savedObjectsClient.bulkCreate(objectsToOverwrite, { - overwrite: true, - namespace, - }); - errorAccumulator = [ - ...errorAccumulator, - ...extractErrors(bulkCreateResult.saved_objects, objectsToOverwrite), - ]; - successCount += bulkCreateResult.saved_objects.filter((obj) => !obj.error).length; - } - if (objectsToNotOverwrite.length) { - const bulkCreateResult = await savedObjectsClient.bulkCreate(objectsToNotOverwrite, { - namespace, - }); - errorAccumulator = [ - ...errorAccumulator, - ...extractErrors(bulkCreateResult.saved_objects, objectsToNotOverwrite), + let successResults: Array<{ type: string; id: string; newId?: string }> = []; + const bulkCreateObjects = async (objects: Array>, overwrite?: boolean) => { + const options = { savedObjectsClient, importIdMap, namespace, overwrite }; + const { createdObjects, errors: bulkCreateErrors } = await createSavedObjects(objects, options); + errorAccumulator = [...errorAccumulator, ...bulkCreateErrors]; + successCount += createdObjects.length; + successResults = [ + ...successResults, + ...createdObjects.map(({ type, id, newId }) => ({ type, id, ...(newId && { newId }) })), ]; - successCount += bulkCreateResult.saved_objects.filter((obj) => !obj.error).length; - } + }; + const { objectsToOverwrite, objectsToNotOverwrite } = splitOverwrites(filteredObjects, retries); + await bulkCreateObjects(objectsToOverwrite, true); + await bulkCreateObjects(objectsToNotOverwrite); return { successCount, success: errorAccumulator.length === 0, - ...(errorAccumulator.length ? { errors: errorAccumulator } : {}), + ...(successResults.length && { successResults }), + ...(errorAccumulator.length && { errors: errorAccumulator }), }; } diff --git a/src/core/server/saved_objects/import/types.ts b/src/core/server/saved_objects/import/types.ts index f8cfa8c5ac015..362261e713173 100644 --- a/src/core/server/saved_objects/import/types.ts +++ b/src/core/server/saved_objects/import/types.ts @@ -29,6 +29,10 @@ export interface SavedObjectsImportRetry { type: string; id: string; overwrite: boolean; + /** + * The object ID that will be overwritten. Only used if `overwrite` == true. This is required to resolve ambiguous conflicts. + */ + idToOverwrite?: string; replaceReferences: Array<{ type: string; from: string; @@ -44,6 +48,16 @@ export interface SavedObjectsImportConflictError { type: 'conflict'; } +/** + * Represents a failure to import due to a conflict, which can be resolved in different ways with an overwrite. + * @public + */ +export interface SavedObjectsImportAmbiguousConflictError { + type: 'ambiguous_conflict'; + sources: Array<{ id: string; title?: string; updatedAt?: string }>; + destinations: Array<{ id: string; title?: string; updatedAt?: string }>; +} + /** * Represents a failure to import due to having an unsupported saved object type. * @public @@ -88,11 +102,25 @@ export interface SavedObjectsImportError { title?: string; error: | SavedObjectsImportConflictError + | SavedObjectsImportAmbiguousConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError; } +/** + * Represents a successful import. + * @public + */ +export interface SavedObjectsImportSuccess { + id: string; + type: string; + /** + * If `newId` is specified, the new object has a new ID that is different from the import ID. + */ + newId?: string; +} + /** * The response describing the result of an import. * @public @@ -100,6 +128,7 @@ export interface SavedObjectsImportError { export interface SavedObjectsImportResponse { success: boolean; successCount: number; + successResults?: SavedObjectsImportSuccess[]; errors?: SavedObjectsImportError[]; } diff --git a/src/core/server/saved_objects/routes/resolve_import_errors.ts b/src/core/server/saved_objects/routes/resolve_import_errors.ts index 361b6ec862a8a..6e505cb862689 100644 --- a/src/core/server/saved_objects/routes/resolve_import_errors.ts +++ b/src/core/server/saved_objects/routes/resolve_import_errors.ts @@ -48,19 +48,29 @@ export const registerResolveImportErrorsRoute = (router: IRouter, config: SavedO body: schema.object({ file: schema.stream(), retries: schema.arrayOf( - schema.object({ - type: schema.string(), - id: schema.string(), - overwrite: schema.boolean({ defaultValue: false }), - replaceReferences: schema.arrayOf( - schema.object({ - type: schema.string(), - from: schema.string(), - to: schema.string(), - }), - { defaultValue: [] } - ), - }) + schema.object( + { + type: schema.string(), + id: schema.string(), + overwrite: schema.boolean({ defaultValue: false }), + idToOverwrite: schema.maybe(schema.string()), + replaceReferences: schema.arrayOf( + schema.object({ + type: schema.string(), + from: schema.string(), + to: schema.string(), + }), + { defaultValue: [] } + ), + }, + { + validate: (object) => { + if (object.idToOverwrite && !object.overwrite) { + return 'cannot use [idToOverwrite] without [overwrite]'; + } + }, + } + ) ), }), }, diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index 9ff5cc9deb12c..8023d420c79f6 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -24,7 +24,9 @@ import { PropertyValidators } from './validation'; export { SavedObjectsImportResponse, + SavedObjectsImportSuccess, SavedObjectsImportConflictError, + SavedObjectsImportAmbiguousConflictError, SavedObjectsImportUnsupportedTypeError, SavedObjectsImportMissingReferencesError, SavedObjectsImportUnknownError, diff --git a/src/plugins/saved_objects_management/public/lib/process_import_response.test.ts b/src/plugins/saved_objects_management/public/lib/process_import_response.test.ts index c1a153b800550..cd35d4d726400 100644 --- a/src/plugins/saved_objects_management/public/lib/process_import_response.test.ts +++ b/src/plugins/saved_objects_management/public/lib/process_import_response.test.ts @@ -19,6 +19,7 @@ import { SavedObjectsImportConflictError, + SavedObjectsImportAmbiguousConflictError, SavedObjectsImportUnknownError, SavedObjectsImportMissingReferencesError, } from 'src/core/public'; @@ -35,7 +36,7 @@ describe('processImportResponse()', () => { expect(result.importCount).toBe(0); }); - test('conflict errors get added to failedImports', () => { + test('conflict errors get added to failedImports and result in idle status', () => { const response = { success: false, successCount: 0, @@ -63,9 +64,41 @@ describe('processImportResponse()', () => { }, ] `); + expect(result.status).toBe('idle'); }); - test('unknown errors get added to failedImports', () => { + test('ambiguous conflict errors get added to failedImports and result in idle status', () => { + const response = { + success: false, + successCount: 0, + errors: [ + { + type: 'a', + id: '1', + error: { + type: 'ambiguous_conflict', + } as SavedObjectsImportAmbiguousConflictError, + }, + ], + }; + const result = processImportResponse(response); + expect(result.failedImports).toMatchInlineSnapshot(` + Array [ + Object { + "error": Object { + "type": "ambiguous_conflict", + }, + "obj": Object { + "id": "1", + "type": "a", + }, + }, + ] + `); + expect(result.status).toBe('idle'); + }); + + test('unknown errors get added to failedImports and result in success status', () => { const response = { success: false, successCount: 0, @@ -93,9 +126,10 @@ describe('processImportResponse()', () => { }, ] `); + expect(result.status).toBe('success'); }); - test('missing references get added to failedImports', () => { + test('missing references get added to failedImports and result in idle status', () => { const response = { success: false, successCount: 0, @@ -135,5 +169,6 @@ describe('processImportResponse()', () => { }, ] `); + expect(result.status).toBe('idle'); }); }); diff --git a/src/plugins/saved_objects_management/public/lib/process_import_response.ts b/src/plugins/saved_objects_management/public/lib/process_import_response.ts index 4725000aa9d55..ece8c924f8885 100644 --- a/src/plugins/saved_objects_management/public/lib/process_import_response.ts +++ b/src/plugins/saved_objects_management/public/lib/process_import_response.ts @@ -20,6 +20,7 @@ import { SavedObjectsImportResponse, SavedObjectsImportConflictError, + SavedObjectsImportAmbiguousConflictError, SavedObjectsImportUnsupportedTypeError, SavedObjectsImportMissingReferencesError, SavedObjectsImportUnknownError, @@ -30,6 +31,7 @@ export interface FailedImport { obj: Pick; error: | SavedObjectsImportConflictError + | SavedObjectsImportAmbiguousConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError; @@ -48,6 +50,9 @@ export interface ProcessedImportResponse { conflictedSearchDocs: undefined; } +const isConflict = ({ type }: FailedImport['error']) => + type === 'conflict' || type === 'ambiguous_conflict'; + export function processImportResponse( response: SavedObjectsImportResponse ): ProcessedImportResponse { @@ -80,8 +85,7 @@ export function processImportResponse( // Import won't be successful in the scenario unmatched references exist, import API returned errors of type unknown or import API // returned errors of type missing_references. status: - unmatchedReferences.size === 0 && - !failedImports.some((issue) => issue.error.type === 'conflict') + unmatchedReferences.size === 0 && !failedImports.some((issue) => isConflict(issue.error)) ? 'success' : 'idle', importCount: response.successCount, diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts index 7c45c4596b09d..9f1edadb97760 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts @@ -7,6 +7,7 @@ import { SavedObjectsImportResponse, SavedObjectsImportOptions, SavedObjectsExportOptions, + SavedObjectsImportSuccess, } from 'src/core/server'; import { copySavedObjectsToSpacesFactory } from './copy_to_spaces'; import { Readable } from 'stream'; @@ -76,6 +77,9 @@ describe('copySavedObjectsToSpaces', () => { const response: SavedObjectsImportResponse = { success: true, successCount: setupOpts.objects.length, + successResults: [ + ('Some success(es) occurred!' as unknown) as SavedObjectsImportSuccess, + ], }; return Promise.resolve(response); @@ -136,11 +140,17 @@ describe('copySavedObjectsToSpaces', () => { "errors": undefined, "success": true, "successCount": 3, + "successResults": Array [ + "Some success(es) occurred!", + ], }, "destination2": Object { "errors": undefined, "success": true, "successCount": 3, + "successResults": Array [ + "Some success(es) occurred!", + ], }, } `); @@ -358,6 +368,7 @@ describe('copySavedObjectsToSpaces', () => { return Promise.resolve({ success: true, successCount: 3, + successResults: [('Some success(es) occurred!' as unknown) as SavedObjectsImportSuccess], }); }, }); @@ -398,11 +409,17 @@ describe('copySavedObjectsToSpaces', () => { "errors": undefined, "success": true, "successCount": 3, + "successResults": Array [ + "Some success(es) occurred!", + ], }, "non-existent-space": Object { "errors": undefined, "success": true, "successCount": 3, + "successResults": Array [ + "Some success(es) occurred!", + ], }, } `); diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts index cd1d119cd0623..997b2239e73e9 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts @@ -60,6 +60,7 @@ export function copySavedObjectsToSpacesFactory( return { success: importResponse.success, successCount: importResponse.successCount, + successResults: importResponse.successResults, errors: importResponse.errors, }; } catch (error) { diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts index e4a30e79436ac..f8664349fc9cd 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts @@ -7,6 +7,7 @@ import { SavedObjectsImportResponse, SavedObjectsResolveImportErrorsOptions, SavedObjectsExportOptions, + SavedObjectsImportSuccess, } from 'src/core/server'; import { coreMock, httpServerMock } from 'src/core/server/mocks'; import { Readable } from 'stream'; @@ -77,6 +78,9 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { const response: SavedObjectsImportResponse = { success: true, successCount: setupOpts.objects.length, + successResults: [ + ('Some success(es) occurred!' as unknown) as SavedObjectsImportSuccess, + ], }; return response; @@ -152,11 +156,17 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { "errors": undefined, "success": true, "successCount": 3, + "successResults": Array [ + "Some success(es) occurred!", + ], }, "destination2": Object { "errors": undefined, "success": true, "successCount": 3, + "successResults": Array [ + "Some success(es) occurred!", + ], }, } `); @@ -388,6 +398,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { return Promise.resolve({ success: true, successCount: 3, + successResults: [('Some success(es) occurred!' as unknown) as SavedObjectsImportSuccess], }); }, }); @@ -446,11 +457,17 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { "errors": undefined, "success": true, "successCount": 3, + "successResults": Array [ + "Some success(es) occurred!", + ], }, "non-existent-space": Object { "errors": undefined, "success": true, "successCount": 3, + "successResults": Array [ + "Some success(es) occurred!", + ], }, } `); diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts index 33b0f7cabd594..8b78b07e08b78 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts @@ -5,7 +5,7 @@ */ import { Readable } from 'stream'; -import { SavedObject, CoreStart, KibanaRequest } from 'src/core/server'; +import { SavedObject, CoreStart, KibanaRequest, SavedObjectsImportRetry } from 'src/core/server'; import { exportSavedObjectsToStream, resolveSavedObjectsImportErrors, @@ -44,12 +44,7 @@ export function resolveCopySavedObjectsToSpacesConflictsFactory( const resolveConflictsForSpace = async ( spaceId: string, objectsStream: Readable, - retries: Array<{ - type: string; - id: string; - overwrite: boolean; - replaceReferences: Array<{ type: string; from: string; to: string }>; - }> + retries: SavedObjectsImportRetry[] ) => { try { const importResponse = await resolveSavedObjectsImportErrors({ @@ -64,6 +59,7 @@ export function resolveCopySavedObjectsToSpacesConflictsFactory( return { success: importResponse.success, successCount: importResponse.successCount, + successResults: importResponse.successResults, errors: importResponse.errors, }; } catch (error) { diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/types.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/types.ts index 1bbe5aa6625b0..41606126516ea 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/types.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/types.ts @@ -5,7 +5,11 @@ */ import { Payload } from 'boom'; -import { SavedObjectsImportError } from 'src/core/server'; +import { + SavedObjectsImportSuccess, + SavedObjectsImportError, + SavedObjectsImportRetry, +} from 'src/core/server'; export interface CopyOptions { objects: Array<{ type: string; id: string }>; @@ -17,7 +21,7 @@ export interface ResolveConflictsOptions { objects: Array<{ type: string; id: string }>; includeReferences: boolean; retries: { - [spaceId: string]: Array<{ type: string; id: string; overwrite: boolean }>; + [spaceId: string]: Array>; }; } @@ -25,6 +29,7 @@ export interface CopyResponse { [spaceId: string]: { success: boolean; successCount: number; + successResults?: SavedObjectsImportSuccess[]; errors?: Array; }; } diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts index f7879b61df123..b452c923fe091 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts @@ -308,6 +308,30 @@ describe('copy to space', () => { ).toThrowErrorMatchingInlineSnapshot(`"[objects]: duplicate objects are not allowed"`); }); + it(`does not allow "idToOverwrite" to be used without "overwrite"`, async () => { + const payload = { + retries: { + ['a-space']: [ + { + type: 'foo', + id: 'bar', + overwrite: false, + idToOverwrite: 'baz', + }, + ], + }, + objects: [{ type: 'foo', id: 'bar' }], + }; + + const { resolveConflicts } = await setup(); + + expect(() => + (resolveConflicts.routeValidation.body as ObjectType).validate(payload) + ).toThrowErrorMatchingInlineSnapshot( + `"[retries.a-space.0]: cannot use [idToOverwrite] without [overwrite]"` + ); + }); + it(`requires well-formed space ids`, async () => { const payload = { retries: { diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts index 4c9f62503a21b..39d4c29416956 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts @@ -101,11 +101,21 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { }, }), schema.arrayOf( - schema.object({ - type: schema.string(), - id: schema.string(), - overwrite: schema.boolean({ defaultValue: false }), - }) + schema.object( + { + type: schema.string(), + id: schema.string(), + overwrite: schema.boolean({ defaultValue: false }), + idToOverwrite: schema.maybe(schema.string()), + }, + { + validate: (object) => { + if (object.idToOverwrite && !object.overwrite) { + return 'cannot use [idToOverwrite] without [overwrite]'; + } + }, + } + ) ) ), objects: schema.arrayOf( From 924ef9667c9489311a238ab2f073369c8af82c9b Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Wed, 6 May 2020 11:59:14 -0400 Subject: [PATCH 09/55] Server-side support for "duplicate" resolution of import conflicts Saved object import that resulted in a Conflict error previously had one resolution, which is "overwrite". This commit adds another resolution, "duplicate", which will create a new object with a different ID instead. --- .../import/check_conflicts.test.ts | 20 ++++++++++++++-- .../saved_objects/import/check_conflicts.ts | 7 ++++-- .../import/create_saved_objects.test.ts | 8 +++---- .../import/create_saved_objects.ts | 4 ++-- src/core/server/saved_objects/import/types.ts | 9 +++++++ .../routes/resolve_import_errors.ts | 3 +++ .../routes/api/external/copy_to_space.test.ts | 24 +++++++++++++++++++ .../routes/api/external/copy_to_space.ts | 3 +++ 8 files changed, 68 insertions(+), 10 deletions(-) diff --git a/src/core/server/saved_objects/import/check_conflicts.test.ts b/src/core/server/saved_objects/import/check_conflicts.test.ts index 9481292b326ae..36aef5549ccde 100644 --- a/src/core/server/saved_objects/import/check_conflicts.test.ts +++ b/src/core/server/saved_objects/import/check_conflicts.test.ts @@ -17,6 +17,7 @@ * under the License. */ +import { mockUuidv4 } from './__mocks__'; import { SavedObjectsClientContract, SavedObjectReference, @@ -460,6 +461,9 @@ describe('#getImportIdMapForRetries', () => { ): SavedObjectsImportRetry => { return { type, id, overwrite: true, idToOverwrite, replaceReferences: [] }; }; + const createDuplicateRetry = (obj: { type: string; id: string }): SavedObjectsImportRetry => { + return { type: obj.type, id: obj.id, overwrite: false, duplicate: true, replaceReferences: [] }; + }; test('returns expected results', async () => { const obj1 = createObject(OTHER_TYPE, 'id-1'); @@ -468,7 +472,9 @@ describe('#getImportIdMapForRetries', () => { const obj4 = createObject(MULTI_NS_TYPE, 'id-4'); const obj5 = createObject(MULTI_NS_TYPE, 'id-5'); const obj6 = createObject(MULTI_NS_TYPE, 'id-6'); - const objects = [obj1, obj2, obj3, obj4, obj5, obj6]; + const obj7 = createObject(OTHER_TYPE, 'id-7'); + const obj8 = createObject(MULTI_NS_TYPE, 'id-8'); + const objects = [obj1, obj2, obj3, obj4, obj5, obj6, obj7, obj8]; const retries = [ // all three overwrite retries for non-multi-namespace types are ignored; // retries for non-multi-namespace these should not have `idToOverwrite` specified, but we test it here for posterity @@ -478,10 +484,20 @@ describe('#getImportIdMapForRetries', () => { createOverwriteRetry(obj4), // retries that do not have `idToOverwrite` specified are ignored createOverwriteRetry(obj5, obj5.id), // retries that have `id` that matches `idToOverwrite` are ignored createOverwriteRetry(obj6, 'id-Y'), // this retry will get added to the `importIdMap`! + createDuplicateRetry(obj7), // this retry will get added to the `importIdMap`! + createDuplicateRetry(obj8), // this retry will get added to the `importIdMap`! ]; const options = setupOptions(retries); + mockUuidv4.mockReturnValueOnce(`new-id-for-${obj7.id}`); + mockUuidv4.mockReturnValueOnce(`new-id-for-${obj8.id}`); const checkConflictsResult = await getImportIdMapForRetries(objects, options); - expect(checkConflictsResult).toEqual(new Map([[`${obj6.type}:${obj6.id}`, { id: 'id-Y' }]])); + expect(checkConflictsResult).toEqual( + new Map([ + [`${obj6.type}:${obj6.id}`, { id: 'id-Y' }], + [`${obj7.type}:${obj7.id}`, { id: `new-id-for-${obj7.id}`, omitOriginId: true }], + [`${obj8.type}:${obj8.id}`, { id: `new-id-for-${obj8.id}`, omitOriginId: true }], + ]) + ); }); }); diff --git a/src/core/server/saved_objects/import/check_conflicts.ts b/src/core/server/saved_objects/import/check_conflicts.ts index 26f9795a46e80..1735dbb2b6dc6 100644 --- a/src/core/server/saved_objects/import/check_conflicts.ts +++ b/src/core/server/saved_objects/import/check_conflicts.ts @@ -19,6 +19,7 @@ /* eslint-disable @typescript-eslint/consistent-type-definitions */ +import { v4 as uuidv4 } from 'uuid'; import { SavedObject, SavedObjectsClientContract, @@ -196,12 +197,12 @@ export async function getImportIdMapForRetries( (acc, cur) => acc.set(`${cur.type}:${cur.id}`, cur), new Map() ); - const importIdMap = new Map(); + const importIdMap = new Map(); objects.forEach(({ type, id }) => { const retry = retryMap.get(`${type}:${id}`); if (retry) { - const { overwrite, idToOverwrite } = retry; + const { overwrite, idToOverwrite, duplicate } = retry; if ( overwrite && idToOverwrite && @@ -209,6 +210,8 @@ export async function getImportIdMapForRetries( typeRegistry.isMultiNamespace(type) ) { importIdMap.set(`${type}:${id}`, { id: idToOverwrite }); + } else if (!overwrite && duplicate) { + importIdMap.set(`${type}:${id}`, { id: uuidv4(), omitOriginId: true }); } } }); diff --git a/src/core/server/saved_objects/import/create_saved_objects.test.ts b/src/core/server/saved_objects/import/create_saved_objects.test.ts index b1129ec574270..c808928909b2b 100644 --- a/src/core/server/saved_objects/import/create_saved_objects.test.ts +++ b/src/core/server/saved_objects/import/create_saved_objects.test.ts @@ -45,7 +45,7 @@ const OTHER_TYPE = 'other'; */ const obj1 = createObject(MULTI_NS_TYPE, 'id-1', 'originId-a'); // -> success const obj2 = createObject(MULTI_NS_TYPE, 'id-2', 'originId-b'); // -> conflict -const obj3 = createObject(MULTI_NS_TYPE, 'id-3', 'originId-c'); // -> conflict (with known importId) +const obj3 = createObject(MULTI_NS_TYPE, 'id-3', 'originId-c'); // -> conflict (with known importId and omitOriginId=true) const obj4 = createObject(MULTI_NS_TYPE, 'id-4', 'originId-d'); // -> conflict (with known importId) const obj5 = createObject(MULTI_NS_TYPE, 'id-5', 'originId-e'); // -> unresolvable conflict const obj6 = createObject(MULTI_NS_TYPE, 'id-6'); // -> success @@ -62,7 +62,7 @@ const importId3 = 'id-foo'; const importId4 = 'id-bar'; const importId8 = 'id-baz'; const importIdMap = new Map([ - [`${obj3.type}:${obj3.id}`, { id: importId3 }], + [`${obj3.type}:${obj3.id}`, { id: importId3, omitOriginId: true }], [`${obj4.type}:${obj4.id}`, { id: importId4 }], [`${obj8.type}:${obj8.id}`, { id: importId8 }], ]); @@ -190,7 +190,7 @@ describe('#createSavedObjects', () => { await createSavedObjects(objs, options); expect(bulkCreate).toHaveBeenCalledTimes(1); // these three objects are transformed before being created, because they are included in the `importIdMap` - const x3 = { ...obj3, id: importId3 }; // this import object already has an originId + const x3 = { ...obj3, id: importId3, originId: undefined }; // this import object already has an originId, but the retry has omitOriginId=true const x4 = { ...obj4, id: importId4 }; // this import object already has an originId const x8 = { ...obj8, id: importId8, originId: obj8.id }; // this import object doesn't have an originId, so it is set before create const argObjs = [obj1, obj2, x3, x4, obj6, obj7, x8, obj10, obj11, obj12, obj13]; @@ -283,7 +283,7 @@ describe('#createSavedObjects', () => { await createSavedObjects(objs, options); expect(bulkCreate).toHaveBeenCalledTimes(2); // these three objects are transformed before being created, because they are included in the `importIdMap` - const x3 = { ...obj3, id: importId3 }; // this import object already has an originId + const x3 = { ...obj3, id: importId3, originId: undefined }; // this import object already has an originId, but the retry has omitOriginId=true const x4 = { ...obj4, id: importId4 }; // this import object already has an originId const x8 = { ...obj8, id: importId8, originId: obj8.id }; // this import object doesn't have an originId, so it is set before create const argObjs = [obj1, obj2, x3, x4, obj5, obj6, obj7, x8, obj9, obj10, obj11, obj12, obj13]; diff --git a/src/core/server/saved_objects/import/create_saved_objects.ts b/src/core/server/saved_objects/import/create_saved_objects.ts index e847f603e50f2..5904c30221d9c 100644 --- a/src/core/server/saved_objects/import/create_saved_objects.ts +++ b/src/core/server/saved_objects/import/create_saved_objects.ts @@ -25,7 +25,7 @@ import { extractErrors } from './extract_errors'; interface CreateSavedObjectsOptions { savedObjectsClient: SavedObjectsClientContract; - importIdMap: Map; + importIdMap: Map; namespace?: string; overwrite?: boolean; } @@ -74,7 +74,7 @@ export const createSavedObjects = async ( const importIdEntry = importIdMap.get(`${object.type}:${object.id}`); if (importIdEntry) { objectIdMap.set(`${object.type}:${importIdEntry.id}`, object); - const originId = object.originId ?? object.id; + const originId = importIdEntry.omitOriginId ? undefined : object.originId ?? object.id; return { ...object, id: importIdEntry.id, originId }; } return object; diff --git a/src/core/server/saved_objects/import/types.ts b/src/core/server/saved_objects/import/types.ts index 362261e713173..26b08c92afeb3 100644 --- a/src/core/server/saved_objects/import/types.ts +++ b/src/core/server/saved_objects/import/types.ts @@ -28,11 +28,20 @@ import { ISavedObjectTypeRegistry } from '..'; export interface SavedObjectsImportRetry { type: string; id: string; + /** + * Resolve an import conflict by overwriting a destination object. + * Note: this attribute is mutually-exclusive with `duplicate`. If both are enabled, `overwrite` takes precedence. + */ overwrite: boolean; /** * The object ID that will be overwritten. Only used if `overwrite` == true. This is required to resolve ambiguous conflicts. */ idToOverwrite?: string; + /** + * Resolve an import conflict by creating a duplicate object with a new (undefined) originId. + * Note: this attribute is mutually-exclusive with `overwrite`. If both are enabled, `overwrite` takes precedence. + */ + duplicate?: boolean; replaceReferences: Array<{ type: string; from: string; diff --git a/src/core/server/saved_objects/routes/resolve_import_errors.ts b/src/core/server/saved_objects/routes/resolve_import_errors.ts index 6e505cb862689..a192c60bf70d6 100644 --- a/src/core/server/saved_objects/routes/resolve_import_errors.ts +++ b/src/core/server/saved_objects/routes/resolve_import_errors.ts @@ -54,6 +54,7 @@ export const registerResolveImportErrorsRoute = (router: IRouter, config: SavedO id: schema.string(), overwrite: schema.boolean({ defaultValue: false }), idToOverwrite: schema.maybe(schema.string()), + duplicate: schema.boolean({ defaultValue: false }), replaceReferences: schema.arrayOf( schema.object({ type: schema.string(), @@ -67,6 +68,8 @@ export const registerResolveImportErrorsRoute = (router: IRouter, config: SavedO validate: (object) => { if (object.idToOverwrite && !object.overwrite) { return 'cannot use [idToOverwrite] without [overwrite]'; + } else if (object.overwrite && object.duplicate) { + return 'cannot use [overwrite] with [duplicate]'; } }, } diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts index b452c923fe091..446c58a922067 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts @@ -332,6 +332,30 @@ describe('copy to space', () => { ); }); + it(`does not allow "overwrite" to be used with "duplicate"`, async () => { + const payload = { + retries: { + ['a-space']: [ + { + type: 'foo', + id: 'bar', + overwrite: true, + duplicate: true, + }, + ], + }, + objects: [{ type: 'foo', id: 'bar' }], + }; + + const { resolveConflicts } = await setup(); + + expect(() => + (resolveConflicts.routeValidation.body as ObjectType).validate(payload) + ).toThrowErrorMatchingInlineSnapshot( + `"[retries.a-space.0]: cannot use [overwrite] with [duplicate]"` + ); + }); + it(`requires well-formed space ids`, async () => { const payload = { retries: { diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts index 39d4c29416956..3a2080111dbc1 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts @@ -107,11 +107,14 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { id: schema.string(), overwrite: schema.boolean({ defaultValue: false }), idToOverwrite: schema.maybe(schema.string()), + duplicate: schema.boolean({ defaultValue: false }), }, { validate: (object) => { if (object.idToOverwrite && !object.overwrite) { return 'cannot use [idToOverwrite] without [overwrite]'; + } else if (object.overwrite && object.duplicate) { + return 'cannot use [overwrite] with [duplicate]'; } }, } From 3741526ce1a477ac03932bd35f7dd17ebc1af39c Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Thu, 7 May 2020 14:32:37 -0400 Subject: [PATCH 10/55] Fix existing Spaces API integration tests and refactor These test suites do not currently include test cases for multi- namespace saved object types. However, the test suites broke due to the changes to the import APIs. I fixed the few problems that were present, and took this opportunity to refactor some of the test suites to make them more readable and maintainable. --- .../common/suites/copy_to_space.ts | 377 ++++++++--------- .../suites/resolve_copy_to_space_conflicts.ts | 232 ++++------- .../security_and_spaces/apis/copy_to_space.ts | 381 +++++------------- .../apis/resolve_copy_to_space_conflicts.ts | 227 +++-------- .../spaces_only/apis/copy_to_space.ts | 4 +- 5 files changed, 408 insertions(+), 813 deletions(-) diff --git a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts index ebec70793e8fd..4d9aee50c219d 100644 --- a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts @@ -53,28 +53,14 @@ interface SpaceBucket { } const INITIAL_COUNTS: Record> = { - [DEFAULT_SPACE_ID]: { - dashboard: 2, - visualization: 3, - 'index-pattern': 1, - }, - space_1: { - dashboard: 2, - visualization: 3, - 'index-pattern': 1, - }, - space_2: { - dashboard: 1, - }, + [DEFAULT_SPACE_ID]: { dashboard: 2, visualization: 3, 'index-pattern': 1 }, + space_1: { dashboard: 2, visualization: 3, 'index-pattern': 1 }, + space_2: { dashboard: 1 }, }; const getDestinationWithoutConflicts = () => 'space_2'; -const getDestinationWithConflicts = (originSpaceId?: string) => { - if (!originSpaceId || originSpaceId === DEFAULT_SPACE_ID) { - return 'space_1'; - } - return DEFAULT_SPACE_ID; -}; +const getDestinationWithConflicts = (originSpaceId?: string) => + !originSpaceId || originSpaceId === DEFAULT_SPACE_ID ? 'space_1' : DEFAULT_SPACE_ID; export function copyToSpaceTestSuiteFactory( es: any, @@ -86,27 +72,11 @@ export function copyToSpaceTestSuiteFactory( index: '.kibana', body: { size: 0, - query: { - terms: { - type: ['visualization', 'dashboard', 'index-pattern'], - }, - }, + query: { terms: { type: ['visualization', 'dashboard', 'index-pattern'] } }, aggs: { count: { - terms: { - field: 'namespace', - missing: DEFAULT_SPACE_ID, - size: 10, - }, - aggs: { - countByType: { - terms: { - field: 'type', - missing: 'UNKNOWN', - size: 10, - }, - }, - }, + terms: { field: 'namespace', missing: DEFAULT_SPACE_ID, size: 10 }, + aggs: { countByType: { terms: { field: 'type', missing: 'UNKNOWN', size: 10 } } }, }, }, }, @@ -135,13 +105,7 @@ export function copyToSpaceTestSuiteFactory( const { countByType } = spaceBucket; const expectedBuckets = Object.entries(expectedCounts).reduce((acc, entry) => { const [type, count] = entry; - return [ - ...acc, - { - key: type, - doc_count: count, - }, - ]; + return [...acc, { key: type, doc_count: count }]; }, [] as CountByTypeBucket[]); expectedBuckets.sort(bucketSorter); @@ -154,14 +118,6 @@ export function copyToSpaceTestSuiteFactory( }); }; - const expectRbacForbiddenResponse = async (resp: TestResponse) => { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: 'Unable to bulk_get dashboard', - }); - }; - const expectNotFoundResponse = async (resp: TestResponse) => { expect(resp.body).to.eql({ statusCode: 404, @@ -179,6 +135,7 @@ export function copyToSpaceTestSuiteFactory( [spaceId]: { success: true, successCount: 1, + successResults: [{ id: 'cts_dashboard', type: 'dashboard' }], }, } as CopyResponse); @@ -198,13 +155,22 @@ export function copyToSpaceTestSuiteFactory( 1 ); - const expectNoConflictsWithReferencesResult = async (resp: TestResponse) => { + const expectNoConflictsWithReferencesResult = (spaceId: string = DEFAULT_SPACE_ID) => async ( + resp: TestResponse + ) => { const destination = getDestinationWithoutConflicts(); const result = resp.body as CopyResponse; expect(result).to.eql({ [destination]: { success: true, successCount: 5, + successResults: [ + { id: 'cts_ip_1', type: 'index-pattern' }, + { id: `cts_vis_1_${spaceId}`, type: 'visualization' }, + { id: `cts_vis_2_${spaceId}`, type: 'visualization' }, + { id: 'cts_vis_3', type: 'visualization' }, + { id: 'cts_dashboard', type: 'dashboard' }, + ], }, } as CopyResponse); @@ -288,6 +254,13 @@ export function copyToSpaceTestSuiteFactory( [destination]: { success: true, successCount: 5, + successResults: [ + { id: 'cts_ip_1', type: 'index-pattern' }, + { id: `cts_vis_1_${spaceId}`, type: 'visualization' }, + { id: `cts_vis_2_${spaceId}`, type: 'visualization' }, + { id: 'cts_vis_3', type: 'visualization' }, + { id: 'cts_dashboard', type: 'dashboard' }, + ], }, } as CopyResponse); @@ -309,27 +282,25 @@ export function copyToSpaceTestSuiteFactory( const result = resp.body as CopyResponse; result[destination].errors!.sort(errorSorter); + const expectedSuccessResults = [ + { id: `cts_vis_1_${spaceId}`, type: 'visualization' }, + { id: `cts_vis_2_${spaceId}`, type: 'visualization' }, + ]; const expectedErrors = [ { - error: { - type: 'conflict', - }, + error: { type: 'conflict' }, id: 'cts_dashboard', title: `This is the ${spaceId} test space CTS dashboard`, type: 'dashboard', }, { - error: { - type: 'conflict', - }, + error: { type: 'conflict' }, id: 'cts_ip_1', title: `Copy to Space index pattern 1 from ${spaceId} space`, type: 'index-pattern', }, { - error: { - type: 'conflict', - }, + error: { type: 'conflict' }, id: 'cts_vis_3', title: `CTS vis 3 from ${spaceId} space`, type: 'visualization', @@ -341,6 +312,7 @@ export function copyToSpaceTestSuiteFactory( [destination]: { success: false, successCount: 2, + successResults: expectedSuccessResults, errors: expectedErrors, }, } as CopyResponse); @@ -363,162 +335,132 @@ export function copyToSpaceTestSuiteFactory( expect(['default', 'space_1']).to.contain(spaceId); }); - beforeEach(() => esArchiver.load('saved_objects/spaces')); - afterEach(() => esArchiver.unload('saved_objects/spaces')); - - it(`should return ${tests.noConflictsWithoutReferences.statusCode} when copying to space without conflicts or references`, async () => { - const destination = getDestinationWithoutConflicts(); - - await assertSpaceCounts(destination, INITIAL_COUNTS[destination]); - - return supertest - .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`) - .auth(user.username, user.password) - .send({ - objects: [ - { - type: 'dashboard', - id: 'cts_dashboard', - }, - ], - spaces: [destination], - includeReferences: false, - overwrite: false, - }) - .expect(tests.noConflictsWithoutReferences.statusCode) - .then(tests.noConflictsWithoutReferences.response); - }); - - it(`should return ${tests.noConflictsWithReferences.statusCode} when copying to space without conflicts with references`, async () => { - const destination = getDestinationWithoutConflicts(); - - await assertSpaceCounts(destination, INITIAL_COUNTS[destination]); - - return supertest - .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`) - .auth(user.username, user.password) - .send({ - objects: [ - { - type: 'dashboard', - id: 'cts_dashboard', - }, - ], - spaces: [destination], - includeReferences: true, - overwrite: false, - }) - .expect(tests.noConflictsWithReferences.statusCode) - .then(tests.noConflictsWithReferences.response); - }); - - it(`should return ${tests.withConflictsOverwriting.statusCode} when copying to space with conflicts when overwriting`, async () => { - const destination = getDestinationWithConflicts(spaceId); - - await assertSpaceCounts(destination, INITIAL_COUNTS[destination]); - - return supertest - .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`) - .auth(user.username, user.password) - .send({ - objects: [ - { - type: 'dashboard', - id: 'cts_dashboard', - }, - ], - spaces: [destination], - includeReferences: true, - overwrite: true, - }) - .expect(tests.withConflictsOverwriting.statusCode) - .then(tests.withConflictsOverwriting.response); - }); - - it(`should return ${tests.withConflictsWithoutOverwriting.statusCode} when copying to space with conflicts without overwriting`, async () => { - const destination = getDestinationWithConflicts(spaceId); - - await assertSpaceCounts(destination, INITIAL_COUNTS[destination]); - - return supertest - .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`) - .auth(user.username, user.password) - .send({ - objects: [ - { - type: 'dashboard', - id: 'cts_dashboard', - }, - ], - spaces: [destination], - includeReferences: true, - overwrite: false, - }) - .expect(tests.withConflictsWithoutOverwriting.statusCode) - .then(tests.withConflictsWithoutOverwriting.response); - }); - - it(`should return ${tests.multipleSpaces.statusCode} when copying to multiple spaces`, async () => { - const conflictDestination = getDestinationWithConflicts(spaceId); - const noConflictDestination = getDestinationWithoutConflicts(); - - return supertest - .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`) - .auth(user.username, user.password) - .send({ - objects: [ - { - type: 'dashboard', - id: 'cts_dashboard', - }, - ], - spaces: [conflictDestination, noConflictDestination], - includeReferences: true, - overwrite: true, - }) - .expect(tests.multipleSpaces.statusCode) - .then((response: TestResponse) => { - if (tests.multipleSpaces.statusCode === 200) { - expect(Object.keys(response.body).length).to.eql(2); + describe('single-namespace types', () => { + beforeEach(() => esArchiver.load('saved_objects/spaces')); + afterEach(() => esArchiver.unload('saved_objects/spaces')); + + const dashboardObject = { type: 'dashboard', id: 'cts_dashboard' }; + + it(`should return ${tests.noConflictsWithoutReferences.statusCode} when copying to space without conflicts or references`, async () => { + const destination = getDestinationWithoutConflicts(); + + await assertSpaceCounts(destination, INITIAL_COUNTS[destination]); + + return supertest + .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`) + .auth(user.username, user.password) + .send({ + objects: [dashboardObject], + spaces: [destination], + includeReferences: false, + overwrite: false, + }) + .expect(tests.noConflictsWithoutReferences.statusCode) + .then(tests.noConflictsWithoutReferences.response); + }); + + it(`should return ${tests.noConflictsWithReferences.statusCode} when copying to space without conflicts with references`, async () => { + const destination = getDestinationWithoutConflicts(); + + await assertSpaceCounts(destination, INITIAL_COUNTS[destination]); + + return supertest + .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`) + .auth(user.username, user.password) + .send({ + objects: [dashboardObject], + spaces: [destination], + includeReferences: true, + overwrite: false, + }) + .expect(tests.noConflictsWithReferences.statusCode) + .then(tests.noConflictsWithReferences.response); + }); + + it(`should return ${tests.withConflictsOverwriting.statusCode} when copying to space with conflicts when overwriting`, async () => { + const destination = getDestinationWithConflicts(spaceId); + + await assertSpaceCounts(destination, INITIAL_COUNTS[destination]); + + return supertest + .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`) + .auth(user.username, user.password) + .send({ + objects: [dashboardObject], + spaces: [destination], + includeReferences: true, + overwrite: true, + }) + .expect(tests.withConflictsOverwriting.statusCode) + .then(tests.withConflictsOverwriting.response); + }); + + it(`should return ${tests.withConflictsWithoutOverwriting.statusCode} when copying to space with conflicts without overwriting`, async () => { + const destination = getDestinationWithConflicts(spaceId); + + await assertSpaceCounts(destination, INITIAL_COUNTS[destination]); + + return supertest + .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`) + .auth(user.username, user.password) + .send({ + objects: [dashboardObject], + spaces: [destination], + includeReferences: true, + overwrite: false, + }) + .expect(tests.withConflictsWithoutOverwriting.statusCode) + .then(tests.withConflictsWithoutOverwriting.response); + }); + + it(`should return ${tests.multipleSpaces.statusCode} when copying to multiple spaces`, async () => { + const conflictDestination = getDestinationWithConflicts(spaceId); + const noConflictDestination = getDestinationWithoutConflicts(); + + return supertest + .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`) + .auth(user.username, user.password) + .send({ + objects: [dashboardObject], + spaces: [conflictDestination, noConflictDestination], + includeReferences: true, + overwrite: true, + }) + .expect(tests.multipleSpaces.statusCode) + .then((response: TestResponse) => { + if (tests.multipleSpaces.statusCode === 200) { + expect(Object.keys(response.body).length).to.eql(2); + return Promise.all([ + tests.multipleSpaces.noConflictsResponse({ + body: { [noConflictDestination]: response.body[noConflictDestination] }, + }), + tests.multipleSpaces.withConflictsResponse({ + body: { [conflictDestination]: response.body[conflictDestination] }, + }), + ]); + } + + // non-200 status codes will not have a response body broken out by space id, like above. return Promise.all([ - tests.multipleSpaces.noConflictsResponse({ - body: { - [noConflictDestination]: response.body[noConflictDestination], - }, - }), - tests.multipleSpaces.withConflictsResponse({ - body: { - [conflictDestination]: response.body[conflictDestination], - }, - }), + tests.multipleSpaces.noConflictsResponse(response), + tests.multipleSpaces.withConflictsResponse(response), ]); - } - - // non-200 status codes will not have a response body broken out by space id, like above. - return Promise.all([ - tests.multipleSpaces.noConflictsResponse(response), - tests.multipleSpaces.withConflictsResponse(response), - ]); - }); - }); - - it(`should return ${tests.nonExistentSpace.statusCode} when copying to non-existent space`, async () => { - return supertest - .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`) - .auth(user.username, user.password) - .send({ - objects: [ - { - type: 'dashboard', - id: 'cts_dashboard', - }, - ], - spaces: ['non_existent_space'], - includeReferences: false, - overwrite: true, - }) - .expect(tests.nonExistentSpace.statusCode) - .then(tests.nonExistentSpace.response); + }); + }); + + it(`should return ${tests.nonExistentSpace.statusCode} when copying to non-existent space`, async () => { + return supertest + .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`) + .auth(user.username, user.password) + .send({ + objects: [dashboardObject], + spaces: ['non_existent_space'], + includeReferences: false, + overwrite: true, + }) + .expect(tests.nonExistentSpace.statusCode) + .then(tests.nonExistentSpace.response); + }); }); }); }; @@ -534,7 +476,6 @@ export function copyToSpaceTestSuiteFactory( expectNoConflictsForNonExistentSpaceResult, createExpectWithConflictsOverwritingResult, createExpectWithConflictsWithoutOverwritingResult, - expectRbacForbiddenResponse, expectNotFoundResponse, createExpectUnauthorizedAtSpaceWithReferencesResult, createExpectUnauthorizedAtSpaceWithoutReferencesResult, diff --git a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts index 3529d8f3ae9c9..f693719ba810d 100644 --- a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts +++ b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts @@ -76,6 +76,7 @@ export function resolveCopyToSpaceConflictsSuite( [destination]: { success: true, successCount: 1, + successResults: [{ id: 'cts_vis_3', type: 'visualization' }], }, }); const [dashboard, visualization] = await getObjectsAtSpace(destination); @@ -94,6 +95,7 @@ export function resolveCopyToSpaceConflictsSuite( [destinationSpaceId]: { success: true, successCount: 1, + successResults: [{ id: 'cts_dashboard', type: 'dashboard' }], }, }); const [dashboard, visualization] = await getObjectsAtSpace(destinationSpaceId); @@ -119,9 +121,7 @@ export function resolveCopyToSpaceConflictsSuite( successCount: 0, errors: [ { - error: { - type: 'conflict', - }, + error: { type: 'conflict' }, id: 'cts_vis_3', title: `CTS vis 3 from ${sourceSpaceId} space`, type: 'visualization', @@ -149,9 +149,7 @@ export function resolveCopyToSpaceConflictsSuite( successCount: 0, errors: [ { - error: { - type: 'conflict', - }, + error: { type: 'conflict' }, id: 'cts_dashboard', title: `This is the ${sourceSpaceId} test space CTS dashboard`, type: 'dashboard', @@ -274,147 +272,87 @@ export function resolveCopyToSpaceConflictsSuite( expect(['default', 'space_1']).to.contain(spaceId); }); - beforeEach(() => esArchiver.load('saved_objects/spaces')); - afterEach(() => esArchiver.unload('saved_objects/spaces')); - - it(`should return ${tests.withReferencesNotOverwriting.statusCode} when not overwriting, with references`, async () => { - const destination = getDestinationSpace(spaceId); - - return supertestWithoutAuth - .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`) - .auth(user.username, user.password) - .send({ - objects: [ - { - type: 'dashboard', - id: 'cts_dashboard', - }, - ], - includeReferences: true, - retries: { - [destination]: [ - { - type: 'visualization', - id: 'cts_vis_3', - overwrite: false, - }, - ], - }, - }) - .expect(tests.withReferencesNotOverwriting.statusCode) - .then(tests.withReferencesNotOverwriting.response); - }); - - it(`should return ${tests.withReferencesOverwriting.statusCode} when overwriting, with references`, async () => { - const destination = getDestinationSpace(spaceId); - - return supertestWithoutAuth - .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`) - .auth(user.username, user.password) - .send({ - objects: [ - { - type: 'dashboard', - id: 'cts_dashboard', - }, - ], - includeReferences: true, - retries: { - [destination]: [ - { - type: 'visualization', - id: 'cts_vis_3', - overwrite: true, - }, - ], - }, - }) - .expect(tests.withReferencesOverwriting.statusCode) - .then(tests.withReferencesOverwriting.response); - }); - - it(`should return ${tests.withoutReferencesOverwriting.statusCode} when overwriting, without references`, async () => { - const destination = getDestinationSpace(spaceId); - - return supertestWithoutAuth - .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`) - .auth(user.username, user.password) - .send({ - objects: [ - { - type: 'dashboard', - id: 'cts_dashboard', - }, - ], - includeReferences: false, - retries: { - [destination]: [ - { - type: 'dashboard', - id: 'cts_dashboard', - overwrite: true, - }, - ], - }, - }) - .expect(tests.withoutReferencesOverwriting.statusCode) - .then(tests.withoutReferencesOverwriting.response); - }); - - it(`should return ${tests.withoutReferencesNotOverwriting.statusCode} when not overwriting, without references`, async () => { - const destination = getDestinationSpace(spaceId); - - return supertestWithoutAuth - .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`) - .auth(user.username, user.password) - .send({ - objects: [ - { - type: 'dashboard', - id: 'cts_dashboard', - }, - ], - includeReferences: false, - retries: { - [destination]: [ - { - type: 'dashboard', - id: 'cts_dashboard', - overwrite: false, - }, - ], - }, - }) - .expect(tests.withoutReferencesNotOverwriting.statusCode) - .then(tests.withoutReferencesNotOverwriting.response); - }); - - it(`should return ${tests.nonExistentSpace.statusCode} when resolving within a non-existent space`, async () => { - const destination = NON_EXISTENT_SPACE_ID; - - return supertestWithoutAuth - .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`) - .auth(user.username, user.password) - .send({ - objects: [ - { - type: 'dashboard', - id: 'cts_dashboard', - }, - ], - includeReferences: false, - retries: { - [destination]: [ - { - type: 'dashboard', - id: 'cts_dashboard', - overwrite: true, - }, - ], - }, - }) - .expect(tests.nonExistentSpace.statusCode) - .then(tests.nonExistentSpace.response); + describe('single-namespace types', () => { + beforeEach(() => esArchiver.load('saved_objects/spaces')); + afterEach(() => esArchiver.unload('saved_objects/spaces')); + + const dashboardObject = { type: 'dashboard', id: 'cts_dashboard' }; + const visualizationObject = { type: 'visualization', id: 'cts_vis_3' }; + + it(`should return ${tests.withReferencesNotOverwriting.statusCode} when not overwriting, with references`, async () => { + const destination = getDestinationSpace(spaceId); + + return supertestWithoutAuth + .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`) + .auth(user.username, user.password) + .send({ + objects: [dashboardObject], + includeReferences: true, + retries: { [destination]: [{ ...visualizationObject, overwrite: false }] }, + }) + .expect(tests.withReferencesNotOverwriting.statusCode) + .then(tests.withReferencesNotOverwriting.response); + }); + + it(`should return ${tests.withReferencesOverwriting.statusCode} when overwriting, with references`, async () => { + const destination = getDestinationSpace(spaceId); + + return supertestWithoutAuth + .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`) + .auth(user.username, user.password) + .send({ + objects: [dashboardObject], + includeReferences: true, + retries: { [destination]: [{ ...visualizationObject, overwrite: true }] }, + }) + .expect(tests.withReferencesOverwriting.statusCode) + .then(tests.withReferencesOverwriting.response); + }); + + it(`should return ${tests.withoutReferencesOverwriting.statusCode} when overwriting, without references`, async () => { + const destination = getDestinationSpace(spaceId); + + return supertestWithoutAuth + .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`) + .auth(user.username, user.password) + .send({ + objects: [dashboardObject], + includeReferences: false, + retries: { [destination]: [{ ...dashboardObject, overwrite: true }] }, + }) + .expect(tests.withoutReferencesOverwriting.statusCode) + .then(tests.withoutReferencesOverwriting.response); + }); + + it(`should return ${tests.withoutReferencesNotOverwriting.statusCode} when not overwriting, without references`, async () => { + const destination = getDestinationSpace(spaceId); + + return supertestWithoutAuth + .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`) + .auth(user.username, user.password) + .send({ + objects: [dashboardObject], + includeReferences: false, + retries: { [destination]: [{ ...dashboardObject, overwrite: false }] }, + }) + .expect(tests.withoutReferencesNotOverwriting.statusCode) + .then(tests.withoutReferencesNotOverwriting.response); + }); + + it(`should return ${tests.nonExistentSpace.statusCode} when resolving within a non-existent space`, async () => { + const destination = NON_EXISTENT_SPACE_ID; + + return supertestWithoutAuth + .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`) + .auth(user.username, user.password) + .send({ + objects: [dashboardObject], + includeReferences: false, + retries: { [destination]: [{ ...dashboardObject, overwrite: true }] }, + }) + .expect(tests.nonExistentSpace.statusCode) + .then(tests.nonExistentSpace.response); + }); }); }); }; diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space.ts index 08450f48567c8..535c78fb8ea4c 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space.ts @@ -55,325 +55,140 @@ export default function copyToSpaceSpacesAndSecuritySuite({ getService }: TestIn dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, }, }, - ].forEach((scenario) => { - copyToSpaceTest(`user with no access from the ${scenario.spaceId} space`, { - spaceId: scenario.spaceId, - user: scenario.users.noAccess, + ].forEach(({ spaceId, ...scenario }) => { + const definitionNoAccess = (user: { username: string; password: string }) => ({ + spaceId, + user, tests: { - noConflictsWithoutReferences: { - statusCode: 404, - response: expectNotFoundResponse, - }, - noConflictsWithReferences: { - statusCode: 404, - response: expectNotFoundResponse, - }, - withConflictsOverwriting: { - statusCode: 404, - response: expectNotFoundResponse, - }, - withConflictsWithoutOverwriting: { - statusCode: 404, - response: expectNotFoundResponse, - }, + noConflictsWithoutReferences: { statusCode: 404, response: expectNotFoundResponse }, + noConflictsWithReferences: { statusCode: 404, response: expectNotFoundResponse }, + withConflictsOverwriting: { statusCode: 404, response: expectNotFoundResponse }, + withConflictsWithoutOverwriting: { statusCode: 404, response: expectNotFoundResponse }, multipleSpaces: { statusCode: 404, withConflictsResponse: expectNotFoundResponse, noConflictsResponse: expectNotFoundResponse, }, - nonExistentSpace: { - statusCode: 404, - response: expectNotFoundResponse, - }, + nonExistentSpace: { statusCode: 404, response: expectNotFoundResponse }, }, }); - - copyToSpaceTest(`superuser from the ${scenario.spaceId} space`, { - spaceId: scenario.spaceId, - user: scenario.users.superuser, - tests: { - noConflictsWithoutReferences: { - statusCode: 200, - response: expectNoConflictsWithoutReferencesResult, - }, - noConflictsWithReferences: { - statusCode: 200, - response: expectNoConflictsWithReferencesResult, - }, - withConflictsOverwriting: { - statusCode: 200, - response: createExpectWithConflictsOverwritingResult(scenario.spaceId), - }, - withConflictsWithoutOverwriting: { - statusCode: 200, - response: createExpectWithConflictsWithoutOverwritingResult(scenario.spaceId), - }, - multipleSpaces: { - statusCode: 200, - withConflictsResponse: createExpectWithConflictsOverwritingResult(scenario.spaceId), - noConflictsResponse: expectNoConflictsWithReferencesResult, - }, - nonExistentSpace: { - statusCode: 200, - response: expectNoConflictsForNonExistentSpaceResult, - }, + // In *this* test suite, a user who is unauthorized to write (but authorized to read) in the destination space will get the same exact + // results as a user who is unauthorized to read in the destination space. However, that may not *always* be the case depending on the + // input that is submitted, due to the `validateReferences` check that can trigger a `bulkGet` for the destination space. See also the + // integration tests in `./resolve_copy_to_space_conflicts`, which behave differently. + const commonUnauthorizedTests = { + noConflictsWithoutReferences: { + statusCode: 200, + response: createExpectUnauthorizedAtSpaceWithoutReferencesResult( + spaceId, + 'without-conflicts' + ), }, - }); - - copyToSpaceTest(`rbac user with all globally from the ${scenario.spaceId} space`, { - spaceId: scenario.spaceId, - user: scenario.users.allGlobally, - tests: { - noConflictsWithoutReferences: { - statusCode: 200, - response: expectNoConflictsWithoutReferencesResult, - }, - noConflictsWithReferences: { - statusCode: 200, - response: expectNoConflictsWithReferencesResult, - }, - withConflictsOverwriting: { - statusCode: 200, - response: createExpectWithConflictsOverwritingResult(scenario.spaceId), - }, - withConflictsWithoutOverwriting: { - statusCode: 200, - response: createExpectWithConflictsWithoutOverwritingResult(scenario.spaceId), - }, - multipleSpaces: { - statusCode: 200, - withConflictsResponse: createExpectWithConflictsOverwritingResult(scenario.spaceId), - noConflictsResponse: expectNoConflictsWithReferencesResult, - }, - nonExistentSpace: { - statusCode: 200, - response: expectNoConflictsForNonExistentSpaceResult, - }, + noConflictsWithReferences: { + statusCode: 200, + response: createExpectUnauthorizedAtSpaceWithReferencesResult( + spaceId, + 'without-conflicts' + ), }, - }); - - copyToSpaceTest(`dual-privileges user from the ${scenario.spaceId} space`, { - spaceId: scenario.spaceId, - user: scenario.users.dualAll, - tests: { - noConflictsWithoutReferences: { - statusCode: 200, - response: expectNoConflictsWithoutReferencesResult, - }, - noConflictsWithReferences: { - statusCode: 200, - response: expectNoConflictsWithReferencesResult, - }, - withConflictsOverwriting: { - statusCode: 200, - response: createExpectWithConflictsOverwritingResult(scenario.spaceId), - }, - withConflictsWithoutOverwriting: { - statusCode: 200, - response: createExpectWithConflictsWithoutOverwritingResult(scenario.spaceId), - }, - multipleSpaces: { - statusCode: 200, - withConflictsResponse: createExpectWithConflictsOverwritingResult(scenario.spaceId), - noConflictsResponse: expectNoConflictsWithReferencesResult, - }, - nonExistentSpace: { - statusCode: 200, - response: expectNoConflictsForNonExistentSpaceResult, - }, + withConflictsOverwriting: { + statusCode: 200, + response: createExpectUnauthorizedAtSpaceWithReferencesResult(spaceId, 'with-conflicts'), }, - }); - - copyToSpaceTest(`legacy user from the ${scenario.spaceId} space`, { - spaceId: scenario.spaceId, - user: scenario.users.legacyAll, - tests: { - noConflictsWithoutReferences: { - statusCode: 404, - response: expectNotFoundResponse, - }, - noConflictsWithReferences: { - statusCode: 404, - response: expectNotFoundResponse, - }, - withConflictsOverwriting: { - statusCode: 404, - response: expectNotFoundResponse, - }, - withConflictsWithoutOverwriting: { - statusCode: 404, - response: expectNotFoundResponse, - }, - multipleSpaces: { - statusCode: 404, - withConflictsResponse: expectNotFoundResponse, - noConflictsResponse: expectNotFoundResponse, - }, - nonExistentSpace: { - statusCode: 404, - response: expectNotFoundResponse, - }, + withConflictsWithoutOverwriting: { + statusCode: 200, + response: createExpectUnauthorizedAtSpaceWithReferencesResult(spaceId, 'with-conflicts'), }, - }); - - copyToSpaceTest(`rbac user with read globally from the ${scenario.spaceId} space`, { - spaceId: scenario.spaceId, - user: scenario.users.readGlobally, - tests: { - noConflictsWithoutReferences: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithoutReferencesResult( - scenario.spaceId, - 'without-conflicts' - ), - }, - noConflictsWithReferences: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithReferencesResult( - scenario.spaceId, - 'without-conflicts' - ), - }, - withConflictsOverwriting: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithReferencesResult( - scenario.spaceId, - 'with-conflicts' - ), - }, - withConflictsWithoutOverwriting: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithReferencesResult( - scenario.spaceId, - 'with-conflicts' - ), - }, - multipleSpaces: { - statusCode: 200, - withConflictsResponse: createExpectUnauthorizedAtSpaceWithReferencesResult( - scenario.spaceId, - 'with-conflicts' - ), - noConflictsResponse: createExpectUnauthorizedAtSpaceWithReferencesResult( - scenario.spaceId, - 'without-conflicts' - ), - }, - nonExistentSpace: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithoutReferencesResult( - scenario.spaceId, - 'non-existent' - ), - }, + multipleSpaces: { + statusCode: 200, + withConflictsResponse: createExpectUnauthorizedAtSpaceWithReferencesResult( + spaceId, + 'with-conflicts' + ), + noConflictsResponse: createExpectUnauthorizedAtSpaceWithReferencesResult( + spaceId, + 'without-conflicts' + ), }, - }); - - copyToSpaceTest(`dual-privileges readonly user from the ${scenario.spaceId} space`, { - spaceId: scenario.spaceId, - user: scenario.users.dualRead, - tests: { - noConflictsWithoutReferences: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithoutReferencesResult( - scenario.spaceId, - 'without-conflicts' - ), - }, - noConflictsWithReferences: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithReferencesResult( - scenario.spaceId, - 'without-conflicts' - ), - }, - withConflictsOverwriting: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithReferencesResult( - scenario.spaceId, - 'with-conflicts' - ), - }, - withConflictsWithoutOverwriting: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithReferencesResult( - scenario.spaceId, - 'with-conflicts' - ), - }, - multipleSpaces: { - statusCode: 200, - withConflictsResponse: createExpectUnauthorizedAtSpaceWithReferencesResult( - scenario.spaceId, - 'with-conflicts' - ), - noConflictsResponse: createExpectUnauthorizedAtSpaceWithReferencesResult( - scenario.spaceId, - 'without-conflicts' - ), - }, - nonExistentSpace: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithoutReferencesResult( - scenario.spaceId, - 'non-existent' - ), - }, + nonExistentSpace: { + statusCode: 200, + response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(spaceId, 'non-existent'), }, + }; + const definitionUnauthorizedRead = (user: { username: string; password: string }) => ({ + spaceId, + user, + tests: commonUnauthorizedTests, }); - - copyToSpaceTest(`rbac user with all at space from the ${scenario.spaceId} space`, { - spaceId: scenario.spaceId, - user: scenario.users.allAtSpace, + const definitionUnauthorizedWrite = (user: { username: string; password: string }) => ({ + spaceId, + user, + tests: commonUnauthorizedTests, + }); + const definitionAuthorized = (user: { username: string; password: string }) => ({ + spaceId, + user, tests: { noConflictsWithoutReferences: { statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithoutReferencesResult( - scenario.spaceId, - 'without-conflicts' - ), + response: expectNoConflictsWithoutReferencesResult, }, noConflictsWithReferences: { statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithReferencesResult( - scenario.spaceId, - 'without-conflicts' - ), + response: expectNoConflictsWithReferencesResult(spaceId), }, withConflictsOverwriting: { statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithReferencesResult( - scenario.spaceId, - 'with-conflicts' - ), + response: createExpectWithConflictsOverwritingResult(spaceId), }, withConflictsWithoutOverwriting: { statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithReferencesResult( - scenario.spaceId, - 'with-conflicts' - ), + response: createExpectWithConflictsWithoutOverwritingResult(spaceId), }, multipleSpaces: { statusCode: 200, - withConflictsResponse: createExpectUnauthorizedAtSpaceWithReferencesResult( - scenario.spaceId, - 'with-conflicts' - ), - noConflictsResponse: createExpectUnauthorizedAtSpaceWithReferencesResult( - scenario.spaceId, - 'without-conflicts' - ), + withConflictsResponse: createExpectWithConflictsOverwritingResult(spaceId), + noConflictsResponse: expectNoConflictsWithReferencesResult(spaceId), }, nonExistentSpace: { statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithoutReferencesResult( - scenario.spaceId, - 'non-existent' - ), + response: expectNoConflictsForNonExistentSpaceResult, }, }, }); + + copyToSpaceTest( + `user with no access from the ${spaceId} space`, + definitionNoAccess(scenario.users.noAccess) + ); + copyToSpaceTest( + `superuser from the ${spaceId} space`, + definitionAuthorized(scenario.users.superuser) + ); + copyToSpaceTest( + `rbac user with all globally from the ${spaceId} space`, + definitionAuthorized(scenario.users.allGlobally) + ); + copyToSpaceTest( + `dual-privileges user from the ${spaceId} space`, + definitionAuthorized(scenario.users.dualAll) + ); + copyToSpaceTest( + `legacy user from the ${spaceId} space`, + definitionNoAccess(scenario.users.legacyAll) + ); + copyToSpaceTest( + `rbac user with read globally from the ${spaceId} space`, + definitionUnauthorizedWrite(scenario.users.readGlobally) + ); + copyToSpaceTest( + `dual-privileges readonly user from the ${spaceId} space`, + definitionUnauthorizedWrite(scenario.users.dualRead) + ); + copyToSpaceTest( + `rbac user with all at space from the ${spaceId} space`, + definitionUnauthorizedRead(scenario.users.allAtSpace) + ); }); }); } diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/resolve_copy_to_space_conflicts.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/resolve_copy_to_space_conflicts.ts index 472ec1a927126..1f05b9439e3a2 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/resolve_copy_to_space_conflicts.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/resolve_copy_to_space_conflicts.ts @@ -56,10 +56,10 @@ export default function resolveCopyToSpaceConflictsTestSuite({ getService }: Tes dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, }, }, - ].forEach((scenario) => { - resolveCopyToSpaceConflictsTest(`user with no access from the ${scenario.spaceId} space`, { - spaceId: scenario.spaceId, - user: scenario.users.noAccess, + ].forEach(({ spaceId, ...scenario }) => { + const definitionNoAccess = (user: { username: string; password: string }) => ({ + spaceId, + user, tests: { withReferencesNotOverwriting: { statusCode: 404, @@ -83,224 +83,125 @@ export default function resolveCopyToSpaceConflictsTestSuite({ getService }: Tes }, }, }); - - resolveCopyToSpaceConflictsTest(`superuser from the ${scenario.spaceId} space`, { - spaceId: scenario.spaceId, - user: scenario.users.superuser, + const definitionUnauthorizedRead = (user: { username: string; password: string }) => ({ + spaceId, + user, tests: { withReferencesNotOverwriting: { statusCode: 200, - response: createExpectNonOverriddenResponseWithReferences(scenario.spaceId), + response: createExpectUnauthorizedAtSpaceWithReferencesResult(spaceId), }, withReferencesOverwriting: { statusCode: 200, - response: createExpectOverriddenResponseWithReferences(scenario.spaceId), + response: createExpectUnauthorizedAtSpaceWithReferencesResult(spaceId), }, withoutReferencesOverwriting: { statusCode: 200, - response: createExpectOverriddenResponseWithoutReferences(scenario.spaceId), + response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(spaceId), }, withoutReferencesNotOverwriting: { statusCode: 200, - response: createExpectNonOverriddenResponseWithoutReferences(scenario.spaceId), + response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(spaceId), }, nonExistentSpace: { statusCode: 200, - response: createExpectOverriddenResponseWithoutReferences( - scenario.spaceId, + response: createExpectUnauthorizedAtSpaceWithoutReferencesResult( + spaceId, NON_EXISTENT_SPACE_ID ), }, }, }); - - resolveCopyToSpaceConflictsTest( - `rbac user with all globally from the ${scenario.spaceId} space`, - { - spaceId: scenario.spaceId, - user: scenario.users.allGlobally, - tests: { - withReferencesNotOverwriting: { - statusCode: 200, - response: createExpectNonOverriddenResponseWithReferences(scenario.spaceId), - }, - withReferencesOverwriting: { - statusCode: 200, - response: createExpectOverriddenResponseWithReferences(scenario.spaceId), - }, - withoutReferencesOverwriting: { - statusCode: 200, - response: createExpectOverriddenResponseWithoutReferences(scenario.spaceId), - }, - withoutReferencesNotOverwriting: { - statusCode: 200, - response: createExpectNonOverriddenResponseWithoutReferences(scenario.spaceId), - }, - nonExistentSpace: { - statusCode: 200, - response: createExpectOverriddenResponseWithoutReferences( - scenario.spaceId, - NON_EXISTENT_SPACE_ID - ), - }, - }, - } - ); - - resolveCopyToSpaceConflictsTest(`dual-privileges user from the ${scenario.spaceId} space`, { - spaceId: scenario.spaceId, - user: scenario.users.dualAll, + const definitionUnauthorizedWrite = (user: { username: string; password: string }) => ({ + spaceId, + user, tests: { withReferencesNotOverwriting: { statusCode: 200, - response: createExpectNonOverriddenResponseWithReferences(scenario.spaceId), + response: createExpectReadonlyAtSpaceWithReferencesResult(spaceId), }, withReferencesOverwriting: { statusCode: 200, - response: createExpectOverriddenResponseWithReferences(scenario.spaceId), + response: createExpectReadonlyAtSpaceWithReferencesResult(spaceId), }, withoutReferencesOverwriting: { statusCode: 200, - response: createExpectOverriddenResponseWithoutReferences(scenario.spaceId), + response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(spaceId), }, withoutReferencesNotOverwriting: { statusCode: 200, - response: createExpectNonOverriddenResponseWithoutReferences(scenario.spaceId), + response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(spaceId), }, nonExistentSpace: { statusCode: 200, - response: createExpectOverriddenResponseWithoutReferences( - scenario.spaceId, + response: createExpectUnauthorizedAtSpaceWithoutReferencesResult( + spaceId, NON_EXISTENT_SPACE_ID ), }, }, }); - - resolveCopyToSpaceConflictsTest(`legacy user from the ${scenario.spaceId} space`, { - spaceId: scenario.spaceId, - user: scenario.users.legacyAll, + const definitionAuthorized = (user: { username: string; password: string }) => ({ + spaceId, + user, tests: { withReferencesNotOverwriting: { - statusCode: 404, - response: expectNotFoundResponse, + statusCode: 200, + response: createExpectNonOverriddenResponseWithReferences(spaceId), }, withReferencesOverwriting: { - statusCode: 404, - response: expectNotFoundResponse, + statusCode: 200, + response: createExpectOverriddenResponseWithReferences(spaceId), }, withoutReferencesOverwriting: { - statusCode: 404, - response: expectNotFoundResponse, + statusCode: 200, + response: createExpectOverriddenResponseWithoutReferences(spaceId), }, withoutReferencesNotOverwriting: { - statusCode: 404, - response: expectNotFoundResponse, + statusCode: 200, + response: createExpectNonOverriddenResponseWithoutReferences(spaceId), }, nonExistentSpace: { - statusCode: 404, - response: expectNotFoundResponse, + statusCode: 200, + response: createExpectOverriddenResponseWithoutReferences( + spaceId, + NON_EXISTENT_SPACE_ID + ), }, }, }); resolveCopyToSpaceConflictsTest( - `rbac user with read globally from the ${scenario.spaceId} space`, - { - spaceId: scenario.spaceId, - user: scenario.users.readGlobally, - tests: { - withReferencesNotOverwriting: { - statusCode: 200, - response: createExpectReadonlyAtSpaceWithReferencesResult(scenario.spaceId), - }, - withReferencesOverwriting: { - statusCode: 200, - response: createExpectReadonlyAtSpaceWithReferencesResult(scenario.spaceId), - }, - withoutReferencesOverwriting: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(scenario.spaceId), - }, - withoutReferencesNotOverwriting: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(scenario.spaceId), - }, - nonExistentSpace: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithoutReferencesResult( - scenario.spaceId, - NON_EXISTENT_SPACE_ID - ), - }, - }, - } + `user with no access from the ${spaceId} space`, + definitionNoAccess(scenario.users.noAccess) ); - resolveCopyToSpaceConflictsTest( - `dual-privileges readonly user from the ${scenario.spaceId} space`, - { - spaceId: scenario.spaceId, - user: scenario.users.dualRead, - tests: { - withReferencesNotOverwriting: { - statusCode: 200, - response: createExpectReadonlyAtSpaceWithReferencesResult(scenario.spaceId), - }, - withReferencesOverwriting: { - statusCode: 200, - response: createExpectReadonlyAtSpaceWithReferencesResult(scenario.spaceId), - }, - withoutReferencesOverwriting: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(scenario.spaceId), - }, - withoutReferencesNotOverwriting: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(scenario.spaceId), - }, - nonExistentSpace: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithoutReferencesResult( - scenario.spaceId, - NON_EXISTENT_SPACE_ID - ), - }, - }, - } + `superuser from the ${spaceId} space`, + definitionAuthorized(scenario.users.superuser) ); - resolveCopyToSpaceConflictsTest( - `rbac user with all at space from the ${scenario.spaceId} space`, - { - spaceId: scenario.spaceId, - user: scenario.users.allAtSpace, - tests: { - withReferencesNotOverwriting: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithReferencesResult(scenario.spaceId), - }, - withReferencesOverwriting: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithReferencesResult(scenario.spaceId), - }, - withoutReferencesOverwriting: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(scenario.spaceId), - }, - withoutReferencesNotOverwriting: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(scenario.spaceId), - }, - nonExistentSpace: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithoutReferencesResult( - scenario.spaceId, - NON_EXISTENT_SPACE_ID - ), - }, - }, - } + `rbac user with all globally from the ${spaceId} space`, + definitionAuthorized(scenario.users.allGlobally) + ); + resolveCopyToSpaceConflictsTest( + `dual-privileges user from the ${spaceId} space`, + definitionAuthorized(scenario.users.dualAll) + ); + resolveCopyToSpaceConflictsTest( + `legacy user from the ${spaceId} space`, + definitionNoAccess(scenario.users.legacyAll) + ); + resolveCopyToSpaceConflictsTest( + `rbac user with read globally from the ${spaceId} space`, + definitionUnauthorizedWrite(scenario.users.readGlobally) + ); + resolveCopyToSpaceConflictsTest( + `dual-privileges readonly user from the ${spaceId} space`, + definitionUnauthorizedWrite(scenario.users.dualRead) + ); + resolveCopyToSpaceConflictsTest( + `rbac user with all at space from the ${spaceId} space`, + definitionUnauthorizedRead(scenario.users.allAtSpace) ); }); }); diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/copy_to_space.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/copy_to_space.ts index 75b35fecd5d83..1de69d8b73aa8 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/copy_to_space.ts @@ -34,7 +34,7 @@ export default function copyToSpacesOnlySuite({ getService }: FtrProviderContext }, noConflictsWithReferences: { statusCode: 200, - response: expectNoConflictsWithReferencesResult, + response: expectNoConflictsWithReferencesResult(spaceId), }, withConflictsOverwriting: { statusCode: 200, @@ -47,7 +47,7 @@ export default function copyToSpacesOnlySuite({ getService }: FtrProviderContext multipleSpaces: { statusCode: 200, withConflictsResponse: createExpectWithConflictsOverwritingResult(spaceId), - noConflictsResponse: expectNoConflictsWithReferencesResult, + noConflictsResponse: expectNoConflictsWithReferencesResult(spaceId), }, nonExistentSpace: { statusCode: 200, From a49f114d535e480ca1b7dac3b6cf5c74f845f1be Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 11 May 2020 13:48:51 -0400 Subject: [PATCH 11/55] Add and update API integration tests for Saved Objects and Spaces Also fixed existing Jest integration tests, but did not add new test cases for those since they are currently disabled in CI. --- .../routes/integration_tests/import.test.ts | 6 + .../resolve_import_errors.test.ts | 26 ++- .../apis/saved_objects/import.js | 10 + .../apis/saved_objects/migrations.js | 11 +- .../saved_objects/resolve_import_errors.js | 24 ++- .../saved_objects/spaces/data.json | 88 ++++++++ .../saved_objects/spaces/mappings.json | 3 + .../saved_object_test_plugin/server/plugin.ts | 1 + .../common/lib/saved_object_test_utils.ts | 4 +- .../common/suites/bulk_create.ts | 11 + .../common/suites/export.ts | 64 +++--- .../common/suites/find.ts | 22 +- .../common/suites/import.ts | 135 ++++++++++--- .../common/suites/resolve_import_errors.ts | 128 +++++++++--- .../security_and_spaces/apis/bulk_create.ts | 15 +- .../security_and_spaces/apis/export.ts | 9 +- .../security_and_spaces/apis/import.ts | 162 +++++++++++---- .../apis/resolve_import_errors.ts | 135 ++++++++----- .../security_only/apis/bulk_create.ts | 5 +- .../security_only/apis/export.ts | 9 +- .../security_only/apis/import.ts | 105 +++++++--- .../apis/resolve_import_errors.ts | 81 +++++--- .../spaces_only/apis/bulk_create.ts | 15 +- .../spaces_only/apis/import.ts | 77 +++++-- .../spaces_only/apis/resolve_import_errors.ts | 80 ++++++-- .../saved_objects/spaces/data.json | 133 +++++++++++- .../saved_objects/spaces/mappings.json | 3 + .../spaces_test_plugin/server/plugin.ts | 6 + .../common/lib/saved_object_test_cases.ts | 4 +- .../common/suites/copy_to_space.ts | 190 ++++++++++++++++++ .../common/suites/delete.ts | 17 +- .../suites/resolve_copy_to_space_conflicts.ts | 139 +++++++++++++ .../security_and_spaces/apis/copy_to_space.ts | 13 +- .../apis/resolve_copy_to_space_conflicts.ts | 5 + .../security_and_spaces/apis/share_add.ts | 4 +- .../security_and_spaces/apis/share_remove.ts | 2 +- .../spaces_only/apis/copy_to_space.ts | 2 + .../apis/resolve_copy_to_space_conflicts.ts | 2 + .../spaces_only/apis/share_add.ts | 4 +- .../spaces_only/apis/share_remove.ts | 4 +- 40 files changed, 1436 insertions(+), 318 deletions(-) diff --git a/src/core/server/saved_objects/routes/integration_tests/import.test.ts b/src/core/server/saved_objects/routes/integration_tests/import.test.ts index 5ab7baba90586..84add9fdbf4ff 100644 --- a/src/core/server/saved_objects/routes/integration_tests/import.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/import.test.ts @@ -120,6 +120,7 @@ describe('POST /internal/saved_objects/_import', () => { expect(result.body).toEqual({ success: true, successCount: 1, + successResults: [{ type: 'index-pattern', id: 'my-pattern' }], }); expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); const firstBulkCreateCallArray = savedObjectsClient.bulkCreate.mock.calls[0][0]; @@ -173,6 +174,10 @@ describe('POST /internal/saved_objects/_import', () => { expect(result.body).toEqual({ success: true, successCount: 2, + successResults: [ + { type: 'index-pattern', id: 'my-pattern' }, + { type: 'dashboard', id: 'my-dashboard' }, + ], }); }); @@ -219,6 +224,7 @@ describe('POST /internal/saved_objects/_import', () => { expect(result.body).toEqual({ success: false, successCount: 1, + successResults: [{ type: 'dashboard', id: 'my-dashboard' }], errors: [ { id: 'my-pattern', diff --git a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts index a36f246f9dbc5..ec0b2ff75aa39 100644 --- a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts @@ -113,7 +113,11 @@ describe('POST /api/saved_objects/_resolve_import_errors', () => { ) .expect(200); - expect(result.body).toEqual({ success: true, successCount: 1 }); + expect(result.body).toEqual({ + success: true, + successCount: 1, + successResults: [{ type: 'dashboard', id: 'my-dashboard' }], + }); expect(savedObjectsClient.bulkCreate.mock.calls).toHaveLength(1); const firstBulkCreateCallArray = savedObjectsClient.bulkCreate.mock.calls[0][0]; expect(firstBulkCreateCallArray).toHaveLength(1); @@ -154,7 +158,11 @@ describe('POST /api/saved_objects/_resolve_import_errors', () => { ) .expect(200); - expect(result.body).toEqual({ success: true, successCount: 1 }); + expect(result.body).toEqual({ + success: true, + successCount: 1, + successResults: [{ type: 'dashboard', id: 'my-dashboard' }], + }); expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ @@ -171,6 +179,7 @@ describe('POST /api/saved_objects/_resolve_import_errors', () => { ], Object { "namespace": undefined, + "overwrite": undefined, }, ], ], @@ -219,7 +228,11 @@ describe('POST /api/saved_objects/_resolve_import_errors', () => { ) .expect(200); - expect(result.body).toEqual({ success: true, successCount: 1 }); + expect(result.body).toEqual({ + success: true, + successCount: 1, + successResults: [{ type: 'dashboard', id: 'my-dashboard' }], + }); expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ @@ -300,7 +313,11 @@ describe('POST /api/saved_objects/_resolve_import_errors', () => { ) .expect(200); - expect(result.body).toEqual({ success: true, successCount: 1 }); + expect(result.body).toEqual({ + success: true, + successCount: 1, + successResults: [{ type: 'visualization', id: 'my-vis' }], + }); expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ @@ -324,6 +341,7 @@ describe('POST /api/saved_objects/_resolve_import_errors', () => { ], Object { "namespace": undefined, + "overwrite": undefined, }, ], ], diff --git a/test/api_integration/apis/saved_objects/import.js b/test/api_integration/apis/saved_objects/import.js index fbacfe458d976..fe0849eb7bcab 100644 --- a/test/api_integration/apis/saved_objects/import.js +++ b/test/api_integration/apis/saved_objects/import.js @@ -40,6 +40,11 @@ export default function ({ getService }) { expect(resp.body).to.eql({ success: true, successCount: 3, + successResults: [ + { type: 'index-pattern', id: '91200a00-9efd-11e7-acb3-3dab96693fab' }, + { type: 'visualization', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab' }, + { type: 'dashboard', id: 'be3733a0-9efe-11e7-acb3-3dab96693fab' }, + ], }); }); }); @@ -108,6 +113,11 @@ export default function ({ getService }) { expect(resp.body).to.eql({ success: true, successCount: 3, + successResults: [ + { type: 'index-pattern', id: '91200a00-9efd-11e7-acb3-3dab96693fab' }, + { type: 'visualization', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab' }, + { type: 'dashboard', id: 'be3733a0-9efe-11e7-acb3-3dab96693fab' }, + ], }); }); }); diff --git a/test/api_integration/apis/saved_objects/migrations.js b/test/api_integration/apis/saved_objects/migrations.js index d0ff4cc06c57e..4a18617cc2143 100644 --- a/test/api_integration/apis/saved_objects/migrations.js +++ b/test/api_integration/apis/saved_objects/migrations.js @@ -292,13 +292,10 @@ export default ({ getService }) => { ); // It only created the original and the dest - assert.deepEqual( - _.pluck( - await callCluster('cat.indices', { index: '.migration-c*', format: 'json' }), - 'index' - ).sort(), - ['.migration-c_1', '.migration-c_2'] - ); + const indices = (await callCluster('cat.indices', { index: '.migration-c*', format: 'json' })) + .map(({ index }) => index) + .sort(); + assert.deepEqual(indices, ['.migration-c_1', '.migration-c_2']); // The docs in the original index are unchanged assert.deepEqual(await fetchDocs({ callCluster, index: `${index}_1` }), [ diff --git a/test/api_integration/apis/saved_objects/resolve_import_errors.js b/test/api_integration/apis/saved_objects/resolve_import_errors.js index aacfcd4382fac..093bfc74c60d4 100644 --- a/test/api_integration/apis/saved_objects/resolve_import_errors.js +++ b/test/api_integration/apis/saved_objects/resolve_import_errors.js @@ -72,6 +72,11 @@ export default function ({ getService }) { expect(resp.body).to.eql({ success: true, successCount: 3, + successResults: [ + { type: 'index-pattern', id: '91200a00-9efd-11e7-acb3-3dab96693fab' }, + { type: 'visualization', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab' }, + { type: 'dashboard', id: 'be3733a0-9efe-11e7-acb3-3dab96693fab' }, + ], }); }); }); @@ -234,7 +239,15 @@ export default function ({ getService }) { .attach('file', join(__dirname, '../../fixtures/import.ndjson')) .expect(200) .then((resp) => { - expect(resp.body).to.eql({ success: true, successCount: 3 }); + expect(resp.body).to.eql({ + success: true, + successCount: 3, + successResults: [ + { type: 'index-pattern', id: '91200a00-9efd-11e7-acb3-3dab96693fab' }, + { type: 'visualization', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab' }, + { type: 'dashboard', id: 'be3733a0-9efe-11e7-acb3-3dab96693fab' }, + ], + }); }); }); @@ -254,7 +267,13 @@ export default function ({ getService }) { .attach('file', join(__dirname, '../../fixtures/import.ndjson')) .expect(200) .then((resp) => { - expect(resp.body).to.eql({ success: true, successCount: 1 }); + expect(resp.body).to.eql({ + success: true, + successCount: 1, + successResults: [ + { type: 'visualization', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab' }, + ], + }); }); }); @@ -298,6 +317,7 @@ export default function ({ getService }) { expect(resp.body).to.eql({ success: true, successCount: 1, + successResults: [{ type: 'visualization', id: '1' }], }); }); await supertest diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json index d2c14189e2529..4c0447c29c8f9 100644 --- a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json +++ b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json @@ -397,3 +397,91 @@ "type": "doc" } } + +{ + "type": "doc", + "value": { + "id": "sharedtype:conflict_1", + "index": ".kibana", + "source": { + "sharedtype": { + "title": "A shared saved-object in all spaces" + }, + "type": "sharedtype", + "namespaces": ["default", "space_1", "space_2"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:conflict_2a", + "index": ".kibana", + "source": { + "originId": "conflict_2", + "sharedtype": { + "title": "A shared saved-object in all spaces" + }, + "type": "sharedtype", + "namespaces": ["default", "space_1", "space_2"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:conflict_2b", + "index": ".kibana", + "source": { + "originId": "conflict_2", + "sharedtype": { + "title": "A shared saved-object in all spaces" + }, + "type": "sharedtype", + "namespaces": ["default", "space_1", "space_2"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:conflict_3", + "index": ".kibana", + "source": { + "sharedtype": { + "title": "A shared saved-object in all spaces" + }, + "type": "sharedtype", + "namespaces": ["default", "space_1", "space_2"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:conflict_4a", + "index": ".kibana", + "source": { + "originId": "conflict_4", + "sharedtype": { + "title": "A shared saved-object in all spaces" + }, + "type": "sharedtype", + "namespaces": ["default", "space_1", "space_2"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json index 7b5b1d86f6bcc..73f0e536b9295 100644 --- a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json +++ b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json @@ -182,6 +182,9 @@ "namespaces": { "type": "keyword" }, + "originId": { + "type": "keyword" + }, "search": { "properties": { "columns": { diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/saved_object_test_plugin/server/plugin.ts b/x-pack/test/saved_object_api_integration/common/fixtures/saved_object_test_plugin/server/plugin.ts index 0c15ab4bd2f80..45880635586a7 100644 --- a/x-pack/test/saved_object_api_integration/common/fixtures/saved_object_test_plugin/server/plugin.ts +++ b/x-pack/test/saved_object_api_integration/common/fixtures/saved_object_test_plugin/server/plugin.ts @@ -48,6 +48,7 @@ export class Plugin { name: 'sharedtype', hidden: false, namespaceType: 'multiple', + management, mappings, }); core.savedObjects.registerType({ diff --git a/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts b/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts index de036494caa83..115f4dca8b4bc 100644 --- a/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts +++ b/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts @@ -161,7 +161,9 @@ export const expectResponses = { expect(actualNamespace).to.eql(spaceId); } if (isMultiNamespace(type)) { - if (id === CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1.id) { + if (['conflict_1', 'conflict_2a', 'conflict_2b', 'conflict_3', 'conflict_4a'].includes(id)) { + expect(actualNamespaces).to.eql([DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID]); + } else if (id === CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1.id) { expect(actualNamespaces).to.eql([DEFAULT_SPACE_ID, SPACE_1_ID]); } else if (id === CASES.MULTI_NAMESPACE_ONLY_SPACE_1.id) { expect(actualNamespaces).to.eql([SPACE_1_ID]); diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts index dd32c42597c32..707060cedfe66 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts @@ -6,6 +6,7 @@ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; +import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; import { SPACES } from '../lib/spaces'; import { @@ -23,6 +24,7 @@ export interface BulkCreateTestDefinition extends TestDefinition { export type BulkCreateTestSuite = TestSuite; export interface BulkCreateTestCase extends TestCase { failure?: 400 | 409; // only used for permitted response case + fail409Param?: string; } const NEW_ATTRIBUTE_KEY = 'title'; // all type mappings include this attribute, for simplicity's sake @@ -56,6 +58,15 @@ export function bulkCreateTestSuiteFactory(es: any, esArchiver: any, supertest: for (let i = 0; i < savedObjects.length; i++) { const object = savedObjects[i]; const testCase = testCaseArray[i]; + if (testCase.failure === 409 && testCase.fail409Param === 'unresolvableConflict') { + const { type, id } = testCase; + const error = SavedObjectsErrorHelpers.createConflictError(type, id); + const payload = { ...error.output.payload, metadata: { isNotOverwritable: true } }; + expect(object.type).to.eql(type); + expect(object.id).to.eql(id); + expect(object.error).to.eql(payload); + continue; + } await expectResponses.permitted(object, testCase); if (!testCase.failure) { expect(object.attributes[NEW_ATTRIBUTE_KEY]).to.eql(NEW_ATTRIBUTE_VAL); diff --git a/x-pack/test/saved_object_api_integration/common/suites/export.ts b/x-pack/test/saved_object_api_integration/common/suites/export.ts index 394693677699f..ce5a3538d2060 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/export.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/export.ts @@ -8,7 +8,7 @@ import { SuperTest } from 'supertest'; import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; import { SPACES } from '../lib/spaces'; import { expectResponses, getUrlPrefix } from '../lib/saved_object_test_utils'; -import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; +import { ExpectResponseBody, TestDefinition, TestSuite } from '../lib/types'; const { DEFAULT: { spaceId: DEFAULT_SPACE_ID }, @@ -20,15 +20,28 @@ export interface ExportTestDefinition extends TestDefinition { request: ReturnType; } export type ExportTestSuite = TestSuite; +interface SuccessResult { + type: string; + id: string; + originId?: string; +} export interface ExportTestCase { title: string; type: string; id?: string; - successResult?: TestCase | TestCase[]; + successResult?: SuccessResult | SuccessResult[]; failure?: 400 | 403; } -export const getTestCases = (spaceId?: string) => ({ +// additional sharedtype objects that exist but do not have common test cases defined +const CID = 'conflict_'; +const CONFLICT_1_OBJ = Object.freeze({ type: 'sharedtype', id: `${CID}1` }); +const CONFLICT_2A_OBJ = Object.freeze({ type: 'sharedtype', id: `${CID}2a`, originId: `${CID}2` }); +const CONFLICT_2B_OBJ = Object.freeze({ type: 'sharedtype', id: `${CID}2b`, originId: `${CID}2` }); +const CONFLICT_3_OBJ = Object.freeze({ type: 'sharedtype', id: `${CID}3` }); +const CONFLICT_4A_OBJ = Object.freeze({ type: 'sharedtype', id: `${CID}4a`, originId: `${CID}4` }); + +export const getTestCases = (spaceId?: string): { [key: string]: ExportTestCase } => ({ singleNamespaceObject: { title: 'single-namespace object', ...(spaceId === SPACE_1_ID @@ -36,7 +49,7 @@ export const getTestCases = (spaceId?: string) => ({ : spaceId === SPACE_2_ID ? CASES.SINGLE_NAMESPACE_SPACE_2 : CASES.SINGLE_NAMESPACE_DEFAULT_SPACE), - } as ExportTestCase, + }, singleNamespaceType: { // this test explicitly ensures that single-namespace objects from other spaces are not returned title: 'single-namespace type', @@ -47,7 +60,7 @@ export const getTestCases = (spaceId?: string) => ({ : spaceId === SPACE_2_ID ? CASES.SINGLE_NAMESPACE_SPACE_2 : CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, - } as ExportTestCase, + }, multiNamespaceObject: { title: 'multi-namespace object', ...(spaceId === SPACE_1_ID @@ -55,30 +68,30 @@ export const getTestCases = (spaceId?: string) => ({ : spaceId === SPACE_2_ID ? CASES.MULTI_NAMESPACE_ONLY_SPACE_2 : CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1), - failure: 400, // multi-namespace types cannot be exported yet - } as ExportTestCase, + }, multiNamespaceType: { title: 'multi-namespace type', type: 'sharedtype', - // successResult: - // spaceId === SPACE_1_ID - // ? [CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, CASES.MULTI_NAMESPACE_ONLY_SPACE_1] - // : spaceId === SPACE_2_ID - // ? CASES.MULTI_NAMESPACE_ONLY_SPACE_2 - // : CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, - failure: 400, // multi-namespace types cannot be exported yet - } as ExportTestCase, + successResult: (spaceId === SPACE_1_ID + ? [CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, CASES.MULTI_NAMESPACE_ONLY_SPACE_1] + : spaceId === SPACE_2_ID + ? [CASES.MULTI_NAMESPACE_ONLY_SPACE_2] + : [CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1] + ) + .concat([CONFLICT_1_OBJ, CONFLICT_2A_OBJ, CONFLICT_2B_OBJ, CONFLICT_3_OBJ, CONFLICT_4A_OBJ]) + .flat(), + }, namespaceAgnosticObject: { title: 'namespace-agnostic object', ...CASES.NAMESPACE_AGNOSTIC, - } as ExportTestCase, + }, namespaceAgnosticType: { title: 'namespace-agnostic type', type: 'globaltype', successResult: CASES.NAMESPACE_AGNOSTIC, - } as ExportTestCase, - hiddenObject: { title: 'hidden object', ...CASES.HIDDEN, failure: 400 } as ExportTestCase, - hiddenType: { title: 'hidden type', type: 'hiddentype', failure: 400 } as ExportTestCase, + }, + hiddenObject: { title: 'hidden object', ...CASES.HIDDEN, failure: 400 }, + hiddenType: { title: 'hidden type', type: 'hiddentype', failure: 400 }, }); export const createRequest = ({ type, id }: ExportTestCase) => id ? { objects: [{ type, id }] } : { type }; @@ -98,7 +111,7 @@ export function exportTestSuiteFactory(esArchiver: any, supertest: SuperTest async ( response: Record ) => { - const { type, id, successResult = { type, id }, failure } = testCase; + const { type, id, successResult = { type, id } as SuccessResult, failure } = testCase; if (failure === 403) { // In export only, the API uses "bulk_get" or "find" depending on the parameters it receives. // The best that could be done here is to have an if statement to ensure at least one of the @@ -125,11 +138,14 @@ export function exportTestSuiteFactory(esArchiver: any, supertest: SuperTest x.id === object.id)!; + expect(expected).not.to.be(undefined); + expect(object.type).to.eql(expected.type); + if (object.originId) { + expect(object.originId).to.eql(expected.originId); + } expect(object.updated_at).to.match(/^[\d-]{10}T[\d:\.]{12}Z$/); // don't test attributes, version, or references } diff --git a/x-pack/test/saved_object_api_integration/common/suites/find.ts b/x-pack/test/saved_object_api_integration/common/suites/find.ts index 13f411fc14fc8..8647f7d90f3b5 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/find.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/find.ts @@ -34,6 +34,14 @@ export interface FindTestCase { failure?: 400 | 403; } +// additional sharedtype objects that exist but do not have common test cases defined +const CID = 'conflict_'; +const CONFLICT_1_OBJ = Object.freeze({ type: 'sharedtype', id: `${CID}1` }); +const CONFLICT_2A_OBJ = Object.freeze({ type: 'sharedtype', id: `${CID}2a`, originId: `${CID}2` }); +const CONFLICT_2B_OBJ = Object.freeze({ type: 'sharedtype', id: `${CID}2b`, originId: `${CID}2` }); +const CONFLICT_3_OBJ = Object.freeze({ type: 'sharedtype', id: `${CID}3` }); +const CONFLICT_4A_OBJ = Object.freeze({ type: 'sharedtype', id: `${CID}4a`, originId: `${CID}4` }); + export const getTestCases = (spaceId?: string) => ({ singleNamespaceType: { title: 'find single-namespace type', @@ -51,12 +59,14 @@ export const getTestCases = (spaceId?: string) => ({ title: 'find multi-namespace type', query: 'type=sharedtype&fields=title', successResult: { - savedObjects: - spaceId === SPACE_1_ID - ? [CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, CASES.MULTI_NAMESPACE_ONLY_SPACE_1] - : spaceId === SPACE_2_ID - ? CASES.MULTI_NAMESPACE_ONLY_SPACE_2 - : CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + savedObjects: (spaceId === SPACE_1_ID + ? [CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, CASES.MULTI_NAMESPACE_ONLY_SPACE_1] + : spaceId === SPACE_2_ID + ? [CASES.MULTI_NAMESPACE_ONLY_SPACE_2] + : [CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1] + ) + .concat([CONFLICT_1_OBJ, CONFLICT_2A_OBJ, CONFLICT_2B_OBJ, CONFLICT_3_OBJ, CONFLICT_4A_OBJ]) + .flat(), }, } as FindTestCase, namespaceAgnosticType: { diff --git a/x-pack/test/saved_object_api_integration/common/suites/import.ts b/x-pack/test/saved_object_api_integration/common/suites/import.ts index a5d2ca238d34e..f12671b551bc2 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/import.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/import.ts @@ -8,49 +8,88 @@ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; import { SPACES } from '../lib/spaces'; -import { - createRequest, - expectResponses, - getUrlPrefix, - getTestTitle, -} from '../lib/saved_object_test_utils'; +import { expectResponses, getUrlPrefix, getTestTitle } from '../lib/saved_object_test_utils'; import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; export interface ImportTestDefinition extends TestDefinition { - request: Array<{ type: string; id: string }>; + request: Array<{ type: string; id: string; originId?: string }>; + overwrite: boolean; } export type ImportTestSuite = TestSuite; export interface ImportTestCase extends TestCase { + originId?: string; + expectedNewId?: string; + successParam?: string; failure?: 400 | 409; // only used for permitted response case + fail403Param?: string; + fail409Param?: string; } const NEW_ATTRIBUTE_KEY = 'title'; // all type mappings include this attribute, for simplicity's sake const NEW_ATTRIBUTE_VAL = `New attribute value ${Date.now()}`; -const NEW_SINGLE_NAMESPACE_OBJ = Object.freeze({ type: 'dashboard', id: 'new-dashboard-id' }); -const NEW_MULTI_NAMESPACE_OBJ = Object.freeze({ type: 'sharedtype', id: 'new-sharedtype-id' }); -const NEW_NAMESPACE_AGNOSTIC_OBJ = Object.freeze({ type: 'globaltype', id: 'new-globaltype-id' }); +// these five saved objects already exist in the sample data: +// * id: conflict_1 +// * id: conflict_2a, originId: conflict_2 +// * id: conflict_2b, originId: conflict_2 +// * id: conflict_3 +// * id: conflict_4a, originId: conflict_4 +// using the seven conflict test case objects below, we can exercise various permutations of exact/inexact/ambiguous conflict scenarios +const CID = 'conflict_'; export const TEST_CASES = Object.freeze({ ...CASES, - NEW_SINGLE_NAMESPACE_OBJ, - NEW_MULTI_NAMESPACE_OBJ, - NEW_NAMESPACE_AGNOSTIC_OBJ, + CONFLICT_1_OBJ: Object.freeze({ type: 'sharedtype', id: `${CID}1` }), + CONFLICT_1A_OBJ: Object.freeze({ type: 'sharedtype', id: `${CID}1a`, originId: `${CID}1` }), + CONFLICT_1B_OBJ: Object.freeze({ type: 'sharedtype', id: `${CID}1b`, originId: `${CID}1` }), + CONFLICT_2C_OBJ: Object.freeze({ type: 'sharedtype', id: `${CID}2c`, originId: `${CID}2` }), + CONFLICT_2D_OBJ: Object.freeze({ type: 'sharedtype', id: `${CID}2d`, originId: `${CID}2` }), + CONFLICT_3A_OBJ: Object.freeze({ + type: 'sharedtype', + id: `${CID}3a`, + originId: `${CID}3`, + expectedNewId: `${CID}3`, + }), + CONFLICT_4_OBJ: Object.freeze({ type: 'sharedtype', id: `${CID}4`, expectedNewId: `${CID}4a` }), + NEW_SINGLE_NAMESPACE_OBJ: Object.freeze({ type: 'dashboard', id: 'new-dashboard-id' }), + NEW_MULTI_NAMESPACE_OBJ: Object.freeze({ type: 'sharedtype', id: 'new-sharedtype-id' }), + NEW_NAMESPACE_AGNOSTIC_OBJ: Object.freeze({ type: 'globaltype', id: 'new-globaltype-id' }), +}); + +/** + * Test cases have additional properties that we don't want to send in HTTP Requests + */ +const createRequest = ({ type, id, originId }: ImportTestCase) => ({ + type, + id, + ...(originId && { originId }), +}); + +const getConflictSource = (id: string) => ({ + id, + title: NEW_ATTRIBUTE_VAL, + // our source objects being imported does not include the `updatedAt` field (though they could) +}); +const getConflictDest = (id: string) => ({ + id, + title: 'A shared saved-object in all spaces', + updatedAt: '2017-09-21T18:59:16.270Z', }); export function importTestSuiteFactory(es: any, esArchiver: any, supertest: SuperTest) { - const expectForbidden = expectResponses.forbidden('bulk_create'); + const expectForbidden = expectResponses.forbidden; const expectResponseBody = ( testCases: ImportTestCase | ImportTestCase[], statusCode: 200 | 403, + fail403Param?: string, spaceId = SPACES.DEFAULT.spaceId ): ExpectResponseBody => async (response: Record) => { const testCaseArray = Array.isArray(testCases) ? testCases : [testCases]; if (statusCode === 403) { const types = testCaseArray.map((x) => x.type); - await expectForbidden(types)(response); + await expectResponses.forbidden(fail403Param!)(types)(response); } else { // permitted - const { success, successCount, errors } = response.body; + const { success, successCount, successResults, errors } = response.body; const expectedSuccesses = testCaseArray.filter((x) => !x.failure); const expectedFailures = testCaseArray.filter((x) => x.failure); expect(success).to.eql(expectedFailures.length === 0); @@ -61,12 +100,31 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe expect(response.body).not.to.have.property('errors'); } for (let i = 0; i < expectedSuccesses.length; i++) { - const { type, id } = expectedSuccesses[i]; - const { _source } = await expectResponses.successCreated(es, spaceId, type, id); + const { type, id, successParam, expectedNewId } = expectedSuccesses[i]; + // we don't know the order of the returned successResults; search for each one + const object = (successResults as Array>).find( + (x) => x.type === type && x.id === id + ); + expect(object).not.to.be(undefined); + const newId = object!.newId as string; + if (successParam === 'newId') { + // Kibana created the object with a different ID than what was specified in the import + // This can happen due to an unresolvable conflict (so the new ID will be random), or due to an inexact match (so the new ID will + // be equal to the ID or originID of the existing object that it inexactly matched) + if (expectedNewId) { + expect(newId).to.be(expectedNewId); + } else { + // the new ID was randomly generated + expect(newId).to.match(/^[0-9a-f-]{36}$/); + } + } else { + expect(newId).to.be(undefined); + } + const { _source } = await expectResponses.successCreated(es, spaceId, type, newId ?? id); expect(_source[type][NEW_ATTRIBUTE_KEY]).to.eql(NEW_ATTRIBUTE_VAL); } for (let i = 0; i < expectedFailures.length; i++) { - const { type, id, failure } = expectedFailures[i]; + const { type, id, failure, fail409Param, expectedNewId } = expectedFailures[i]; // we don't know the order of the returned errors; search for each one const object = (errors as Array>).find( (x) => x.type === type && x.id === id @@ -76,7 +134,33 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe expect(object!.error).to.eql({ type: 'unsupported_type' }); } else { // 409 - expect(object!.error).to.eql({ type: 'conflict' }); + let error: Record = { + type: 'conflict', + ...(expectedNewId && { destinationId: expectedNewId }), + }; + if (fail409Param === 'ambiguous_conflict_1a1b') { + // "ambiguous source" conflict + error = { + type: 'ambiguous_conflict', + sources: [getConflictSource(`${CID}1a`), getConflictSource(`${CID}1b`)], + destinations: [getConflictDest(`${CID}1`)], + }; + } else if (fail409Param === 'ambiguous_conflict_2c') { + // "ambiguous destination" conflict + error = { + type: 'ambiguous_conflict', + sources: [getConflictSource(`${CID}2c`)], + destinations: [getConflictDest(`${CID}2a`), getConflictDest(`${CID}2b`)], + }; + } else if (fail409Param === 'ambiguous_conflict_2c2d') { + // "ambiguous source and destination" conflict + error = { + type: 'ambiguous_conflict', + sources: [getConflictSource(`${CID}2c`), getConflictSource(`${CID}2d`)], + destinations: [getConflictDest(`${CID}2a`), getConflictDest(`${CID}2b`)], + }; + } + expect(object!.error).to.eql(error); } } } @@ -84,10 +168,12 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe const createTestDefinitions = ( testCases: ImportTestCase | ImportTestCase[], forbidden: boolean, + overwrite: boolean, options?: { spaceId?: string; singleRequest?: boolean; responseBodyOverride?: ExpectResponseBody; + fail403Param?: string; } ): ImportTestDefinition[] => { const cases = Array.isArray(testCases) ? testCases : [testCases]; @@ -101,7 +187,8 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe responseStatusCode, responseBody: options?.responseBodyOverride || - expectResponseBody(x, responseStatusCode, options?.spaceId), + expectResponseBody(x, responseStatusCode, options?.fail403Param, options?.spaceId), + overwrite, })); } // batch into a single request to save time during test execution @@ -112,7 +199,8 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe responseStatusCode, responseBody: options?.responseBodyOverride || - expectResponseBody(cases, responseStatusCode, options?.spaceId), + expectResponseBody(cases, responseStatusCode, options?.fail403Param, options?.spaceId), + overwrite, }, ]; }; @@ -134,8 +222,9 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe const requestBody = test.request .map((obj) => JSON.stringify({ ...obj, ...attrs })) .join('\n'); + const query = test.overwrite ? '?overwrite=true' : ''; await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_import`) + .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_import${query}`) .auth(user?.username, user?.password) .attach('file', Buffer.from(requestBody, 'utf8'), 'export.ndjson') .expect(test.responseStatusCode) diff --git a/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts index cb48f26ed645c..04fc6969b95d7 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts @@ -8,34 +8,86 @@ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; import { SPACES } from '../lib/spaces'; -import { - createRequest, - expectResponses, - getUrlPrefix, - getTestTitle, -} from '../lib/saved_object_test_utils'; +import { expectResponses, getUrlPrefix, getTestTitle } from '../lib/saved_object_test_utils'; import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; export interface ResolveImportErrorsTestDefinition extends TestDefinition { - request: Array<{ type: string; id: string }>; + request: { + objects: Array<{ type: string; id: string; originId?: string }>; + retries: Array< + { type: string; id: string } & ( + | { overwrite: true; idToOverwrite?: string } + | { duplicate: true } + | {} + ) + >; + }; overwrite: boolean; + duplicate: boolean; } export type ResolveImportErrorsTestSuite = TestSuite; export interface ResolveImportErrorsTestCase extends TestCase { + originId?: string; + idToOverwrite?: string; // only used for overwrite retries for multi-namespace object types + successParam?: string; failure?: 400 | 409; // only used for permitted response case } const NEW_ATTRIBUTE_KEY = 'title'; // all type mappings include this attribute, for simplicity's sake const NEW_ATTRIBUTE_VAL = `New attribute value ${Date.now()}`; -const NEW_SINGLE_NAMESPACE_OBJ = Object.freeze({ type: 'dashboard', id: 'new-dashboard-id' }); -const NEW_MULTI_NAMESPACE_OBJ = Object.freeze({ type: 'sharedtype', id: 'new-sharedtype-id' }); -const NEW_NAMESPACE_AGNOSTIC_OBJ = Object.freeze({ type: 'globaltype', id: 'new-globaltype-id' }); +// these five saved objects already exist in the sample data: +// * id: conflict_1 +// * id: conflict_2a, originId: conflict_2 +// * id: conflict_2b, originId: conflict_2 +// * id: conflict_3 +// * id: conflict_4a, originId: conflict_4 +// using the six conflict test case objects below, we can exercise various permutations of exact/inexact/ambiguous conflict scenarios +const CID = 'conflict_'; export const TEST_CASES = Object.freeze({ ...CASES, - NEW_SINGLE_NAMESPACE_OBJ, - NEW_MULTI_NAMESPACE_OBJ, - NEW_NAMESPACE_AGNOSTIC_OBJ, + CONFLICT_1A_OBJ: Object.freeze({ + type: 'sharedtype', + id: `${CID}1a`, + originId: `${CID}1`, + idToOverwrite: `${CID}1`, + }), + CONFLICT_1B_OBJ: Object.freeze({ type: 'sharedtype', id: `${CID}1b`, originId: `${CID}1` }), + CONFLICT_2C_OBJ: Object.freeze({ + type: 'sharedtype', + id: `${CID}2c`, + originId: `${CID}2`, + idToOverwrite: `${CID}2a`, + }), + CONFLICT_2D_OBJ: Object.freeze({ + type: 'sharedtype', + id: `${CID}2d`, + originId: `${CID}2`, + idToOverwrite: `${CID}2b`, + }), + CONFLICT_3A_OBJ: Object.freeze({ + type: 'sharedtype', + id: `${CID}3a`, + originId: `${CID}3`, + idToOverwrite: `${CID}3`, + }), + CONFLICT_4_OBJ: Object.freeze({ type: 'sharedtype', id: `${CID}4`, idToOverwrite: `${CID}4a` }), +}); + +/** + * Test cases have additional properties that we don't want to send in HTTP Requests + */ +const createRequest = ( + { type, id, originId, idToOverwrite }: ResolveImportErrorsTestCase, + overwrite: boolean, + duplicate: boolean +): ResolveImportErrorsTestDefinition['request'] => ({ + objects: [{ type, id, ...(originId && { originId }) }], + retries: overwrite + ? [{ type, id, overwrite, ...(idToOverwrite && { idToOverwrite }) }] + : duplicate + ? [{ type, id, duplicate: true }] + : [{ type, id }], }); export function resolveImportErrorsTestSuiteFactory( @@ -47,6 +99,7 @@ export function resolveImportErrorsTestSuiteFactory( const expectResponseBody = ( testCases: ResolveImportErrorsTestCase | ResolveImportErrorsTestCase[], statusCode: 200 | 403, + duplicate: boolean, spaceId = SPACES.DEFAULT.spaceId ): ExpectResponseBody => async (response: Record) => { const testCaseArray = Array.isArray(testCases) ? testCases : [testCases]; @@ -55,7 +108,7 @@ export function resolveImportErrorsTestSuiteFactory( await expectForbidden(types)(response); } else { // permitted - const { success, successCount, errors } = response.body; + const { success, successCount, successResults, errors } = response.body; const expectedSuccesses = testCaseArray.filter((x) => !x.failure); const expectedFailures = testCaseArray.filter((x) => x.failure); expect(success).to.eql(expectedFailures.length === 0); @@ -66,8 +119,27 @@ export function resolveImportErrorsTestSuiteFactory( expect(response.body).not.to.have.property('errors'); } for (let i = 0; i < expectedSuccesses.length; i++) { - const { type, id } = expectedSuccesses[i]; - const { _source } = await expectResponses.successCreated(es, spaceId, type, id); + const { type, id, successParam, idToOverwrite } = expectedSuccesses[i]; + // we don't know the order of the returned successResults; search for each one + const object = (successResults as Array>).find( + (x) => x.type === type && x.id === id + ); + expect(object).not.to.be(undefined); + const newId = object!.newId as string; + if (successParam === 'newId') { + // Kibana created the object with a different ID than what was specified in the import + // This can happen due to an unresolvable conflict (so the new ID will be random), or due to an inexact match (so the new ID will + // be equal to the ID or originID of the existing object that it inexactly matched) + if (idToOverwrite && !duplicate) { + expect(newId).to.be(idToOverwrite); + } else { + // the new ID was randomly generated + expect(newId).to.match(/^[0-9a-f-]{36}$/); + } + } else { + expect(newId).to.be(undefined); + } + const { _source } = await expectResponses.successCreated(es, spaceId, type, newId ?? id); expect(_source[type][NEW_ATTRIBUTE_KEY]).to.eql(NEW_ATTRIBUTE_VAL); } for (let i = 0; i < expectedFailures.length; i++) { @@ -90,6 +162,7 @@ export function resolveImportErrorsTestSuiteFactory( testCases: ResolveImportErrorsTestCase | ResolveImportErrorsTestCase[], forbidden: boolean, overwrite: boolean, + duplicate: boolean, options?: { spaceId?: string; singleRequest?: boolean; @@ -103,24 +176,31 @@ export function resolveImportErrorsTestSuiteFactory( // this ensures that multiple test cases of a single type will each result in a forbidden error return cases.map((x) => ({ title: getTestTitle(x, responseStatusCode), - request: [createRequest(x)], + request: createRequest(x, overwrite, duplicate), responseStatusCode, responseBody: options?.responseBodyOverride || - expectResponseBody(x, responseStatusCode, options?.spaceId), + expectResponseBody(x, responseStatusCode, duplicate, options?.spaceId), overwrite, + duplicate, })); } // batch into a single request to save time during test execution return [ { title: getTestTitle(cases, responseStatusCode), - request: cases.map((x) => createRequest(x)), + request: cases + .map((x) => createRequest(x, overwrite, duplicate)) + .reduce((acc, cur) => ({ + objects: [...acc.objects, ...cur.objects], + retries: [...acc.retries, ...cur.retries], + })), responseStatusCode, responseBody: options?.responseBodyOverride || - expectResponseBody(cases, responseStatusCode, options?.spaceId), + expectResponseBody(cases, responseStatusCode, duplicate, options?.spaceId), overwrite, + duplicate, }, ]; }; @@ -139,17 +219,13 @@ export function resolveImportErrorsTestSuiteFactory( for (const test of tests) { it(`should return ${test.responseStatusCode} ${test.title}`, async () => { - const retryAttrs = test.overwrite ? { overwrite: true } : {}; - const retries = JSON.stringify( - test.request.map(({ type, id }) => ({ type, id, ...retryAttrs })) - ); - const requestBody = test.request + const requestBody = test.request.objects .map((obj) => JSON.stringify({ ...obj, ...attrs })) .join('\n'); await supertest .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_resolve_import_errors`) .auth(user?.username, user?.password) - .field('retries', retries) + .field('retries', JSON.stringify(test.request.retries)) .attach('file', Buffer.from(requestBody, 'utf8'), 'export.ndjson') .expect(test.responseStatusCode) .then(test.responseBody); diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts index d83f3449460ce..0cc5969e2b7ab 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts @@ -20,6 +20,8 @@ const { SPACE_2: { spaceId: SPACE_2_ID }, } = SPACES; const { fail400, fail409 } = testCaseFailures; +const unresolvableConflict = (condition?: boolean) => + condition !== false ? { fail409Param: 'unresolvableConflict' } : {}; const createTestCases = (overwrite: boolean, spaceId: string) => { // for each permitted (non-403) outcome, if failure !== undefined then we expect @@ -34,9 +36,18 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite || (spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID)), + ...unresolvableConflict(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + }, + { + ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, + ...fail409(!overwrite || spaceId !== SPACE_1_ID), + ...unresolvableConflict(spaceId !== SPACE_1_ID), + }, + { + ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, + ...fail409(!overwrite || spaceId !== SPACE_2_ID), + ...unresolvableConflict(spaceId !== SPACE_2_ID), }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409(!overwrite || spaceId !== SPACE_1_ID) }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409(!overwrite || spaceId !== SPACE_2_ID) }, { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, CASES.NEW_SINGLE_NAMESPACE_OBJ, CASES.NEW_MULTI_NAMESPACE_OBJ, diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/export.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/export.ts index f85cd3a36c092..c581a1757565e 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/export.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/export.ts @@ -18,15 +18,12 @@ const createTestCases = (spaceId: string) => { const exportableTypes = [ cases.singleNamespaceObject, cases.singleNamespaceType, - cases.namespaceAgnosticObject, - cases.namespaceAgnosticType, - ]; - const nonExportableTypes = [ cases.multiNamespaceObject, cases.multiNamespaceType, - cases.hiddenObject, - cases.hiddenType, + cases.namespaceAgnosticObject, + cases.namespaceAgnosticType, ]; + const nonExportableTypes = [cases.hiddenObject, cases.hiddenType]; const allTypes = exportableTypes.concat(nonExportableTypes); return { exportableTypes, nonExportableTypes, allTypes }; }; diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts index 6b4dfe1d05f72..2a8db4dd7b671 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts @@ -20,27 +20,59 @@ const { SPACE_2: { spaceId: SPACE_2_ID }, } = SPACES; const { fail400, fail409 } = testCaseFailures; +const newId = (condition?: boolean) => (condition !== false ? { successParam: 'newId' } : {}); +const ambiguousConflict = (suffix: string) => ({ + failure: 409 as 409, + fail409Param: `ambiguous_conflict_${suffix}`, +}); -const createTestCases = (spaceId: string) => { +const createTestCases = (overwrite: boolean, spaceId: string) => { // for each permitted (non-403) outcome, if failure !== undefined then we expect // to receive an error; otherwise, we expect to receive a success result - const importableTypes = [ - { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(spaceId === DEFAULT_SPACE_ID) }, - { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(spaceId === SPACE_1_ID) }, - { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(spaceId === SPACE_2_ID) }, - { ...CASES.NAMESPACE_AGNOSTIC, ...fail409() }, + const group1Importable = [ + { + ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID), + }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID) }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, CASES.NEW_SINGLE_NAMESPACE_OBJ, CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, ]; - const nonImportableTypes = [ - { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail400() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail400() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail400() }, - { ...CASES.HIDDEN, ...fail400() }, - { ...CASES.NEW_MULTI_NAMESPACE_OBJ, ...fail400() }, + const group1NonImportable = [{ ...CASES.HIDDEN, ...fail400() }]; + const group1All = group1Importable.concat(group1NonImportable); + const group2 = [ + CASES.NEW_MULTI_NAMESPACE_OBJ, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail409(!overwrite && (spaceId === DEFAULT_SPACE_ID || spaceId === SPACE_1_ID)), + ...newId(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + }, + { + ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, + ...fail409(!overwrite && spaceId === SPACE_1_ID), + ...newId(spaceId !== SPACE_1_ID), + }, + { + ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, + ...fail409(!overwrite && spaceId === SPACE_2_ID), + ...newId(spaceId !== SPACE_2_ID), + }, + { ...CASES.CONFLICT_1A_OBJ, ...ambiguousConflict('1a1b') }, // "ambiguous source" conflict + { ...CASES.CONFLICT_1B_OBJ, ...ambiguousConflict('1a1b') }, // "ambiguous source" conflict + { ...CASES.CONFLICT_2C_OBJ, ...ambiguousConflict('2c') }, // "ambiguous destination" conflict + { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...newId() }, // "inexact match" conflict + { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...newId() }, // "inexact match" conflict ]; - const allTypes = importableTypes.concat(nonImportableTypes); - return { importableTypes, nonImportableTypes, allTypes }; + const group3 = [ + { ...CASES.CONFLICT_1_OBJ, ...fail409(!overwrite) }, // "exact match" conflict + CASES.CONFLICT_1A_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match + CASES.CONFLICT_1B_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match + { ...CASES.CONFLICT_2C_OBJ, ...ambiguousConflict('2c2d') }, // "ambiguous source and destination" conflict + { ...CASES.CONFLICT_2D_OBJ, ...ambiguousConflict('2c2d') }, // "ambiguous source and destination" conflict + ]; + return { group1Importable, group1NonImportable, group1All, group2, group3 }; }; export default function ({ getService }: FtrProviderContext) { @@ -53,44 +85,90 @@ export default function ({ getService }: FtrProviderContext) { esArchiver, supertest ); - const createTests = (spaceId: string) => { - const { importableTypes, nonImportableTypes, allTypes } = createTestCases(spaceId); + const createTests = (overwrite: boolean, spaceId: string) => { + const { group1Importable, group1NonImportable, group1All, group2, group3 } = createTestCases( + overwrite, + spaceId + ); // use singleRequest to reduce execution time and/or test combined cases + const unauthorizedCommon = [ + createTestDefinitions(group1Importable, true, overwrite, { + spaceId, + fail403Param: 'bulk_create', + }), + createTestDefinitions(group1NonImportable, false, overwrite, { + spaceId, + singleRequest: true, + }), + createTestDefinitions(group1All, true, overwrite, { + spaceId, + singleRequest: true, + responseBodyOverride: expectForbidden('bulk_create')([ + 'dashboard', + 'globaltype', + 'isolatedtype', + ]), + }), + ]; return { - unauthorized: [ - createTestDefinitions(importableTypes, true, { spaceId }), - createTestDefinitions(nonImportableTypes, false, { spaceId, singleRequest: true }), - createTestDefinitions(allTypes, true, { + unauthorizedRead: [ + ...unauthorizedCommon, + // multi-namespace types result in a preflight search request before a create attempt; + // because of this, importing those types will result in a 403 "find" error (as opposed to a 403 "bulk_create" error) + createTestDefinitions(group2, true, overwrite, { + spaceId, + singleRequest: true, + fail403Param: 'find', + }), + createTestDefinitions(group3, true, overwrite, { + spaceId, + singleRequest: true, + fail403Param: 'find', + }), + ].flat(), + unauthorizedWrite: [ + ...unauthorizedCommon, + createTestDefinitions(group2, true, overwrite, { + spaceId, + singleRequest: true, + fail403Param: 'bulk_create', + }), + createTestDefinitions(group3, true, overwrite, { spaceId, singleRequest: true, - responseBodyOverride: expectForbidden(['dashboard', 'globaltype', 'isolatedtype']), + fail403Param: 'bulk_create', }), ].flat(), - authorized: createTestDefinitions(allTypes, false, { spaceId, singleRequest: true }), + authorized: [ + createTestDefinitions(group1All, false, overwrite, { spaceId, singleRequest: true }), + createTestDefinitions(group2, false, overwrite, { spaceId, singleRequest: true }), + createTestDefinitions(group3, false, overwrite, { spaceId, singleRequest: true }), + ].flat(), }; }; describe('_import', () => { - getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => { - const suffix = ` within the ${spaceId} space`; - const { unauthorized, authorized } = createTests(spaceId); - const _addTests = (user: TestUser, tests: ImportTestDefinition[]) => { - addTests(`${user.description}${suffix}`, { user, spaceId, tests }); - }; + getTestScenarios([false, true]).securityAndSpaces.forEach( + ({ spaceId, users, modifier: overwrite }) => { + const suffix = ` within the ${spaceId} space${overwrite ? ' with overwrite enabled' : ''}`; + const { unauthorizedRead, unauthorizedWrite, authorized } = createTests( + overwrite!, + spaceId + ); + const _addTests = (user: TestUser, tests: ImportTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; - [ - users.noAccess, - users.legacyAll, - users.dualRead, - users.readGlobally, - users.readAtSpace, - users.allAtOtherSpace, - ].forEach((user) => { - _addTests(user, unauthorized); - }); - [users.dualAll, users.allGlobally, users.allAtSpace, users.superuser].forEach((user) => { - _addTests(user, authorized); - }); - }); + [users.noAccess, users.legacyAll, users.allAtOtherSpace].forEach((user) => { + _addTests(user, unauthorizedRead); + }); + [users.dualRead, users.readGlobally, users.readAtSpace].forEach((user) => { + _addTests(user, unauthorizedWrite); + }); + [users.dualAll, users.allGlobally, users.allAtSpace, users.superuser].forEach((user) => { + _addTests(user, authorized); + }); + } + ); }); } diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts index 8c16e298c7df9..1adbad321c656 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts @@ -20,30 +20,52 @@ const { SPACE_2: { spaceId: SPACE_2_ID }, } = SPACES; const { fail400, fail409 } = testCaseFailures; +const newId = (condition?: boolean) => (condition !== false ? { successParam: 'newId' } : {}); -const createTestCases = (overwrite: boolean, spaceId: string) => { +const createTestCases = (overwrite: boolean, duplicate: boolean, spaceId: string) => { // for each permitted (non-403) outcome, if failure !== undefined then we expect // to receive an error; otherwise, we expect to receive a success result - const importableTypes = [ + const singleNamespaceObject = + spaceId === DEFAULT_SPACE_ID + ? CASES.SINGLE_NAMESPACE_DEFAULT_SPACE + : spaceId === SPACE_1_ID + ? CASES.SINGLE_NAMESPACE_SPACE_1 + : CASES.SINGLE_NAMESPACE_SPACE_2; + const group1Importable = [ + { ...singleNamespaceObject, ...fail409(!overwrite && !duplicate), ...newId(duplicate) }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite && !duplicate), ...newId(duplicate) }, + ]; + const group1NonImportable = [{ ...CASES.HIDDEN, ...fail400() }]; + const group1All = [...group1Importable, ...group1NonImportable]; + const group2 = [ { - ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, - ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID), + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail409( + !overwrite && !duplicate && (spaceId === DEFAULT_SPACE_ID || spaceId === SPACE_1_ID) + ), + ...newId(duplicate || (spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID)), }, - { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID) }, - { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID) }, - { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, - CASES.NEW_SINGLE_NAMESPACE_OBJ, - CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, - ]; - const nonImportableTypes = [ - { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail400() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail400() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail400() }, - { ...CASES.HIDDEN, ...fail400() }, - { ...CASES.NEW_MULTI_NAMESPACE_OBJ, ...fail400() }, + { + ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, + ...fail409(!overwrite && !duplicate && spaceId === SPACE_1_ID), + ...newId(duplicate || spaceId !== SPACE_1_ID), + }, + { + ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, + ...fail409(!overwrite && !duplicate && spaceId === SPACE_2_ID), + ...newId(duplicate || spaceId !== SPACE_2_ID), + }, + // all of the cases below represent imports that had an inexact match conflict or an ambiguous conflict + // if we call _resolve_import_errors and don't specify overwrite or duplicate, each of these will not result in a conflict because they + // will skip the preflight search results; so the objects will be created instead. + { ...CASES.CONFLICT_1A_OBJ, ...newId(overwrite || duplicate) }, // "ambiguous source" conflict; if overwrite=true, will overwrite 'conflict_1' + { ...CASES.CONFLICT_1B_OBJ, ...newId(duplicate) }, // "ambiguous source" conflict; if overwrite=true, will create a new object (since 'conflict_1a' is overwriting 'conflict_1') + { ...CASES.CONFLICT_2C_OBJ, ...newId(overwrite || duplicate) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2a' + { ...CASES.CONFLICT_2D_OBJ, ...newId(overwrite || duplicate) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2b' + { ...CASES.CONFLICT_3A_OBJ, ...newId(overwrite || duplicate) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3' + { ...CASES.CONFLICT_4_OBJ, ...newId(overwrite || duplicate) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a' ]; - const allTypes = importableTypes.concat(nonImportableTypes); - return { importableTypes, nonImportableTypes, allTypes }; + return { group1Importable, group1NonImportable, group1All, group2 }; }; export default function ({ getService }: FtrProviderContext) { @@ -56,47 +78,66 @@ export default function ({ getService }: FtrProviderContext) { esArchiver, supertest ); - const createTests = (overwrite: boolean, spaceId: string) => { - const { importableTypes, nonImportableTypes, allTypes } = createTestCases(overwrite, spaceId); + const createTests = (overwrite: boolean, duplicate: boolean, spaceId: string) => { + const { group1Importable, group1NonImportable, group1All, group2 } = createTestCases( + overwrite, + duplicate, + spaceId + ); const singleRequest = true; // use singleRequest to reduce execution time and/or test combined cases return { unauthorized: [ - createTestDefinitions(importableTypes, true, overwrite, { spaceId }), - createTestDefinitions(nonImportableTypes, false, overwrite, { spaceId, singleRequest }), - createTestDefinitions(allTypes, true, overwrite, { + createTestDefinitions(group1Importable, true, overwrite, duplicate, { spaceId }), + createTestDefinitions(group1NonImportable, false, overwrite, duplicate, { + spaceId, + singleRequest, + }), + createTestDefinitions(group1All, true, overwrite, duplicate, { spaceId, singleRequest, - responseBodyOverride: expectForbidden(['dashboard', 'globaltype', 'isolatedtype']), + responseBodyOverride: expectForbidden(['globaltype', 'isolatedtype']), }), + createTestDefinitions(group2, true, overwrite, duplicate, { spaceId, singleRequest }), + ].flat(), + authorized: [ + createTestDefinitions(group1All, false, overwrite, duplicate, { spaceId, singleRequest }), + createTestDefinitions(group2, false, overwrite, duplicate, { spaceId, singleRequest }), ].flat(), - authorized: createTestDefinitions(allTypes, false, overwrite, { spaceId, singleRequest }), }; }; describe('_resolve_import_errors', () => { - getTestScenarios([false, true]).securityAndSpaces.forEach( - ({ spaceId, users, modifier: overwrite }) => { - const suffix = ` within the ${spaceId} space${overwrite ? ' with overwrite enabled' : ''}`; - const { unauthorized, authorized } = createTests(overwrite!, spaceId); - const _addTests = (user: TestUser, tests: ResolveImportErrorsTestDefinition[]) => { - addTests(`${user.description}${suffix}`, { user, spaceId, tests }); - }; + getTestScenarios([ + [false, false], + [false, true], + [true, false], + ]).securityAndSpaces.forEach(({ spaceId, users, modifier }) => { + const [overwrite, duplicate] = modifier!; + const suffix = + ` within the ${spaceId} space` + overwrite + ? ' with overwrite enabled' + : duplicate + ? ' with duplicate enabled' + : ''; + const { unauthorized, authorized } = createTests(overwrite, duplicate, spaceId); + const _addTests = (user: TestUser, tests: ResolveImportErrorsTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; - [ - users.noAccess, - users.legacyAll, - users.dualRead, - users.readGlobally, - users.readAtSpace, - users.allAtOtherSpace, - ].forEach((user) => { - _addTests(user, unauthorized); - }); - [users.dualAll, users.allGlobally, users.allAtSpace, users.superuser].forEach((user) => { - _addTests(user, authorized); - }); - } - ); + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.readAtSpace, + users.allAtOtherSpace, + ].forEach((user) => { + _addTests(user, unauthorized); + }); + [users.dualAll, users.allGlobally, users.allAtSpace, users.superuser].forEach((user) => { + _addTests(user, authorized); + }); + }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts index 464a5a1e76016..725120687c231 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts @@ -14,6 +14,7 @@ import { } from '../../common/suites/bulk_create'; const { fail400, fail409 } = testCaseFailures; +const unresolvableConflict = () => ({ fail409Param: 'unresolvableConflict' }); const createTestCases = (overwrite: boolean) => { // for each permitted (non-403) outcome, if failure !== undefined then we expect @@ -23,8 +24,8 @@ const createTestCases = (overwrite: boolean) => { CASES.SINGLE_NAMESPACE_SPACE_1, CASES.SINGLE_NAMESPACE_SPACE_2, { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite) }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409(), ...unresolvableConflict() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409(), ...unresolvableConflict() }, { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, CASES.NEW_SINGLE_NAMESPACE_OBJ, CASES.NEW_MULTI_NAMESPACE_OBJ, diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/export.ts b/x-pack/test/saved_object_api_integration/security_only/apis/export.ts index 61ff6eeb4bd80..99babf683ccfa 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/export.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/export.ts @@ -18,15 +18,12 @@ const createTestCases = () => { const exportableTypes = [ cases.singleNamespaceObject, cases.singleNamespaceType, - cases.namespaceAgnosticObject, - cases.namespaceAgnosticType, - ]; - const nonExportableTypes = [ cases.multiNamespaceObject, cases.multiNamespaceType, - cases.hiddenObject, - cases.hiddenType, + cases.namespaceAgnosticObject, + cases.namespaceAgnosticType, ]; + const nonExportableTypes = [cases.hiddenObject, cases.hiddenType]; const allTypes = exportableTypes.concat(nonExportableTypes); return { exportableTypes, nonExportableTypes, allTypes }; }; diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/import.ts b/x-pack/test/saved_object_api_integration/security_only/apis/import.ts index beec276b3bd73..74e569195f351 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/import.ts @@ -14,27 +14,44 @@ import { } from '../../common/suites/import'; const { fail400, fail409 } = testCaseFailures; +const newId = (condition?: boolean) => (condition !== false ? { successParam: 'newId' } : {}); +const ambiguousConflict = (suffix: string) => ({ + failure: 409 as 409, + fail409Param: `ambiguous_conflict_${suffix}`, +}); -const createTestCases = () => { +const createTestCases = (overwrite: boolean) => { // for each permitted (non-403) outcome, if failure !== undefined then we expect // to receive an error; otherwise, we expect to receive a success result - const importableTypes = [ - { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409() }, + const group1Importable = [ + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(!overwrite) }, CASES.SINGLE_NAMESPACE_SPACE_1, CASES.SINGLE_NAMESPACE_SPACE_2, - { ...CASES.NAMESPACE_AGNOSTIC, ...fail409() }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, CASES.NEW_SINGLE_NAMESPACE_OBJ, CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, ]; - const nonImportableTypes = [ - { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail400() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail400() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail400() }, - { ...CASES.HIDDEN, ...fail400() }, - { ...CASES.NEW_MULTI_NAMESPACE_OBJ, ...fail400() }, + const group1NonImportable = [{ ...CASES.HIDDEN, ...fail400() }]; + const group1All = group1Importable.concat(group1NonImportable); + const group2 = [ + CASES.NEW_MULTI_NAMESPACE_OBJ, + { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...newId() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...newId() }, + { ...CASES.CONFLICT_1A_OBJ, ...ambiguousConflict('1a1b') }, // "ambiguous source" conflict + { ...CASES.CONFLICT_1B_OBJ, ...ambiguousConflict('1a1b') }, // "ambiguous source" conflict + { ...CASES.CONFLICT_2C_OBJ, ...ambiguousConflict('2c') }, // "ambiguous destination" conflict + { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...newId() }, // "inexact match" conflict + { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...newId() }, // "inexact match" conflict ]; - const allTypes = importableTypes.concat(nonImportableTypes); - return { importableTypes, nonImportableTypes, allTypes }; + const group3 = [ + { ...CASES.CONFLICT_1_OBJ, ...fail409(!overwrite) }, // "exact match" conflict + CASES.CONFLICT_1A_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match + CASES.CONFLICT_1B_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match + { ...CASES.CONFLICT_2C_OBJ, ...ambiguousConflict('2c2d') }, // "ambiguous source and destination" conflict + { ...CASES.CONFLICT_2D_OBJ, ...ambiguousConflict('2c2d') }, // "ambiguous source and destination" conflict + ]; + return { group1Importable, group1NonImportable, group1All, group2, group3 }; }; export default function ({ getService }: FtrProviderContext) { @@ -47,40 +64,76 @@ export default function ({ getService }: FtrProviderContext) { esArchiver, supertest ); - const createTests = () => { - const { importableTypes, nonImportableTypes, allTypes } = createTestCases(); + const createTests = (overwrite: boolean) => { + const { group1Importable, group1NonImportable, group1All, group2, group3 } = createTestCases( + overwrite + ); // use singleRequest to reduce execution time and/or test combined cases + const unauthorizedCommon = [ + createTestDefinitions(group1Importable, true, overwrite, { fail403Param: 'bulk_create' }), + createTestDefinitions(group1NonImportable, false, overwrite, { singleRequest: true }), + createTestDefinitions(group1All, true, overwrite, { + singleRequest: true, + responseBodyOverride: expectForbidden('bulk_create')([ + 'dashboard', + 'globaltype', + 'isolatedtype', + ]), + }), + ]; return { - unauthorized: [ - createTestDefinitions(importableTypes, true), - createTestDefinitions(nonImportableTypes, false, { singleRequest: true }), - createTestDefinitions(allTypes, true, { + unauthorizedRead: [ + ...unauthorizedCommon, + // multi-namespace types result in a preflight search request before a create attempt; + // because of this, importing those types will result in a 403 "find" error (as opposed to a 403 "bulk_create" error) + createTestDefinitions(group2, true, overwrite, { + singleRequest: true, + fail403Param: 'find', + }), + createTestDefinitions(group3, true, overwrite, { + singleRequest: true, + fail403Param: 'find', + }), + ].flat(), + unauthorizedWrite: [ + ...unauthorizedCommon, + createTestDefinitions(group2, true, overwrite, { + singleRequest: true, + fail403Param: 'bulk_create', + }), + createTestDefinitions(group3, true, overwrite, { singleRequest: true, - responseBodyOverride: expectForbidden(['dashboard', 'globaltype', 'isolatedtype']), + fail403Param: 'bulk_create', }), ].flat(), - authorized: createTestDefinitions(allTypes, false, { singleRequest: true }), + authorized: [ + createTestDefinitions(group1All, false, overwrite, { singleRequest: true }), + createTestDefinitions(group2, false, overwrite, { singleRequest: true }), + createTestDefinitions(group3, false, overwrite, { singleRequest: true }), + ].flat(), }; }; describe('_import', () => { - getTestScenarios().security.forEach(({ users }) => { - const { unauthorized, authorized } = createTests(); + getTestScenarios([false, true]).security.forEach(({ users, modifier: overwrite }) => { + const suffix = overwrite ? ' with overwrite enabled' : ''; + const { unauthorizedRead, unauthorizedWrite, authorized } = createTests(overwrite!); const _addTests = (user: TestUser, tests: ImportTestDefinition[]) => { - addTests(user.description, { user, tests }); + addTests(`${user.description}${suffix}`, { user, tests }); }; [ users.noAccess, users.legacyAll, - users.dualRead, - users.readGlobally, users.allAtDefaultSpace, users.readAtDefaultSpace, users.allAtSpace1, users.readAtSpace1, ].forEach((user) => { - _addTests(user, unauthorized); + _addTests(user, unauthorizedRead); + }); + [users.dualRead, users.readGlobally].forEach((user) => { + _addTests(user, unauthorizedWrite); }); [users.dualAll, users.allGlobally, users.superuser].forEach((user) => { _addTests(user, authorized); diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts index a0abe4b0483f8..4d80225d1de52 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts @@ -14,27 +14,38 @@ import { } from '../../common/suites/resolve_import_errors'; const { fail400, fail409 } = testCaseFailures; +const newId = (condition?: boolean) => (condition !== false ? { successParam: 'newId' } : {}); -const createTestCases = (overwrite: boolean) => { +const createTestCases = (overwrite: boolean, duplicate: boolean) => { // for each permitted (non-403) outcome, if failure !== undefined then we expect // to receive an error; otherwise, we expect to receive a success result - const importableTypes = [ - { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(!overwrite) }, - CASES.SINGLE_NAMESPACE_SPACE_1, - CASES.SINGLE_NAMESPACE_SPACE_2, - { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, - CASES.NEW_SINGLE_NAMESPACE_OBJ, - CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, + const group1Importable = [ + { + ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + ...fail409(!overwrite && !duplicate), + ...newId(duplicate), + }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite && !duplicate), ...newId(duplicate) }, ]; - const nonImportableTypes = [ - { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail400() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail400() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail400() }, - { ...CASES.HIDDEN, ...fail400() }, - { ...CASES.NEW_MULTI_NAMESPACE_OBJ, ...fail400() }, + const group1NonImportable = [{ ...CASES.HIDDEN, ...fail400() }]; + const group1All = [...group1Importable, ...group1NonImportable]; + const group2 = [ + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail409(!overwrite && !duplicate), + ...newId(duplicate), + }, + // all of the cases below represent imports that had an inexact match conflict or an ambiguous conflict + // if we call _resolve_import_errors and don't specify overwrite or duplicate, each of these will not result in a conflict because they + // will skip the preflight search results; so the objects will be created instead. + { ...CASES.CONFLICT_1A_OBJ, ...newId(overwrite || duplicate) }, // "ambiguous source" conflict; if overwrite=true, will overwrite 'conflict_1' + { ...CASES.CONFLICT_1B_OBJ, ...newId(duplicate) }, // "ambiguous source" conflict; if overwrite=true, will create a new object (since 'conflict_1a' is overwriting 'conflict_1') + { ...CASES.CONFLICT_2C_OBJ, ...newId(overwrite || duplicate) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2a' + { ...CASES.CONFLICT_2D_OBJ, ...newId(overwrite || duplicate) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2b' + { ...CASES.CONFLICT_3A_OBJ, ...newId(overwrite || duplicate) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3' + { ...CASES.CONFLICT_4_OBJ, ...newId(overwrite || duplicate) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a' ]; - const allTypes = importableTypes.concat(nonImportableTypes); - return { importableTypes, nonImportableTypes, allTypes }; + return { group1Importable, group1NonImportable, group1All, group2 }; }; export default function ({ getService }: FtrProviderContext) { @@ -47,26 +58,44 @@ export default function ({ getService }: FtrProviderContext) { esArchiver, supertest ); - const createTests = (overwrite: boolean) => { - const { importableTypes, nonImportableTypes, allTypes } = createTestCases(overwrite); + const createTests = (overwrite: boolean, duplicate: boolean) => { + const { group1Importable, group1NonImportable, group1All, group2 } = createTestCases( + overwrite, + duplicate + ); // use singleRequest to reduce execution time and/or test combined cases return { unauthorized: [ - createTestDefinitions(importableTypes, true, overwrite), - createTestDefinitions(nonImportableTypes, false, overwrite, { singleRequest: true }), - createTestDefinitions(allTypes, true, overwrite, { + createTestDefinitions(group1Importable, true, overwrite, duplicate), + createTestDefinitions(group1NonImportable, false, overwrite, duplicate, { singleRequest: true, - responseBodyOverride: expectForbidden(['dashboard', 'globaltype', 'isolatedtype']), }), + createTestDefinitions(group1All, true, overwrite, duplicate, { + singleRequest: true, + responseBodyOverride: expectForbidden(['globaltype', 'isolatedtype']), + }), + createTestDefinitions(group2, true, overwrite, duplicate, { singleRequest: true }), + ].flat(), + authorized: [ + createTestDefinitions(group1All, false, overwrite, duplicate, { singleRequest: true }), + createTestDefinitions(group2, false, overwrite, duplicate, { singleRequest: true }), ].flat(), - authorized: createTestDefinitions(allTypes, false, overwrite, { singleRequest: true }), }; }; describe('_resolve_import_errors', () => { - getTestScenarios([false, true]).security.forEach(({ users, modifier: overwrite }) => { - const suffix = overwrite ? ' with overwrite enabled' : ''; - const { unauthorized, authorized } = createTests(overwrite!); + getTestScenarios([ + [false, false], + [false, true], + [true, false], + ]).security.forEach(({ users, modifier }) => { + const [overwrite, duplicate] = modifier!; + const suffix = overwrite + ? ' with overwrite enabled' + : duplicate + ? ' with duplicate enabled' + : ''; + const { unauthorized, authorized } = createTests(overwrite, duplicate); const _addTests = (user: TestUser, tests: ResolveImportErrorsTestDefinition[]) => { addTests(`${user.description}${suffix}`, { user, tests }); }; diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts index f9edc56b8ffea..74fade39bf7a5 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts @@ -16,6 +16,8 @@ const { SPACE_2: { spaceId: SPACE_2_ID }, } = SPACES; const { fail400, fail409 } = testCaseFailures; +const unresolvableConflict = (condition?: boolean) => + condition !== false ? { fail409Param: 'unresolvableConflict' } : {}; const createTestCases = (overwrite: boolean, spaceId: string) => [ // for each outcome, if failure !== undefined then we expect to receive @@ -29,9 +31,18 @@ const createTestCases = (overwrite: boolean, spaceId: string) => [ { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite || (spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID)), + ...unresolvableConflict(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + }, + { + ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, + ...fail409(!overwrite || spaceId !== SPACE_1_ID), + ...unresolvableConflict(spaceId !== SPACE_1_ID), + }, + { + ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, + ...fail409(!overwrite || spaceId !== SPACE_2_ID), + ...unresolvableConflict(spaceId !== SPACE_2_ID), }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409(!overwrite || spaceId !== SPACE_1_ID) }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409(!overwrite || spaceId !== SPACE_2_ID) }, { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, { ...CASES.HIDDEN, ...fail400() }, CASES.NEW_SINGLE_NAMESPACE_OBJ, diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts index 45a76a2f39e37..7712ac8561389 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts @@ -15,22 +15,57 @@ const { SPACE_2: { spaceId: SPACE_2_ID }, } = SPACES; const { fail400, fail409 } = testCaseFailures; +const newId = (condition?: boolean) => (condition !== false ? { successParam: 'newId' } : {}); +const ambiguousConflict = (suffix: string) => ({ + failure: 409 as 409, + fail409Param: `ambiguous_conflict_${suffix}`, +}); -const createTestCases = (spaceId: string) => [ +const createTestCases = (overwrite: boolean, spaceId: string) => { // for each outcome, if failure !== undefined then we expect to receive // an error; otherwise, we expect to receive a success result - { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(spaceId === DEFAULT_SPACE_ID) }, - { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(spaceId === SPACE_1_ID) }, - { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(spaceId === SPACE_2_ID) }, - { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail400() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail400() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail400() }, - { ...CASES.NAMESPACE_AGNOSTIC, ...fail409() }, - { ...CASES.HIDDEN, ...fail400() }, - CASES.NEW_SINGLE_NAMESPACE_OBJ, - { ...CASES.NEW_MULTI_NAMESPACE_OBJ, ...fail400() }, - CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, -]; + const group1 = [ + { + ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID), + }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID) }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail409(!overwrite && (spaceId === DEFAULT_SPACE_ID || spaceId === SPACE_1_ID)), + ...newId(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + }, + { + ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, + ...fail409(!overwrite && spaceId === SPACE_1_ID), + ...newId(spaceId !== SPACE_1_ID), + }, + { + ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, + ...fail409(!overwrite && spaceId === SPACE_2_ID), + ...newId(spaceId !== SPACE_2_ID), + }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, + { ...CASES.HIDDEN, ...fail400() }, + { ...CASES.CONFLICT_1A_OBJ, ...ambiguousConflict('1a1b') }, // "ambiguous source" conflict + { ...CASES.CONFLICT_1B_OBJ, ...ambiguousConflict('1a1b') }, // "ambiguous source" conflict + { ...CASES.CONFLICT_2C_OBJ, ...ambiguousConflict('2c') }, // "ambiguous destination" conflict + { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...newId() }, // "inexact match" conflict + { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...newId() }, // "inexact match" conflict + CASES.NEW_SINGLE_NAMESPACE_OBJ, + CASES.NEW_MULTI_NAMESPACE_OBJ, + CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, + ]; + const group2 = [ + { ...CASES.CONFLICT_1_OBJ, ...fail409(!overwrite) }, // "exact match" conflict + CASES.CONFLICT_1A_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match + CASES.CONFLICT_1B_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match + { ...CASES.CONFLICT_2C_OBJ, ...ambiguousConflict('2c2d') }, // "ambiguous source and destination" conflict + { ...CASES.CONFLICT_2D_OBJ, ...ambiguousConflict('2c2d') }, // "ambiguous source and destination" conflict + ]; + return { group1, group2 }; +}; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -38,15 +73,19 @@ export default function ({ getService }: FtrProviderContext) { const es = getService('legacyEs'); const { addTests, createTestDefinitions } = importTestSuiteFactory(es, esArchiver, supertest); - const createTests = (spaceId: string) => { - const testCases = createTestCases(spaceId); - return createTestDefinitions(testCases, false, { spaceId, singleRequest: true }); + const createTests = (overwrite: boolean, spaceId: string) => { + const { group1, group2 } = createTestCases(overwrite, spaceId); + return [ + createTestDefinitions(group1, false, overwrite, { spaceId, singleRequest: true }), + createTestDefinitions(group2, false, overwrite, { spaceId, singleRequest: true }), + ].flat(); }; describe('_import', () => { - getTestScenarios().spaces.forEach(({ spaceId }) => { - const tests = createTests(spaceId); - addTests(`within the ${spaceId} space`, { spaceId, tests }); + getTestScenarios([false, true]).spaces.forEach(({ spaceId, modifier: overwrite }) => { + const suffix = overwrite ? ' with overwrite enabled' : ''; + const tests = createTests(overwrite!, spaceId); + addTests(`within the ${spaceId} space${suffix}`, { spaceId, tests }); }); }); } diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts index a6ef902e2e9eb..f06c15775a5c6 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts @@ -18,25 +18,49 @@ const { SPACE_2: { spaceId: SPACE_2_ID }, } = SPACES; const { fail400, fail409 } = testCaseFailures; +const newId = (condition?: boolean) => (condition !== false ? { successParam: 'newId' } : {}); -const createTestCases = (overwrite: boolean, spaceId: string) => [ +const createTestCases = (overwrite: boolean, duplicate: boolean, spaceId: string) => { // for each outcome, if failure !== undefined then we expect to receive // an error; otherwise, we expect to receive a success result - { - ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, - ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID), - }, - { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID) }, - { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID) }, - { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail400() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail400() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail400() }, - { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, - { ...CASES.HIDDEN, ...fail400() }, - CASES.NEW_SINGLE_NAMESPACE_OBJ, - { ...CASES.NEW_MULTI_NAMESPACE_OBJ, ...fail400() }, - CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, -]; + const singleNamespaceObject = + spaceId === DEFAULT_SPACE_ID + ? CASES.SINGLE_NAMESPACE_DEFAULT_SPACE + : spaceId === SPACE_1_ID + ? CASES.SINGLE_NAMESPACE_SPACE_1 + : CASES.SINGLE_NAMESPACE_SPACE_2; + return [ + { ...singleNamespaceObject, ...fail409(!overwrite && !duplicate), ...newId(duplicate) }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail409( + !overwrite && !duplicate && (spaceId === DEFAULT_SPACE_ID || spaceId === SPACE_1_ID) + ), + ...newId(duplicate || (spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID)), + }, + { + ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, + ...fail409(!overwrite && !duplicate && spaceId === SPACE_1_ID), + ...newId(duplicate || spaceId !== SPACE_1_ID), + }, + { + ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, + ...fail409(!overwrite && !duplicate && spaceId === SPACE_2_ID), + ...newId(duplicate || spaceId !== SPACE_2_ID), + }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite && !duplicate), ...newId(duplicate) }, + { ...CASES.HIDDEN, ...fail400() }, + // all of the cases below represent imports that had an inexact match conflict or an ambiguous conflict + // if we call _resolve_import_errors and don't specify overwrite or duplicate, each of these will not result in a conflict because they + // will skip the preflight search results; so the objects will be created instead. + { ...CASES.CONFLICT_1A_OBJ, ...newId(overwrite || duplicate) }, // "ambiguous source" conflict; if overwrite=true, will overwrite 'conflict_1' + { ...CASES.CONFLICT_1B_OBJ, ...newId(duplicate) }, // "ambiguous source" conflict; if overwrite=true, will create a new object (since 'conflict_1a' is overwriting 'conflict_1') + { ...CASES.CONFLICT_2C_OBJ, ...newId(overwrite || duplicate) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2a' + { ...CASES.CONFLICT_2D_OBJ, ...newId(overwrite || duplicate) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2b' + { ...CASES.CONFLICT_3A_OBJ, ...newId(overwrite || duplicate) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3' + { ...CASES.CONFLICT_4_OBJ, ...newId(overwrite || duplicate) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a' + ]; +}; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -48,15 +72,27 @@ export default function ({ getService }: FtrProviderContext) { esArchiver, supertest ); - const createTests = (overwrite: boolean, spaceId: string) => { - const testCases = createTestCases(overwrite, spaceId); - return createTestDefinitions(testCases, false, overwrite, { spaceId, singleRequest: true }); + const createTests = (overwrite: boolean, duplicate: boolean, spaceId: string) => { + const testCases = createTestCases(overwrite, duplicate, spaceId); + return createTestDefinitions(testCases, false, overwrite, duplicate, { + spaceId, + singleRequest: true, + }); }; describe('_resolve_import_errors', () => { - getTestScenarios([false, true]).spaces.forEach(({ spaceId, modifier: overwrite }) => { - const suffix = overwrite ? ' with overwrite enabled' : ''; - const tests = createTests(overwrite!, spaceId); + getTestScenarios([ + [false, false], + [false, true], + [true, false], + ]).spaces.forEach(({ spaceId, modifier }) => { + const [overwrite, duplicate] = modifier!; + const suffix = overwrite + ? ' with overwrite enabled' + : duplicate + ? ' with duplicate enabled' + : ''; + const tests = createTests(overwrite, duplicate, spaceId); addTests(`within the ${spaceId} space${suffix}`, { spaceId, tests }); }); }); diff --git a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json index 9a8a0a1fdda14..7e528c23c20a0 100644 --- a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json +++ b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json @@ -380,11 +380,11 @@ { "type": "doc", "value": { - "id": "sharedtype:default_space_only", + "id": "sharedtype:default_only", "index": ".kibana", "source": { "sharedtype": { - "title": "A shared saved-object in the default space" + "title": "A shared saved-object in one space" }, "type": "sharedtype", "namespaces": ["default"], @@ -401,7 +401,7 @@ "index": ".kibana", "source": { "sharedtype": { - "title": "A shared saved-object in the space_1 space" + "title": "A shared saved-object in one space" }, "type": "sharedtype", "namespaces": ["space_1"], @@ -418,7 +418,7 @@ "index": ".kibana", "source": { "sharedtype": { - "title": "A shared saved-object in the space_2 space" + "title": "A shared saved-object in one space" }, "type": "sharedtype", "namespaces": ["space_2"], @@ -496,3 +496,128 @@ } } +{ + "type": "doc", + "value": { + "id": "sharedtype:conflict_1_default", + "index": ".kibana", + "source": { + "originId": "conflict_1", + "sharedtype": { + "title": "A shared saved-object in one space" + }, + "type": "sharedtype", + "namespaces": ["default"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:conflict_1_space_1", + "index": ".kibana", + "source": { + "originId": "conflict_1", + "sharedtype": { + "title": "A shared saved-object in one space" + }, + "type": "sharedtype", + "namespaces": ["space_1"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:conflict_1_space_2", + "index": ".kibana", + "source": { + "originId": "conflict_1", + "sharedtype": { + "title": "A shared saved-object in one space" + }, + "type": "sharedtype", + "namespaces": ["space_2"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:conflict_2_default", + "index": ".kibana", + "source": { + "originId": "conflict_2", + "sharedtype": { + "title": "A shared saved-object in one space" + }, + "type": "sharedtype", + "namespaces": ["default"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:conflict_2_space_1", + "index": ".kibana", + "source": { + "originId": "conflict_2", + "sharedtype": { + "title": "A shared saved-object in one space" + }, + "type": "sharedtype", + "namespaces": ["space_1"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:conflict_2_space_2", + "index": ".kibana", + "source": { + "originId": "conflict_2", + "sharedtype": { + "title": "A shared saved-object in one space" + }, + "type": "sharedtype", + "namespaces": ["space_2"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:conflict_2_all", + "index": ".kibana", + "source": { + "originId": "conflict_2", + "sharedtype": { + "title": "A shared saved-object in all spaces" + }, + "type": "sharedtype", + "namespaces": ["default", "space_1", "space_2"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} diff --git a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json index 508de68c32f70..a2f8088ce0436 100644 --- a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json +++ b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json @@ -162,6 +162,9 @@ "namespaces": { "type": "keyword" }, + "originId": { + "type": "keyword" + }, "search": { "properties": { "columns": { diff --git a/x-pack/test/spaces_api_integration/common/fixtures/spaces_test_plugin/server/plugin.ts b/x-pack/test/spaces_api_integration/common/fixtures/spaces_test_plugin/server/plugin.ts index ee03fa6b648af..ed45f0870a45c 100644 --- a/x-pack/test/spaces_api_integration/common/fixtures/spaces_test_plugin/server/plugin.ts +++ b/x-pack/test/spaces_api_integration/common/fixtures/spaces_test_plugin/server/plugin.ts @@ -15,6 +15,12 @@ export class Plugin { name: 'sharedtype', hidden: false, namespaceType: 'multiple', + management: { + importableAndExportable: true, + getTitle(obj) { + return obj.attributes.title; + }, + }, mappings: { properties: { title: { type: 'text' }, diff --git a/x-pack/test/spaces_api_integration/common/lib/saved_object_test_cases.ts b/x-pack/test/spaces_api_integration/common/lib/saved_object_test_cases.ts index 67f5d737ba010..3b0f5f8570aa3 100644 --- a/x-pack/test/spaces_api_integration/common/lib/saved_object_test_cases.ts +++ b/x-pack/test/spaces_api_integration/common/lib/saved_object_test_cases.ts @@ -5,8 +5,8 @@ */ export const MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES = Object.freeze({ - DEFAULT_SPACE_ONLY: Object.freeze({ - id: 'default_space_only', + DEFAULT_ONLY: Object.freeze({ + id: 'default_only', existingNamespaces: ['default'], }), SPACE_1_ONLY: Object.freeze({ diff --git a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts index 4d9aee50c219d..2c016c4682d23 100644 --- a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts @@ -19,6 +19,11 @@ interface CopyToSpaceTest { response: (resp: TestResponse) => Promise; } +interface CopyToSpaceMultiNamespaceTest extends CopyToSpaceTest { + testTitle: string; + objects: Array>; +} + interface CopyToSpaceTests { noConflictsWithoutReferences: CopyToSpaceTest; noConflictsWithReferences: CopyToSpaceTest; @@ -30,6 +35,7 @@ interface CopyToSpaceTests { withConflictsResponse: (resp: TestResponse) => Promise; noConflictsResponse: (resp: TestResponse) => Promise; }; + multiNamespaceTestCases: (overwrite: boolean) => CopyToSpaceMultiNamespaceTest[]; } interface CopyToSpaceTestDefinition { @@ -325,6 +331,168 @@ export function copyToSpaceTestSuiteFactory( }); }; + /** + * Creates test cases for multi-namespace saved object types. + * Note: these are written with the assumption that test data will only be reloaded between each group of test cases, *not* before every + * single test case. This saves time during test execution. + */ + const createMultiNamespaceTestCases = ( + spaceId: string, + outcome: 'authorized' | 'unauthorizedRead' | 'unauthorizedWrite' | 'noAccess' = 'authorized' + ) => (overwrite: boolean): CopyToSpaceMultiNamespaceTest[] => { + // the status code of the HTTP response differs depending on the error type + // a 403 error actually comes back as an HTTP 200 response + const statusCode = outcome === 'noAccess' ? 404 : 200; + const type = 'sharedtype'; + const v4 = new RegExp(/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i); + const noConflictId = `${spaceId}_only`; + const exactMatchId = 'all_spaces'; + const inexactMatchId = `conflict_1_${spaceId}`; + const ambiguousConflictId = `conflict_2_${spaceId}`; + + const getResult = (response: TestResponse) => (response.body as CopyResponse).space_2; + const expectForbiddenResponse = (response: TestResponse) => { + const action = outcome === 'unauthorizedRead' ? 'find' : 'bulk_create'; + expect(response.body).to.eql({ + space_2: { + success: false, + successCount: 0, + errors: [ + { statusCode: 403, error: 'Forbidden', message: `Unable to ${action} sharedtype` }, + ], + }, + }); + }; + + return [ + { + testTitle: 'copying with no conflict', + objects: [{ type, id: noConflictId }], + statusCode, + response: async (response: TestResponse) => { + if (outcome === 'authorized') { + const { success, successCount, successResults, errors } = getResult(response); + expect(success).to.eql(true); + expect(successCount).to.eql(1); + const newId = successResults![0].newId; + expect(newId).to.match(v4); + expect(successResults).to.eql([{ type, id: noConflictId, newId }]); + expect(errors).to.be(undefined); + } else if (outcome === 'noAccess') { + expectNotFoundResponse(response); + } else { + // unauthorized read/write + expectForbiddenResponse(response); + } + }, + }, + { + testTitle: 'copying with an exact match conflict', + objects: [{ type, id: exactMatchId }], + statusCode, + response: async (response: TestResponse) => { + if (outcome === 'authorized') { + const { success, successCount, successResults, errors } = getResult(response); + if (overwrite) { + expect(success).to.eql(true); + expect(successCount).to.eql(1); + expect(successResults).to.eql([{ type, id: exactMatchId }]); + expect(errors).to.be(undefined); + } else { + expect(success).to.eql(false); + expect(successCount).to.eql(0); + expect(successResults).to.be(undefined); + expect(errors).to.eql([ + { + error: { type: 'conflict' }, + type, + id: exactMatchId, + title: 'A shared saved-object in the default, space_1, and space_2 spaces', + }, + ]); + } + } else if (outcome === 'noAccess') { + expectNotFoundResponse(response); + } else { + // unauthorized read/write + expectForbiddenResponse(response); + } + }, + }, + { + testTitle: 'copying with an inexact match conflict', + objects: [{ type, id: inexactMatchId }], + statusCode, + response: async (response: TestResponse) => { + if (outcome === 'authorized') { + const { success, successCount, successResults, errors } = getResult(response); + const newId = 'conflict_1_space_2'; + if (overwrite) { + expect(success).to.eql(true); + expect(successCount).to.eql(1); + expect(successResults).to.eql([{ type, id: inexactMatchId, newId }]); + expect(errors).to.be(undefined); + } else { + expect(success).to.eql(false); + expect(successCount).to.eql(0); + expect(successResults).to.be(undefined); + expect(errors).to.eql([ + { + error: { type: 'conflict', destinationId: newId }, + type, + id: inexactMatchId, + title: 'A shared saved-object in one space', + }, + ]); + } + } else if (outcome === 'noAccess') { + expectNotFoundResponse(response); + } else { + // unauthorized read/write + expectForbiddenResponse(response); + } + }, + }, + { + testTitle: 'copying with an ambiguous conflict', + objects: [{ type, id: ambiguousConflictId }], + statusCode, + response: async (response: TestResponse) => { + if (outcome === 'authorized' || outcome === 'unauthorizedWrite') { + // when an ambiguous conflict is encountered, the import function never actually attempts to create the object -- + // because of that, a consumer who is authorized to read (but not write) will see the same response as a user who is authorized + const { success, successCount, successResults, errors } = getResult(response); + const updatedAt = '2017-09-21T18:59:16.270Z'; + const sources = [ + { id: ambiguousConflictId, title: 'A shared saved-object in one space', updatedAt }, + ]; + const destinations = [ + // response should be sorted by ID in ascending order + { id: 'conflict_2_all', title: 'A shared saved-object in all spaces', updatedAt }, + { id: 'conflict_2_space_2', title: 'A shared saved-object in one space', updatedAt }, + ]; + expect(success).to.eql(false); + expect(successCount).to.eql(0); + expect(successResults).to.be(undefined); + expect(errors).to.eql([ + { + error: { type: 'ambiguous_conflict', sources, destinations }, + type, + id: ambiguousConflictId, + title: 'A shared saved-object in one space', + }, + ]); + } else if (outcome === 'noAccess') { + expectNotFoundResponse(response); + } else { + // unauthorized read + expectForbiddenResponse(response); + } + }, + }, + ]; + }; + const makeCopyToSpaceTest = (describeFn: DescribeFn) => ( description: string, { user = {}, spaceId = DEFAULT_SPACE_ID, tests }: CopyToSpaceTestDefinition @@ -462,6 +630,27 @@ export function copyToSpaceTestSuiteFactory( .then(tests.nonExistentSpace.response); }); }); + + [false, true].forEach((overwrite) => { + const spaces = ['space_2']; + const includeReferences = false; + describe(`multi-namespace types with overwrite=${overwrite}`, () => { + before(() => esArchiver.load('saved_objects/spaces')); + after(() => esArchiver.unload('saved_objects/spaces')); + + const testCases = tests.multiNamespaceTestCases(overwrite); + testCases.forEach(({ testTitle, objects, statusCode, response }) => { + it(`should return ${statusCode} when ${testTitle}`, async () => { + return supertest + .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`) + .auth(user.username, user.password) + .send({ objects, spaces, includeReferences, overwrite }) + .expect(statusCode) + .then(response); + }); + }); + }); + }); }); }; @@ -479,6 +668,7 @@ export function copyToSpaceTestSuiteFactory( expectNotFoundResponse, createExpectUnauthorizedAtSpaceWithReferencesResult, createExpectUnauthorizedAtSpaceWithoutReferencesResult, + createMultiNamespaceTestCases, originSpaces: ['default', 'space_1'], }; } diff --git a/x-pack/test/spaces_api_integration/common/suites/delete.ts b/x-pack/test/spaces_api_integration/common/suites/delete.ts index 15a90092f5517..69b5697d8a9a8 100644 --- a/x-pack/test/spaces_api_integration/common/suites/delete.ts +++ b/x-pack/test/spaces_api_integration/common/suites/delete.ts @@ -130,7 +130,7 @@ export function deleteTestSuiteFactory(es: any, esArchiver: any, supertest: Supe expect(buckets).to.eql(expectedBuckets); - // There were seven multi-namespace objects. + // There were eleven multi-namespace objects. // Since Space 2 was deleted, any multi-namespace objects that existed in that space // are updated to remove it, and of those, any that don't exist in any space are deleted. const multiNamespaceResponse = await es.search({ @@ -138,16 +138,13 @@ export function deleteTestSuiteFactory(es: any, esArchiver: any, supertest: Supe body: { query: { terms: { type: ['sharedtype'] } } }, }); const docs: [Record] = multiNamespaceResponse.hits.hits; - expect(docs).length(6); // just six results, since spaces_2_only got deleted - Object.values(CASES).forEach(({ id, existingNamespaces }) => { - const remainingNamespaces = existingNamespaces.filter((x) => x !== 'space_2'); - const doc = docs.find((x) => x._id === `sharedtype:${id}`); - if (remainingNamespaces.length > 0) { - expect(doc?._source?.namespaces).to.eql(remainingNamespaces); - } else { - expect(doc).to.be(undefined); - } + expect(docs).length(10); // just ten results, since spaces_2_only got deleted + docs.forEach((doc) => () => { + const containsSpace2 = doc?._source?.namespaces.includes('space_2'); + expect(containsSpace2).to.eql(false); }); + const space2OnlyObjExists = docs.some((x) => x._id === CASES.SPACE_2_ONLY); + expect(space2OnlyObjExists).to.eql(false); }; const expectNotFound = (resp: { [key: string]: any }) => { diff --git a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts index f693719ba810d..e5a3830759db1 100644 --- a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts +++ b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts @@ -20,12 +20,19 @@ interface ResolveCopyToSpaceTest { response: (resp: TestResponse) => Promise; } +interface ResolveCopyToSpaceMultiNamespaceTest extends ResolveCopyToSpaceTest { + testTitle: string; + objects: Array>; + retries: Record; +} + interface ResolveCopyToSpaceTests { withReferencesNotOverwriting: ResolveCopyToSpaceTest; withReferencesOverwriting: ResolveCopyToSpaceTest; withoutReferencesOverwriting: ResolveCopyToSpaceTest; withoutReferencesNotOverwriting: ResolveCopyToSpaceTest; nonExistentSpace: ResolveCopyToSpaceTest; + multiNamespaceTestCases: (overwrite: boolean) => ResolveCopyToSpaceMultiNamespaceTest[]; } interface ResolveCopyToSpaceTestDefinition { @@ -262,6 +269,116 @@ export function resolveCopyToSpaceConflictsSuite( } }; + /** + * Creates test cases for multi-namespace saved object types. + * Note: these are written with the assumption that test data will only be reloaded between each group of test cases, *not* before every + * single test case. This saves time during test execution. + */ + const createMultiNamespaceTestCases = ( + spaceId: string, + outcome: 'authorized' | 'unauthorizedRead' | 'unauthorizedWrite' | 'noAccess' = 'authorized' + ) => (overwrite: boolean): ResolveCopyToSpaceMultiNamespaceTest[] => { + // the status code of the HTTP response differs depending on the error type + // a 403 error actually comes back as an HTTP 200 response + const statusCode = outcome === 'noAccess' ? 404 : 200; + const type = 'sharedtype'; + const v4 = new RegExp(/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i); + const exactMatchId = 'all_spaces'; + const inexactMatchId = `conflict_1_${spaceId}`; + const ambiguousConflictId = `conflict_2_${spaceId}`; + + const createRetries = ( + overwriteRetry: Record, + duplicateRetry: Record + ) => ({ space_2: [overwrite ? overwriteRetry : duplicateRetry] }); + const getResult = (response: TestResponse) => (response.body as CopyResponse).space_2; + const expectForbiddenResponse = (response: TestResponse) => { + expect(response.body).to.eql({ + space_2: { + success: false, + successCount: 0, + errors: [ + { statusCode: 403, error: 'Forbidden', message: `Unable to bulk_create sharedtype` }, + ], + }, + }); + }; + const expectSuccessResponse = (response: TestResponse, id: string, newId?: string) => { + const { success, successCount, successResults, errors } = getResult(response); + expect(success).to.eql(true); + expect(successCount).to.eql(1); + expect(errors).to.be(undefined); + if (overwrite) { + expect(successResults).to.eql([{ type, id, ...(newId && { newId }) }]); + } else { + // duplicate + const duplicateId = successResults![0].newId; + expect(duplicateId).to.match(v4); + expect(successResults).to.eql([{ type, id, newId: duplicateId }]); + } + }; + + return [ + { + testTitle: 'copying with an exact match conflict', + objects: [{ type, id: exactMatchId }], + retries: createRetries( + { type, id: exactMatchId, overwrite }, + { type, id: exactMatchId, duplicate: true } + ), + statusCode, + response: async (response: TestResponse) => { + if (outcome === 'authorized') { + expectSuccessResponse(response, exactMatchId); + } else if (outcome === 'noAccess') { + expectNotFoundResponse(response); + } else { + // unauthorized read/write + expectForbiddenResponse(response); + } + }, + }, + { + testTitle: 'copying with an inexact match conflict', + objects: [{ type, id: inexactMatchId }], + retries: createRetries( + { type, id: inexactMatchId, overwrite, idToOverwrite: 'conflict_1_space_2' }, + { type, id: inexactMatchId, duplicate: true } + ), + statusCode, + response: async (response: TestResponse) => { + if (outcome === 'authorized') { + expectSuccessResponse(response, inexactMatchId, 'conflict_1_space_2'); + } else if (outcome === 'noAccess') { + expectNotFoundResponse(response); + } else { + // unauthorized read/write + expectForbiddenResponse(response); + } + }, + }, + { + testTitle: 'copying with an ambiguous conflict', + objects: [{ type, id: ambiguousConflictId }], + retries: createRetries( + { type, id: ambiguousConflictId, overwrite, idToOverwrite: 'conflict_2_space_2' }, + { type, id: ambiguousConflictId, duplicate: true } + ), + statusCode, + response: async (response: TestResponse) => { + if (outcome === 'authorized') { + expectSuccessResponse(response, ambiguousConflictId, 'conflict_2_space_2'); + } else if (outcome === 'noAccess') { + expectNotFoundResponse(response); + } else { + // unauthorized read/write + expectForbiddenResponse(response); + } + }, + }, + ]; + }; + const makeResolveCopyToSpaceConflictsTest = (describeFn: DescribeFn) => ( description: string, { user = {}, spaceId = DEFAULT_SPACE_ID, tests }: ResolveCopyToSpaceTestDefinition @@ -354,6 +471,27 @@ export function resolveCopyToSpaceConflictsSuite( .then(tests.nonExistentSpace.response); }); }); + + [false, true].forEach((overwrite) => { + const includeReferences = false; + const retryType = overwrite ? 'overwrite' : 'duplicate'; + describe(`multi-namespace types with "${retryType}" retry`, () => { + before(() => esArchiver.load('saved_objects/spaces')); + after(() => esArchiver.unload('saved_objects/spaces')); + + const testCases = tests.multiNamespaceTestCases(overwrite); + testCases.forEach(({ testTitle, objects, retries, statusCode, response }) => { + it(`should return ${statusCode} when ${testTitle}`, async () => { + return supertestWithoutAuth + .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`) + .auth(user.username, user.password) + .send({ objects, includeReferences, retries }) + .expect(statusCode) + .then(response); + }); + }); + }); + }); }); }; @@ -371,6 +509,7 @@ export function resolveCopyToSpaceConflictsSuite( createExpectUnauthorizedAtSpaceWithReferencesResult, createExpectReadonlyAtSpaceWithReferencesResult, createExpectUnauthorizedAtSpaceWithoutReferencesResult, + createMultiNamespaceTestCases, originSpaces: ['default', 'space_1'], NON_EXISTENT_SPACE_ID, }; diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space.ts index 535c78fb8ea4c..2c4fc6d38d79d 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space.ts @@ -25,6 +25,7 @@ export default function copyToSpaceSpacesAndSecuritySuite({ getService }: TestIn createExpectUnauthorizedAtSpaceWithReferencesResult, createExpectUnauthorizedAtSpaceWithoutReferencesResult, expectNotFoundResponse, + createMultiNamespaceTestCases, } = copyToSpaceTestSuiteFactory(es, esArchiver, supertestWithoutAuth); describe('copy to spaces', () => { @@ -70,6 +71,7 @@ export default function copyToSpaceSpacesAndSecuritySuite({ getService }: TestIn noConflictsResponse: expectNotFoundResponse, }, nonExistentSpace: { statusCode: 404, response: expectNotFoundResponse }, + multiNamespaceTestCases: createMultiNamespaceTestCases(spaceId, 'noAccess'), }, }); // In *this* test suite, a user who is unauthorized to write (but authorized to read) in the destination space will get the same exact @@ -118,12 +120,18 @@ export default function copyToSpaceSpacesAndSecuritySuite({ getService }: TestIn const definitionUnauthorizedRead = (user: { username: string; password: string }) => ({ spaceId, user, - tests: commonUnauthorizedTests, + tests: { + ...commonUnauthorizedTests, + multiNamespaceTestCases: createMultiNamespaceTestCases(spaceId, 'unauthorizedRead'), + }, }); const definitionUnauthorizedWrite = (user: { username: string; password: string }) => ({ spaceId, user, - tests: commonUnauthorizedTests, + tests: { + ...commonUnauthorizedTests, + multiNamespaceTestCases: createMultiNamespaceTestCases(spaceId, 'unauthorizedWrite'), + }, }); const definitionAuthorized = (user: { username: string; password: string }) => ({ spaceId, @@ -154,6 +162,7 @@ export default function copyToSpaceSpacesAndSecuritySuite({ getService }: TestIn statusCode: 200, response: expectNoConflictsForNonExistentSpaceResult, }, + multiNamespaceTestCases: createMultiNamespaceTestCases(spaceId, 'authorized'), }, }); diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/resolve_copy_to_space_conflicts.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/resolve_copy_to_space_conflicts.ts index 1f05b9439e3a2..b81f2965eba22 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/resolve_copy_to_space_conflicts.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/resolve_copy_to_space_conflicts.ts @@ -25,6 +25,7 @@ export default function resolveCopyToSpaceConflictsTestSuite({ getService }: Tes createExpectUnauthorizedAtSpaceWithReferencesResult, createExpectReadonlyAtSpaceWithReferencesResult, createExpectUnauthorizedAtSpaceWithoutReferencesResult, + createMultiNamespaceTestCases, NON_EXISTENT_SPACE_ID, } = resolveCopyToSpaceConflictsSuite(esArchiver, supertestWithAuth, supertestWithoutAuth); @@ -81,6 +82,7 @@ export default function resolveCopyToSpaceConflictsTestSuite({ getService }: Tes statusCode: 404, response: expectNotFoundResponse, }, + multiNamespaceTestCases: createMultiNamespaceTestCases(spaceId, 'noAccess'), }, }); const definitionUnauthorizedRead = (user: { username: string; password: string }) => ({ @@ -110,6 +112,7 @@ export default function resolveCopyToSpaceConflictsTestSuite({ getService }: Tes NON_EXISTENT_SPACE_ID ), }, + multiNamespaceTestCases: createMultiNamespaceTestCases(spaceId, 'unauthorizedRead'), }, }); const definitionUnauthorizedWrite = (user: { username: string; password: string }) => ({ @@ -139,6 +142,7 @@ export default function resolveCopyToSpaceConflictsTestSuite({ getService }: Tes NON_EXISTENT_SPACE_ID ), }, + multiNamespaceTestCases: createMultiNamespaceTestCases(spaceId, 'unauthorizedWrite'), }, }); const definitionAuthorized = (user: { username: string; password: string }) => ({ @@ -168,6 +172,7 @@ export default function resolveCopyToSpaceConflictsTestSuite({ getService }: Tes NON_EXISTENT_SPACE_ID ), }, + multiNamespaceTestCases: createMultiNamespaceTestCases(spaceId, 'authorized'), }, }); diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_add.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_add.ts index f3e6580e439bb..ddd029c8d7d68 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_add.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_add.ts @@ -25,7 +25,7 @@ const createTestCases = (spaceId: string) => { const namespaces = [spaceId]; return [ // Test cases to check adding the target namespace to different saved objects - { ...CASES.DEFAULT_SPACE_ONLY, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.DEFAULT_ONLY, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, { ...CASES.SPACE_1_ONLY, namespaces, ...fail404(spaceId !== SPACE_1_ID) }, { ...CASES.SPACE_2_ONLY, namespaces, ...fail404(spaceId !== SPACE_2_ID) }, { ...CASES.DEFAULT_AND_SPACE_1, namespaces, ...fail404(spaceId === SPACE_2_ID) }, @@ -37,7 +37,7 @@ const createTestCases = (spaceId: string) => { // These are non-exhaustive, they only check cases for adding two additional namespaces to a saved object // More permutations are covered in the corresponding spaces_only test suite { - ...CASES.DEFAULT_SPACE_ONLY, + ...CASES.DEFAULT_ONLY, namespaces: [SPACE_1_ID, SPACE_2_ID], ...fail404(spaceId !== DEFAULT_SPACE_ID), }, diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_remove.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_remove.ts index d83020a9598f1..4b120a71213b7 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_remove.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_remove.ts @@ -29,7 +29,7 @@ const createTestCases = (spaceId: string) => { // Test cases to check removing the target namespace from different saved objects let namespaces = [spaceId]; const singleSpace = [ - { id: CASES.DEFAULT_SPACE_ONLY.id, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { id: CASES.DEFAULT_ONLY.id, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, { id: CASES.SPACE_1_ONLY.id, namespaces, ...fail404(spaceId !== SPACE_1_ID) }, { id: CASES.SPACE_2_ONLY.id, namespaces, ...fail404(spaceId !== SPACE_2_ID) }, { id: CASES.DEFAULT_AND_SPACE_1.id, namespaces, ...fail404(spaceId === SPACE_2_ID) }, diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/copy_to_space.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/copy_to_space.ts index 1de69d8b73aa8..85efe797c7402 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/copy_to_space.ts @@ -20,6 +20,7 @@ export default function copyToSpacesOnlySuite({ getService }: FtrProviderContext expectNoConflictsForNonExistentSpaceResult, createExpectWithConflictsOverwritingResult, createExpectWithConflictsWithoutOverwritingResult, + createMultiNamespaceTestCases, originSpaces, } = copyToSpaceTestSuiteFactory(es, esArchiver, supertestWithoutAuth); @@ -53,6 +54,7 @@ export default function copyToSpacesOnlySuite({ getService }: FtrProviderContext statusCode: 200, response: expectNoConflictsForNonExistentSpaceResult, }, + multiNamespaceTestCases: createMultiNamespaceTestCases(spaceId), }, }); }); diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/resolve_copy_to_space_conflicts.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/resolve_copy_to_space_conflicts.ts index ef2735de3d3db..5c84475d32850 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/resolve_copy_to_space_conflicts.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/resolve_copy_to_space_conflicts.ts @@ -19,6 +19,7 @@ export default function resolveCopyToSpaceConflictsTestSuite({ getService }: Ftr createExpectNonOverriddenResponseWithoutReferences, createExpectOverriddenResponseWithReferences, createExpectOverriddenResponseWithoutReferences, + createMultiNamespaceTestCases, NON_EXISTENT_SPACE_ID, originSpaces, } = resolveCopyToSpaceConflictsSuite(esArchiver, supertestWithAuth, supertestWithoutAuth); @@ -51,6 +52,7 @@ export default function resolveCopyToSpaceConflictsTestSuite({ getService }: Ftr NON_EXISTENT_SPACE_ID ), }, + multiNamespaceTestCases: createMultiNamespaceTestCases(spaceId), }, }); }); diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/share_add.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/share_add.ts index 5cdebf9edfcfd..25ba986a12fd8 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/share_add.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/share_add.ts @@ -27,7 +27,7 @@ const { fail404 } = testCaseFailures; const createSingleTestCases = (spaceId: string) => { const namespaces = ['some-space-id']; return [ - { ...CASES.DEFAULT_SPACE_ONLY, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.DEFAULT_ONLY, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, { ...CASES.SPACE_1_ONLY, namespaces, ...fail404(spaceId !== SPACE_1_ID) }, { ...CASES.SPACE_2_ONLY, namespaces, ...fail404(spaceId !== SPACE_2_ID) }, { ...CASES.DEFAULT_AND_SPACE_1, namespaces, ...fail404(spaceId === SPACE_2_ID) }, @@ -43,7 +43,7 @@ const createSingleTestCases = (spaceId: string) => { */ const createMultiTestCases = () => { const allSpaces = [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID]; - let id = CASES.DEFAULT_SPACE_ONLY.id; + let id = CASES.DEFAULT_ONLY.id; const one = [{ id, namespaces: allSpaces }]; id = CASES.DEFAULT_AND_SPACE_1.id; const two = [{ id, namespaces: allSpaces }]; diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/share_remove.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/share_remove.ts index 8bcd294b38f3f..2c4506b723533 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/share_remove.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/share_remove.ts @@ -27,7 +27,7 @@ const { fail404 } = testCaseFailures; const createSingleTestCases = (spaceId: string) => { const namespaces = [spaceId]; return [ - { ...CASES.DEFAULT_SPACE_ONLY, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.DEFAULT_ONLY, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, { ...CASES.SPACE_1_ONLY, namespaces, ...fail404(spaceId !== SPACE_1_ID) }, { ...CASES.SPACE_2_ONLY, namespaces, ...fail404(spaceId !== SPACE_2_ID) }, { ...CASES.DEFAULT_AND_SPACE_1, namespaces, ...fail404(spaceId === SPACE_2_ID) }, @@ -43,7 +43,7 @@ const createSingleTestCases = (spaceId: string) => { */ const createMultiTestCases = () => { const nonExistentSpaceId = 'does_not_exist'; // space that doesn't exist - let id = CASES.DEFAULT_SPACE_ONLY.id; + let id = CASES.DEFAULT_ONLY.id; const one = [ { id, namespaces: [nonExistentSpaceId] }, { id, namespaces: [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID] }, From 8ab0a11c56645ffb69b15df69a16bdbf7641f427 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 12 May 2020 10:13:38 -0400 Subject: [PATCH 12/55] API docs changes --- .../core/public/kibana-plugin-core-public.md | 2 + ...na-plugin-core-public.savedobject.error.md | 2 + .../kibana-plugin-core-public.savedobject.md | 3 +- ...plugin-core-public.savedobject.originid.md | 13 ++++ ...gin-core-public.savedobjectsfindoptions.md | 1 + ...savedobjectsfindoptions.rawsearchfields.md | 13 ++++ ...portambiguousconflicterror.destinations.md | 15 +++++ ...avedobjectsimportambiguousconflicterror.md | 22 +++++++ ...ctsimportambiguousconflicterror.sources.md | 15 +++++ ...bjectsimportambiguousconflicterror.type.md | 11 ++++ ...re-public.savedobjectsimporterror.error.md | 2 +- ...gin-core-public.savedobjectsimporterror.md | 2 +- ...-core-public.savedobjectsimportresponse.md | 1 + ...vedobjectsimportresponse.successresults.md | 11 ++++ ...ublic.savedobjectsimportretry.duplicate.md | 13 ++++ ...c.savedobjectsimportretry.idtooverwrite.md | 13 ++++ ...gin-core-public.savedobjectsimportretry.md | 4 +- ...ublic.savedobjectsimportretry.overwrite.md | 2 + ...ore-public.savedobjectsimportsuccess.id.md | 11 ++++ ...n-core-public.savedobjectsimportsuccess.md | 22 +++++++ ...-public.savedobjectsimportsuccess.newid.md | 13 ++++ ...e-public.savedobjectsimportsuccess.type.md | 11 ++++ ...ore-server.importsavedobjectsfromstream.md | 4 +- .../core/server/kibana-plugin-core-server.md | 6 +- ...-server.resolvesavedobjectsimporterrors.md | 4 +- ...na-plugin-core-server.savedobject.error.md | 2 + .../kibana-plugin-core-server.savedobject.md | 3 +- ...plugin-core-server.savedobject.originid.md | 13 ++++ ...ore-server.savedobjectsbulkcreateobject.md | 1 + ...r.savedobjectsbulkcreateobject.originid.md | 13 ++++ ...n-core-server.savedobjectscreateoptions.md | 1 + ...rver.savedobjectscreateoptions.originid.md | 13 ++++ ...gin-core-server.savedobjectsfindoptions.md | 1 + ...savedobjectsfindoptions.rawsearchfields.md | 13 ++++ ...portambiguousconflicterror.destinations.md | 15 +++++ ...avedobjectsimportambiguousconflicterror.md | 22 +++++++ ...ctsimportambiguousconflicterror.sources.md | 15 +++++ ...bjectsimportambiguousconflicterror.type.md | 11 ++++ ...re-server.savedobjectsimporterror.error.md | 2 +- ...gin-core-server.savedobjectsimporterror.md | 2 +- ...n-core-server.savedobjectsimportoptions.md | 2 +- ...avedobjectsimportoptions.supportedtypes.md | 13 ---- ....savedobjectsimportoptions.typeregistry.md | 13 ++++ ...-core-server.savedobjectsimportresponse.md | 1 + ...vedobjectsimportresponse.successresults.md | 11 ++++ ...erver.savedobjectsimportretry.duplicate.md | 13 ++++ ...r.savedobjectsimportretry.idtooverwrite.md | 13 ++++ ...gin-core-server.savedobjectsimportretry.md | 4 +- ...erver.savedobjectsimportretry.overwrite.md | 2 + ...ore-server.savedobjectsimportsuccess.id.md | 11 ++++ ...n-core-server.savedobjectsimportsuccess.md | 22 +++++++ ...-server.savedobjectsimportsuccess.newid.md | 13 ++++ ...e-server.savedobjectsimportsuccess.type.md | 11 ++++ ...core-server.savedobjectsrepository.find.md | 4 +- ...savedobjectsrepository.incrementcounter.md | 18 +----- ...ugin-core-server.savedobjectsrepository.md | 2 +- ....savedobjectsresolveimporterrorsoptions.md | 2 +- ...esolveimporterrorsoptions.typeregistry.md} | 8 +-- src/core/public/public.api.md | 38 +++++++++++- src/core/server/server.api.md | 59 ++++++++++++++----- 60 files changed, 534 insertions(+), 69 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobject.originid.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.rawsearchfields.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.destinations.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.sources.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.type.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.successresults.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.duplicate.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.idtooverwrite.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.id.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.newid.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.type.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobject.originid.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.originid.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.originid.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.rawsearchfields.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.destinations.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.sources.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.type.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.supportedtypes.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.typeregistry.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.successresults.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.duplicate.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.idtooverwrite.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.id.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.newid.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.type.md rename docs/development/core/server/{kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.supportedtypes.md => kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.typeregistry.md} (54%) diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index b2524ec48c757..6de257b4a923e 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -113,11 +113,13 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsCreateOptions](./kibana-plugin-core-public.savedobjectscreateoptions.md) | | | [SavedObjectsFindOptions](./kibana-plugin-core-public.savedobjectsfindoptions.md) | | | [SavedObjectsFindResponsePublic](./kibana-plugin-core-public.savedobjectsfindresponsepublic.md) | Return type of the Saved Objects find() method.\*Note\*: this type is different between the Public and Server Saved Objects clients. | +| [SavedObjectsImportAmbiguousConflictError](./kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.md) | Represents a failure to import due to a conflict, which can be resolved in different ways with an overwrite. | | [SavedObjectsImportConflictError](./kibana-plugin-core-public.savedobjectsimportconflicterror.md) | Represents a failure to import due to a conflict. | | [SavedObjectsImportError](./kibana-plugin-core-public.savedobjectsimporterror.md) | Represents a failure to import. | | [SavedObjectsImportMissingReferencesError](./kibana-plugin-core-public.savedobjectsimportmissingreferenceserror.md) | Represents a failure to import due to missing references. | | [SavedObjectsImportResponse](./kibana-plugin-core-public.savedobjectsimportresponse.md) | The response describing the result of an import. | | [SavedObjectsImportRetry](./kibana-plugin-core-public.savedobjectsimportretry.md) | Describes a retry operation for importing a saved object. | +| [SavedObjectsImportSuccess](./kibana-plugin-core-public.savedobjectsimportsuccess.md) | Represents a successful import. | | [SavedObjectsImportUnknownError](./kibana-plugin-core-public.savedobjectsimportunknownerror.md) | Represents a failure to import due to an unknown reason. | | [SavedObjectsImportUnsupportedTypeError](./kibana-plugin-core-public.savedobjectsimportunsupportedtypeerror.md) | Represents a failure to import due to having an unsupported saved object type. | | [SavedObjectsMigrationVersion](./kibana-plugin-core-public.savedobjectsmigrationversion.md) | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobject.error.md b/docs/development/core/public/kibana-plugin-core-public.savedobject.error.md index f6ffa49c2e6b2..3c089cf8c7c91 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobject.error.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobject.error.md @@ -8,7 +8,9 @@ ```typescript error?: { + error: string; message: string; statusCode: number; + metadata?: Record; }; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobject.md b/docs/development/core/public/kibana-plugin-core-public.savedobject.md index b67d0536fb336..0dda7fa11637d 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobject.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobject.md @@ -15,10 +15,11 @@ export interface SavedObject | Property | Type | Description | | --- | --- | --- | | [attributes](./kibana-plugin-core-public.savedobject.attributes.md) | T | The data for a Saved Object is stored as an object in the attributes property. | -| [error](./kibana-plugin-core-public.savedobject.error.md) | {
message: string;
statusCode: number;
} | | +| [error](./kibana-plugin-core-public.savedobject.error.md) | {
error: string;
message: string;
statusCode: number;
metadata?: Record<string, unknown>;
} | | | [id](./kibana-plugin-core-public.savedobject.id.md) | string | The ID of this Saved Object, guaranteed to be unique for all objects of the same type | | [migrationVersion](./kibana-plugin-core-public.savedobject.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | | [namespaces](./kibana-plugin-core-public.savedobject.namespaces.md) | string[] | Namespace(s) that this saved object exists in. This attribute is only used for multi-namespace saved object types. | +| [originId](./kibana-plugin-core-public.savedobject.originid.md) | string | The ID of the saved object this originated from. This is set if this object's id was regenerated; that can happen during migration from a legacy single-namespace type, or during import. It is only set during migration or create operations. This is used during import to ensure that ID regeneration is deterministic, so saved objects will be overwritten if they are imported multiple times into a given space. | | [references](./kibana-plugin-core-public.savedobject.references.md) | SavedObjectReference[] | A reference to another saved object. | | [type](./kibana-plugin-core-public.savedobject.type.md) | string | The type of Saved Object. Each plugin can define it's own custom Saved Object types. | | [updated\_at](./kibana-plugin-core-public.savedobject.updated_at.md) | string | Timestamp of the last time this document had been updated. | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobject.originid.md b/docs/development/core/public/kibana-plugin-core-public.savedobject.originid.md new file mode 100644 index 0000000000000..f5bab09b9bcc0 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobject.originid.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObject](./kibana-plugin-core-public.savedobject.md) > [originId](./kibana-plugin-core-public.savedobject.originid.md) + +## SavedObject.originId property + +The ID of the saved object this originated from. This is set if this object's `id` was regenerated; that can happen during migration from a legacy single-namespace type, or during import. It is only set during migration or create operations. This is used during import to ensure that ID regeneration is deterministic, so saved objects will be overwritten if they are imported multiple times into a given space. + +Signature: + +```typescript +originId?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md index 366e82f2ef07b..4481e09fdee7f 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md @@ -21,6 +21,7 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions | [hasReference](./kibana-plugin-core-public.savedobjectsfindoptions.hasreference.md) | {
type: string;
id: string;
} | | | [page](./kibana-plugin-core-public.savedobjectsfindoptions.page.md) | number | | | [perPage](./kibana-plugin-core-public.savedobjectsfindoptions.perpage.md) | number | | +| [rawSearchFields](./kibana-plugin-core-public.savedobjectsfindoptions.rawsearchfields.md) | string[] | The fields to perform the parsed query against. Unlike the searchFields argument, these are expected to be raw and will not be modified. If used in conjunction with searchFields, both are concatenated together. | | [search](./kibana-plugin-core-public.savedobjectsfindoptions.search.md) | string | Search documents using the Elasticsearch Simple Query String syntax. See Elasticsearch Simple Query String query argument for more information | | [searchFields](./kibana-plugin-core-public.savedobjectsfindoptions.searchfields.md) | string[] | The fields to perform the parsed query against. See Elasticsearch Simple Query String fields argument for more information | | [sortField](./kibana-plugin-core-public.savedobjectsfindoptions.sortfield.md) | string | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.rawsearchfields.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.rawsearchfields.md new file mode 100644 index 0000000000000..b5861402a08ce --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.rawsearchfields.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsFindOptions](./kibana-plugin-core-public.savedobjectsfindoptions.md) > [rawSearchFields](./kibana-plugin-core-public.savedobjectsfindoptions.rawsearchfields.md) + +## SavedObjectsFindOptions.rawSearchFields property + +The fields to perform the parsed query against. Unlike the `searchFields` argument, these are expected to be raw and will not be modified. If used in conjunction with `searchFields`, both are concatenated together. + +Signature: + +```typescript +rawSearchFields?: string[]; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.destinations.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.destinations.md new file mode 100644 index 0000000000000..59ce43c4bea62 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.destinations.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportAmbiguousConflictError](./kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.md) > [destinations](./kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.destinations.md) + +## SavedObjectsImportAmbiguousConflictError.destinations property + +Signature: + +```typescript +destinations: Array<{ + id: string; + title?: string; + updatedAt?: string; + }>; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.md new file mode 100644 index 0000000000000..e0ecf7ae3fc58 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportAmbiguousConflictError](./kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.md) + +## SavedObjectsImportAmbiguousConflictError interface + +Represents a failure to import due to a conflict, which can be resolved in different ways with an overwrite. + +Signature: + +```typescript +export interface SavedObjectsImportAmbiguousConflictError +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [destinations](./kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.destinations.md) | Array<{
id: string;
title?: string;
updatedAt?: string;
}> | | +| [sources](./kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.sources.md) | Array<{
id: string;
title?: string;
updatedAt?: string;
}> | | +| [type](./kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.type.md) | 'ambiguous_conflict' | | + diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.sources.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.sources.md new file mode 100644 index 0000000000000..f0d1dbd9c3efb --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.sources.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportAmbiguousConflictError](./kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.md) > [sources](./kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.sources.md) + +## SavedObjectsImportAmbiguousConflictError.sources property + +Signature: + +```typescript +sources: Array<{ + id: string; + title?: string; + updatedAt?: string; + }>; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.type.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.type.md new file mode 100644 index 0000000000000..600c56988ac75 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportAmbiguousConflictError](./kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.md) > [type](./kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.type.md) + +## SavedObjectsImportAmbiguousConflictError.type property + +Signature: + +```typescript +type: 'ambiguous_conflict'; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.error.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.error.md index a76ab8e5c926a..201f56bf925d1 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.error.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.error.md @@ -7,5 +7,5 @@ Signature: ```typescript -error: SavedObjectsImportConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError; +error: SavedObjectsImportConflictError | SavedObjectsImportAmbiguousConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.md index 5703c613adbd7..8e7f9d6ef347e 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.md @@ -16,7 +16,7 @@ export interface SavedObjectsImportError | Property | Type | Description | | --- | --- | --- | -| [error](./kibana-plugin-core-public.savedobjectsimporterror.error.md) | SavedObjectsImportConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError | | +| [error](./kibana-plugin-core-public.savedobjectsimporterror.error.md) | SavedObjectsImportConflictError | SavedObjectsImportAmbiguousConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError | | | [id](./kibana-plugin-core-public.savedobjectsimporterror.id.md) | string | | | [title](./kibana-plugin-core-public.savedobjectsimporterror.title.md) | string | | | [type](./kibana-plugin-core-public.savedobjectsimporterror.type.md) | string | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.md index 910de33c30e62..0aba4d517e43a 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.md @@ -19,4 +19,5 @@ export interface SavedObjectsImportResponse | [errors](./kibana-plugin-core-public.savedobjectsimportresponse.errors.md) | SavedObjectsImportError[] | | | [success](./kibana-plugin-core-public.savedobjectsimportresponse.success.md) | boolean | | | [successCount](./kibana-plugin-core-public.savedobjectsimportresponse.successcount.md) | number | | +| [successResults](./kibana-plugin-core-public.savedobjectsimportresponse.successresults.md) | SavedObjectsImportSuccess[] | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.successresults.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.successresults.md new file mode 100644 index 0000000000000..51a47b6c2d953 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.successresults.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportResponse](./kibana-plugin-core-public.savedobjectsimportresponse.md) > [successResults](./kibana-plugin-core-public.savedobjectsimportresponse.successresults.md) + +## SavedObjectsImportResponse.successResults property + +Signature: + +```typescript +successResults?: SavedObjectsImportSuccess[]; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.duplicate.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.duplicate.md new file mode 100644 index 0000000000000..44f3682a8aab2 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.duplicate.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportRetry](./kibana-plugin-core-public.savedobjectsimportretry.md) > [duplicate](./kibana-plugin-core-public.savedobjectsimportretry.duplicate.md) + +## SavedObjectsImportRetry.duplicate property + +Resolve an import conflict by creating a duplicate object with a new (undefined) originId. Note: this attribute is mutually-exclusive with `overwrite`. If both are enabled, `overwrite` takes precedence. + +Signature: + +```typescript +duplicate?: boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.idtooverwrite.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.idtooverwrite.md new file mode 100644 index 0000000000000..6c240852908a1 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.idtooverwrite.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportRetry](./kibana-plugin-core-public.savedobjectsimportretry.md) > [idToOverwrite](./kibana-plugin-core-public.savedobjectsimportretry.idtooverwrite.md) + +## SavedObjectsImportRetry.idToOverwrite property + +The object ID that will be overwritten. Only used if `overwrite` == true. This is required to resolve ambiguous conflicts. + +Signature: + +```typescript +idToOverwrite?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.md index d625302d97eed..6c083438ccd7f 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.md @@ -16,8 +16,10 @@ export interface SavedObjectsImportRetry | Property | Type | Description | | --- | --- | --- | +| [duplicate](./kibana-plugin-core-public.savedobjectsimportretry.duplicate.md) | boolean | Resolve an import conflict by creating a duplicate object with a new (undefined) originId. Note: this attribute is mutually-exclusive with overwrite. If both are enabled, overwrite takes precedence. | | [id](./kibana-plugin-core-public.savedobjectsimportretry.id.md) | string | | -| [overwrite](./kibana-plugin-core-public.savedobjectsimportretry.overwrite.md) | boolean | | +| [idToOverwrite](./kibana-plugin-core-public.savedobjectsimportretry.idtooverwrite.md) | string | The object ID that will be overwritten. Only used if overwrite == true. This is required to resolve ambiguous conflicts. | +| [overwrite](./kibana-plugin-core-public.savedobjectsimportretry.overwrite.md) | boolean | Resolve an import conflict by overwriting a destination object. Note: this attribute is mutually-exclusive with duplicate. If both are enabled, overwrite takes precedence. | | [replaceReferences](./kibana-plugin-core-public.savedobjectsimportretry.replacereferences.md) | Array<{
type: string;
from: string;
to: string;
}> | | | [type](./kibana-plugin-core-public.savedobjectsimportretry.type.md) | string | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.overwrite.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.overwrite.md index 51ea151a9cdb3..2c389faabf812 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.overwrite.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.overwrite.md @@ -4,6 +4,8 @@ ## SavedObjectsImportRetry.overwrite property +Resolve an import conflict by overwriting a destination object. Note: this attribute is mutually-exclusive with `duplicate`. If both are enabled, `overwrite` takes precedence. + Signature: ```typescript diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.id.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.id.md new file mode 100644 index 0000000000000..6d6271e37dffe --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportSuccess](./kibana-plugin-core-public.savedobjectsimportsuccess.md) > [id](./kibana-plugin-core-public.savedobjectsimportsuccess.id.md) + +## SavedObjectsImportSuccess.id property + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.md new file mode 100644 index 0000000000000..f7e697d773de4 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportSuccess](./kibana-plugin-core-public.savedobjectsimportsuccess.md) + +## SavedObjectsImportSuccess interface + +Represents a successful import. + +Signature: + +```typescript +export interface SavedObjectsImportSuccess +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [id](./kibana-plugin-core-public.savedobjectsimportsuccess.id.md) | string | | +| [newId](./kibana-plugin-core-public.savedobjectsimportsuccess.newid.md) | string | If newId is specified, the new object has a new ID that is different from the import ID. | +| [type](./kibana-plugin-core-public.savedobjectsimportsuccess.type.md) | string | | + diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.newid.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.newid.md new file mode 100644 index 0000000000000..e8d301dcdef06 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.newid.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportSuccess](./kibana-plugin-core-public.savedobjectsimportsuccess.md) > [newId](./kibana-plugin-core-public.savedobjectsimportsuccess.newid.md) + +## SavedObjectsImportSuccess.newId property + +If `newId` is specified, the new object has a new ID that is different from the import ID. + +Signature: + +```typescript +newId?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.type.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.type.md new file mode 100644 index 0000000000000..6ac14455d281f --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportSuccess](./kibana-plugin-core-public.savedobjectsimportsuccess.md) > [type](./kibana-plugin-core-public.savedobjectsimportsuccess.type.md) + +## SavedObjectsImportSuccess.type property + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.importsavedobjectsfromstream.md b/docs/development/core/server/kibana-plugin-core-server.importsavedobjectsfromstream.md index 6fabfb7a321ae..df79aff35ea78 100644 --- a/docs/development/core/server/kibana-plugin-core-server.importsavedobjectsfromstream.md +++ b/docs/development/core/server/kibana-plugin-core-server.importsavedobjectsfromstream.md @@ -9,14 +9,14 @@ Import saved objects from given stream. See the [options](./kibana-plugin-core-s Signature: ```typescript -export declare function importSavedObjectsFromStream({ readStream, objectLimit, overwrite, savedObjectsClient, supportedTypes, namespace, }: SavedObjectsImportOptions): Promise; +export declare function importSavedObjectsFromStream({ readStream, objectLimit, overwrite, savedObjectsClient, typeRegistry, namespace, }: SavedObjectsImportOptions): Promise; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| { readStream, objectLimit, overwrite, savedObjectsClient, supportedTypes, namespace, } | SavedObjectsImportOptions | | +| { readStream, objectLimit, overwrite, savedObjectsClient, typeRegistry, namespace, } | SavedObjectsImportOptions | | Returns: diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 14e01fda3d287..fb5f17df953d5 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -45,10 +45,10 @@ The plugin integrates with the core system via lifecycle events: `setup` | [deepFreeze(object)](./kibana-plugin-core-server.deepfreeze.md) | Apply Object.freeze to a value recursively and convert the return type to Readonly variant recursively | | [exportSavedObjectsToStream({ types, objects, search, savedObjectsClient, exportSizeLimit, includeReferencesDeep, excludeExportDetails, namespace, })](./kibana-plugin-core-server.exportsavedobjectstostream.md) | Generates sorted saved object stream to be used for export. See the [options](./kibana-plugin-core-server.savedobjectsexportoptions.md) for more detailed information. | | [getFlattenedObject(rootValue)](./kibana-plugin-core-server.getflattenedobject.md) | Flattens a deeply nested object to a map of dot-separated paths pointing to all primitive values \*\*and arrays\*\* from rootValue.example: getFlattenedObject({ a: { b: 1, c: \[2,3\] } }) // => { 'a.b': 1, 'a.c': \[2,3\] } | -| [importSavedObjectsFromStream({ readStream, objectLimit, overwrite, savedObjectsClient, supportedTypes, namespace, })](./kibana-plugin-core-server.importsavedobjectsfromstream.md) | Import saved objects from given stream. See the [options](./kibana-plugin-core-server.savedobjectsimportoptions.md) for more detailed information. | +| [importSavedObjectsFromStream({ readStream, objectLimit, overwrite, savedObjectsClient, typeRegistry, namespace, })](./kibana-plugin-core-server.importsavedobjectsfromstream.md) | Import saved objects from given stream. See the [options](./kibana-plugin-core-server.savedobjectsimportoptions.md) for more detailed information. | | [isRelativeUrl(candidatePath)](./kibana-plugin-core-server.isrelativeurl.md) | Determine if a url is relative. Any url including a protocol, hostname, or port is not considered relative. This means that absolute \*paths\* are considered to be relative \*urls\* | | [modifyUrl(url, urlModifier)](./kibana-plugin-core-server.modifyurl.md) | Takes a URL and a function that takes the meaningful parts of the URL as a key-value object, modifies some or all of the parts, and returns the modified parts formatted again as a url.Url Parts sent: - protocol - slashes (does the url have the //) - auth - hostname (just the name of the host, no port or auth information) - port - pathname (the path after the hostname, no query or hash, starts with a slash if there was a path) - query (always an object, even when no query on original url) - hashWhy? - The default url library in node produces several conflicting properties on the "parsed" output. Modifying any of these might lead to the modifications being ignored (depending on which property was modified) - It's not always clear whether to use path/pathname, host/hostname, so this tries to add helpful constraints | -| [resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, supportedTypes, namespace, })](./kibana-plugin-core-server.resolvesavedobjectsimporterrors.md) | Resolve and return saved object import errors. See the [options](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md) for more detailed informations. | +| [resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, typeRegistry, namespace, })](./kibana-plugin-core-server.resolvesavedobjectsimporterrors.md) | Resolve and return saved object import errors. See the [options](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md) for more detailed informations. | ## Interfaces @@ -157,12 +157,14 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsExportResultDetails](./kibana-plugin-core-server.savedobjectsexportresultdetails.md) | Structure of the export result details entry | | [SavedObjectsFindOptions](./kibana-plugin-core-server.savedobjectsfindoptions.md) | | | [SavedObjectsFindResponse](./kibana-plugin-core-server.savedobjectsfindresponse.md) | Return type of the Saved Objects find() method.\*Note\*: this type is different between the Public and Server Saved Objects clients. | +| [SavedObjectsImportAmbiguousConflictError](./kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.md) | Represents a failure to import due to a conflict, which can be resolved in different ways with an overwrite. | | [SavedObjectsImportConflictError](./kibana-plugin-core-server.savedobjectsimportconflicterror.md) | Represents a failure to import due to a conflict. | | [SavedObjectsImportError](./kibana-plugin-core-server.savedobjectsimporterror.md) | Represents a failure to import. | | [SavedObjectsImportMissingReferencesError](./kibana-plugin-core-server.savedobjectsimportmissingreferenceserror.md) | Represents a failure to import due to missing references. | | [SavedObjectsImportOptions](./kibana-plugin-core-server.savedobjectsimportoptions.md) | Options to control the import operation. | | [SavedObjectsImportResponse](./kibana-plugin-core-server.savedobjectsimportresponse.md) | The response describing the result of an import. | | [SavedObjectsImportRetry](./kibana-plugin-core-server.savedobjectsimportretry.md) | Describes a retry operation for importing a saved object. | +| [SavedObjectsImportSuccess](./kibana-plugin-core-server.savedobjectsimportsuccess.md) | Represents a successful import. | | [SavedObjectsImportUnknownError](./kibana-plugin-core-server.savedobjectsimportunknownerror.md) | Represents a failure to import due to an unknown reason. | | [SavedObjectsImportUnsupportedTypeError](./kibana-plugin-core-server.savedobjectsimportunsupportedtypeerror.md) | Represents a failure to import due to having an unsupported saved object type. | | [SavedObjectsIncrementCounterOptions](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.md) | | diff --git a/docs/development/core/server/kibana-plugin-core-server.resolvesavedobjectsimporterrors.md b/docs/development/core/server/kibana-plugin-core-server.resolvesavedobjectsimporterrors.md index c7f30b0533d04..6ee273fff7059 100644 --- a/docs/development/core/server/kibana-plugin-core-server.resolvesavedobjectsimporterrors.md +++ b/docs/development/core/server/kibana-plugin-core-server.resolvesavedobjectsimporterrors.md @@ -9,14 +9,14 @@ Resolve and return saved object import errors. See the [options](./kibana-plugin Signature: ```typescript -export declare function resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, supportedTypes, namespace, }: SavedObjectsResolveImportErrorsOptions): Promise; +export declare function resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, typeRegistry, namespace, }: SavedObjectsResolveImportErrorsOptions): Promise; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| { readStream, objectLimit, retries, savedObjectsClient, supportedTypes, namespace, } | SavedObjectsResolveImportErrorsOptions | | +| { readStream, objectLimit, retries, savedObjectsClient, typeRegistry, namespace, } | SavedObjectsResolveImportErrorsOptions | | Returns: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobject.error.md b/docs/development/core/server/kibana-plugin-core-server.savedobject.error.md index dffef4392c85c..2182b2970a3eb 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobject.error.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobject.error.md @@ -8,7 +8,9 @@ ```typescript error?: { + error: string; message: string; statusCode: number; + metadata?: Record; }; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobject.md index 94d1c378899df..904b667210144 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobject.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobject.md @@ -15,10 +15,11 @@ export interface SavedObject | Property | Type | Description | | --- | --- | --- | | [attributes](./kibana-plugin-core-server.savedobject.attributes.md) | T | The data for a Saved Object is stored as an object in the attributes property. | -| [error](./kibana-plugin-core-server.savedobject.error.md) | {
message: string;
statusCode: number;
} | | +| [error](./kibana-plugin-core-server.savedobject.error.md) | {
error: string;
message: string;
statusCode: number;
metadata?: Record<string, unknown>;
} | | | [id](./kibana-plugin-core-server.savedobject.id.md) | string | The ID of this Saved Object, guaranteed to be unique for all objects of the same type | | [migrationVersion](./kibana-plugin-core-server.savedobject.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | | [namespaces](./kibana-plugin-core-server.savedobject.namespaces.md) | string[] | Namespace(s) that this saved object exists in. This attribute is only used for multi-namespace saved object types. | +| [originId](./kibana-plugin-core-server.savedobject.originid.md) | string | The ID of the saved object this originated from. This is set if this object's id was regenerated; that can happen during migration from a legacy single-namespace type, or during import. It is only set during migration or create operations. This is used during import to ensure that ID regeneration is deterministic, so saved objects will be overwritten if they are imported multiple times into a given space. | | [references](./kibana-plugin-core-server.savedobject.references.md) | SavedObjectReference[] | A reference to another saved object. | | [type](./kibana-plugin-core-server.savedobject.type.md) | string | The type of Saved Object. Each plugin can define it's own custom Saved Object types. | | [updated\_at](./kibana-plugin-core-server.savedobject.updated_at.md) | string | Timestamp of the last time this document had been updated. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobject.originid.md b/docs/development/core/server/kibana-plugin-core-server.savedobject.originid.md new file mode 100644 index 0000000000000..95bcad7ce8b1b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobject.originid.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObject](./kibana-plugin-core-server.savedobject.md) > [originId](./kibana-plugin-core-server.savedobject.originid.md) + +## SavedObject.originId property + +The ID of the saved object this originated from. This is set if this object's `id` was regenerated; that can happen during migration from a legacy single-namespace type, or during import. It is only set during migration or create operations. This is used during import to ensure that ID regeneration is deterministic, so saved objects will be overwritten if they are imported multiple times into a given space. + +Signature: + +```typescript +originId?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md index 5a9ca36ba56f4..fed7ef5e9df58 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md @@ -18,6 +18,7 @@ export interface SavedObjectsBulkCreateObject | [attributes](./kibana-plugin-core-server.savedobjectsbulkcreateobject.attributes.md) | T | | | [id](./kibana-plugin-core-server.savedobjectsbulkcreateobject.id.md) | string | | | [migrationVersion](./kibana-plugin-core-server.savedobjectsbulkcreateobject.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | +| [originId](./kibana-plugin-core-server.savedobjectsbulkcreateobject.originid.md) | string | Optional ID of the original saved object, if this object's id was regenerated | | [references](./kibana-plugin-core-server.savedobjectsbulkcreateobject.references.md) | SavedObjectReference[] | | | [type](./kibana-plugin-core-server.savedobjectsbulkcreateobject.type.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.originid.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.originid.md new file mode 100644 index 0000000000000..c182a47891f62 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.originid.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsBulkCreateObject](./kibana-plugin-core-server.savedobjectsbulkcreateobject.md) > [originId](./kibana-plugin-core-server.savedobjectsbulkcreateobject.originid.md) + +## SavedObjectsBulkCreateObject.originId property + +Optional ID of the original saved object, if this object's `id` was regenerated + +Signature: + +```typescript +originId?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md index 5e9433c5c9196..95f27f5140fba 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md @@ -17,6 +17,7 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions | --- | --- | --- | | [id](./kibana-plugin-core-server.savedobjectscreateoptions.id.md) | string | (not recommended) Specify an id for the document | | [migrationVersion](./kibana-plugin-core-server.savedobjectscreateoptions.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | +| [originId](./kibana-plugin-core-server.savedobjectscreateoptions.originid.md) | string | Optional ID of the original saved object, if this object's id was regenerated | | [overwrite](./kibana-plugin-core-server.savedobjectscreateoptions.overwrite.md) | boolean | Overwrite existing documents (defaults to false) | | [references](./kibana-plugin-core-server.savedobjectscreateoptions.references.md) | SavedObjectReference[] | | | [refresh](./kibana-plugin-core-server.savedobjectscreateoptions.refresh.md) | MutatingOperationRefreshSetting | The Elasticsearch Refresh setting for this operation | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.originid.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.originid.md new file mode 100644 index 0000000000000..14333079f7440 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.originid.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md) > [originId](./kibana-plugin-core-server.savedobjectscreateoptions.originid.md) + +## SavedObjectsCreateOptions.originId property + +Optional ID of the original saved object, if this object's `id` was regenerated + +Signature: + +```typescript +originId?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md index 7421f4282ec93..7d083a3c32646 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md @@ -21,6 +21,7 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions | [hasReference](./kibana-plugin-core-server.savedobjectsfindoptions.hasreference.md) | {
type: string;
id: string;
} | | | [page](./kibana-plugin-core-server.savedobjectsfindoptions.page.md) | number | | | [perPage](./kibana-plugin-core-server.savedobjectsfindoptions.perpage.md) | number | | +| [rawSearchFields](./kibana-plugin-core-server.savedobjectsfindoptions.rawsearchfields.md) | string[] | The fields to perform the parsed query against. Unlike the searchFields argument, these are expected to be raw and will not be modified. If used in conjunction with searchFields, both are concatenated together. | | [search](./kibana-plugin-core-server.savedobjectsfindoptions.search.md) | string | Search documents using the Elasticsearch Simple Query String syntax. See Elasticsearch Simple Query String query argument for more information | | [searchFields](./kibana-plugin-core-server.savedobjectsfindoptions.searchfields.md) | string[] | The fields to perform the parsed query against. See Elasticsearch Simple Query String fields argument for more information | | [sortField](./kibana-plugin-core-server.savedobjectsfindoptions.sortfield.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.rawsearchfields.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.rawsearchfields.md new file mode 100644 index 0000000000000..11626ac040595 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.rawsearchfields.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsFindOptions](./kibana-plugin-core-server.savedobjectsfindoptions.md) > [rawSearchFields](./kibana-plugin-core-server.savedobjectsfindoptions.rawsearchfields.md) + +## SavedObjectsFindOptions.rawSearchFields property + +The fields to perform the parsed query against. Unlike the `searchFields` argument, these are expected to be raw and will not be modified. If used in conjunction with `searchFields`, both are concatenated together. + +Signature: + +```typescript +rawSearchFields?: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.destinations.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.destinations.md new file mode 100644 index 0000000000000..445979dd740d3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.destinations.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportAmbiguousConflictError](./kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.md) > [destinations](./kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.destinations.md) + +## SavedObjectsImportAmbiguousConflictError.destinations property + +Signature: + +```typescript +destinations: Array<{ + id: string; + title?: string; + updatedAt?: string; + }>; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.md new file mode 100644 index 0000000000000..2b32825354364 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportAmbiguousConflictError](./kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.md) + +## SavedObjectsImportAmbiguousConflictError interface + +Represents a failure to import due to a conflict, which can be resolved in different ways with an overwrite. + +Signature: + +```typescript +export interface SavedObjectsImportAmbiguousConflictError +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [destinations](./kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.destinations.md) | Array<{
id: string;
title?: string;
updatedAt?: string;
}> | | +| [sources](./kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.sources.md) | Array<{
id: string;
title?: string;
updatedAt?: string;
}> | | +| [type](./kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.type.md) | 'ambiguous_conflict' | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.sources.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.sources.md new file mode 100644 index 0000000000000..4edefec1ad100 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.sources.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportAmbiguousConflictError](./kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.md) > [sources](./kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.sources.md) + +## SavedObjectsImportAmbiguousConflictError.sources property + +Signature: + +```typescript +sources: Array<{ + id: string; + title?: string; + updatedAt?: string; + }>; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.type.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.type.md new file mode 100644 index 0000000000000..ca98682873033 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportAmbiguousConflictError](./kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.md) > [type](./kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.type.md) + +## SavedObjectsImportAmbiguousConflictError.type property + +Signature: + +```typescript +type: 'ambiguous_conflict'; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.error.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.error.md index a5d33de32d594..6fc0c86b2fafc 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.error.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.error.md @@ -7,5 +7,5 @@ Signature: ```typescript -error: SavedObjectsImportConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError; +error: SavedObjectsImportConflictError | SavedObjectsImportAmbiguousConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.md index 473812fcbfd72..7383ebdb8192b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.md @@ -16,7 +16,7 @@ export interface SavedObjectsImportError | Property | Type | Description | | --- | --- | --- | -| [error](./kibana-plugin-core-server.savedobjectsimporterror.error.md) | SavedObjectsImportConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError | | +| [error](./kibana-plugin-core-server.savedobjectsimporterror.error.md) | SavedObjectsImportConflictError | SavedObjectsImportAmbiguousConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError | | | [id](./kibana-plugin-core-server.savedobjectsimporterror.id.md) | string | | | [title](./kibana-plugin-core-server.savedobjectsimporterror.title.md) | string | | | [type](./kibana-plugin-core-server.savedobjectsimporterror.type.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.md index f9da9956772bb..0a307aa07aa73 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.md @@ -21,5 +21,5 @@ export interface SavedObjectsImportOptions | [overwrite](./kibana-plugin-core-server.savedobjectsimportoptions.overwrite.md) | boolean | if true, will override existing object if present | | [readStream](./kibana-plugin-core-server.savedobjectsimportoptions.readstream.md) | Readable | The stream of [saved objects](./kibana-plugin-core-server.savedobject.md) to import | | [savedObjectsClient](./kibana-plugin-core-server.savedobjectsimportoptions.savedobjectsclient.md) | SavedObjectsClientContract | [client](./kibana-plugin-core-server.savedobjectsclientcontract.md) to use to perform the import operation | -| [supportedTypes](./kibana-plugin-core-server.savedobjectsimportoptions.supportedtypes.md) | string[] | the list of allowed types to import | +| [typeRegistry](./kibana-plugin-core-server.savedobjectsimportoptions.typeregistry.md) | ISavedObjectTypeRegistry | The registry of all known saved object types | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.supportedtypes.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.supportedtypes.md deleted file mode 100644 index 999cb73cbdfba..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.supportedtypes.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportOptions](./kibana-plugin-core-server.savedobjectsimportoptions.md) > [supportedTypes](./kibana-plugin-core-server.savedobjectsimportoptions.supportedtypes.md) - -## SavedObjectsImportOptions.supportedTypes property - -the list of allowed types to import - -Signature: - -```typescript -supportedTypes: string[]; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.typeregistry.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.typeregistry.md new file mode 100644 index 0000000000000..89c49471d24ef --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.typeregistry.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportOptions](./kibana-plugin-core-server.savedobjectsimportoptions.md) > [typeRegistry](./kibana-plugin-core-server.savedobjectsimportoptions.typeregistry.md) + +## SavedObjectsImportOptions.typeRegistry property + +The registry of all known saved object types + +Signature: + +```typescript +typeRegistry: ISavedObjectTypeRegistry; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.md index 641934d43eddf..52d39d981d0c2 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.md @@ -19,4 +19,5 @@ export interface SavedObjectsImportResponse | [errors](./kibana-plugin-core-server.savedobjectsimportresponse.errors.md) | SavedObjectsImportError[] | | | [success](./kibana-plugin-core-server.savedobjectsimportresponse.success.md) | boolean | | | [successCount](./kibana-plugin-core-server.savedobjectsimportresponse.successcount.md) | number | | +| [successResults](./kibana-plugin-core-server.savedobjectsimportresponse.successresults.md) | SavedObjectsImportSuccess[] | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.successresults.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.successresults.md new file mode 100644 index 0000000000000..63951d3a0b25f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.successresults.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportResponse](./kibana-plugin-core-server.savedobjectsimportresponse.md) > [successResults](./kibana-plugin-core-server.savedobjectsimportresponse.successresults.md) + +## SavedObjectsImportResponse.successResults property + +Signature: + +```typescript +successResults?: SavedObjectsImportSuccess[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.duplicate.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.duplicate.md new file mode 100644 index 0000000000000..2b589d47a11a7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.duplicate.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportRetry](./kibana-plugin-core-server.savedobjectsimportretry.md) > [duplicate](./kibana-plugin-core-server.savedobjectsimportretry.duplicate.md) + +## SavedObjectsImportRetry.duplicate property + +Resolve an import conflict by creating a duplicate object with a new (undefined) originId. Note: this attribute is mutually-exclusive with `overwrite`. If both are enabled, `overwrite` takes precedence. + +Signature: + +```typescript +duplicate?: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.idtooverwrite.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.idtooverwrite.md new file mode 100644 index 0000000000000..c060fe0bfe121 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.idtooverwrite.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportRetry](./kibana-plugin-core-server.savedobjectsimportretry.md) > [idToOverwrite](./kibana-plugin-core-server.savedobjectsimportretry.idtooverwrite.md) + +## SavedObjectsImportRetry.idToOverwrite property + +The object ID that will be overwritten. Only used if `overwrite` == true. This is required to resolve ambiguous conflicts. + +Signature: + +```typescript +idToOverwrite?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.md index 64d8164a1c4a5..a5adc8a1f9f2d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.md @@ -16,8 +16,10 @@ export interface SavedObjectsImportRetry | Property | Type | Description | | --- | --- | --- | +| [duplicate](./kibana-plugin-core-server.savedobjectsimportretry.duplicate.md) | boolean | Resolve an import conflict by creating a duplicate object with a new (undefined) originId. Note: this attribute is mutually-exclusive with overwrite. If both are enabled, overwrite takes precedence. | | [id](./kibana-plugin-core-server.savedobjectsimportretry.id.md) | string | | -| [overwrite](./kibana-plugin-core-server.savedobjectsimportretry.overwrite.md) | boolean | | +| [idToOverwrite](./kibana-plugin-core-server.savedobjectsimportretry.idtooverwrite.md) | string | The object ID that will be overwritten. Only used if overwrite == true. This is required to resolve ambiguous conflicts. | +| [overwrite](./kibana-plugin-core-server.savedobjectsimportretry.overwrite.md) | boolean | Resolve an import conflict by overwriting a destination object. Note: this attribute is mutually-exclusive with duplicate. If both are enabled, overwrite takes precedence. | | [replaceReferences](./kibana-plugin-core-server.savedobjectsimportretry.replacereferences.md) | Array<{
type: string;
from: string;
to: string;
}> | | | [type](./kibana-plugin-core-server.savedobjectsimportretry.type.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.overwrite.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.overwrite.md index f4358ab986563..46fd44f17bf73 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.overwrite.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.overwrite.md @@ -4,6 +4,8 @@ ## SavedObjectsImportRetry.overwrite property +Resolve an import conflict by overwriting a destination object. Note: this attribute is mutually-exclusive with `duplicate`. If both are enabled, `overwrite` takes precedence. + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.id.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.id.md new file mode 100644 index 0000000000000..5b95f7f64bfac --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportSuccess](./kibana-plugin-core-server.savedobjectsimportsuccess.md) > [id](./kibana-plugin-core-server.savedobjectsimportsuccess.id.md) + +## SavedObjectsImportSuccess.id property + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.md new file mode 100644 index 0000000000000..875dd0989de9e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportSuccess](./kibana-plugin-core-server.savedobjectsimportsuccess.md) + +## SavedObjectsImportSuccess interface + +Represents a successful import. + +Signature: + +```typescript +export interface SavedObjectsImportSuccess +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [id](./kibana-plugin-core-server.savedobjectsimportsuccess.id.md) | string | | +| [newId](./kibana-plugin-core-server.savedobjectsimportsuccess.newid.md) | string | If newId is specified, the new object has a new ID that is different from the import ID. | +| [type](./kibana-plugin-core-server.savedobjectsimportsuccess.type.md) | string | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.newid.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.newid.md new file mode 100644 index 0000000000000..abbaced760b7b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.newid.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportSuccess](./kibana-plugin-core-server.savedobjectsimportsuccess.md) > [newId](./kibana-plugin-core-server.savedobjectsimportsuccess.newid.md) + +## SavedObjectsImportSuccess.newId property + +If `newId` is specified, the new object has a new ID that is different from the import ID. + +Signature: + +```typescript +newId?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.type.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.type.md new file mode 100644 index 0000000000000..e6aa894cd0af9 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportSuccess](./kibana-plugin-core-server.savedobjectsimportsuccess.md) > [type](./kibana-plugin-core-server.savedobjectsimportsuccess.type.md) + +## SavedObjectsImportSuccess.type property + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.find.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.find.md index 22222061b3077..55382d0fd019e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.find.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.find.md @@ -7,14 +7,14 @@ Signature: ```typescript -find({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, }: SavedObjectsFindOptions): Promise>; +find({ search, defaultSearchOperator, searchFields, rawSearchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, }: SavedObjectsFindOptions): Promise>; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| { search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, } | SavedObjectsFindOptions | | +| { search, defaultSearchOperator, searchFields, rawSearchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, } | SavedObjectsFindOptions | | Returns: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md index 6b02cd910cdb1..f3a2ee38cbdbd 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md @@ -9,14 +9,7 @@ Increases a counter field by one. Creates the document if one doesn't exist for Signature: ```typescript -incrementCounter(type: string, id: string, counterFieldName: string, options?: SavedObjectsIncrementCounterOptions): Promise<{ - id: string; - type: string; - updated_at: string; - references: any; - version: string; - attributes: any; - }>; +incrementCounter(type: string, id: string, counterFieldName: string, options?: SavedObjectsIncrementCounterOptions): Promise; ``` ## Parameters @@ -30,14 +23,7 @@ incrementCounter(type: string, id: string, counterFieldName: string, options?: S Returns: -`Promise<{ - id: string; - type: string; - updated_at: string; - references: any; - version: string; - attributes: any; - }>` +`Promise` {promise} diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md index bd86ff3abbe9b..71167d23aabd7 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md @@ -23,7 +23,7 @@ export declare class SavedObjectsRepository | [delete(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.delete.md) | | Deletes an object | | [deleteByNamespace(namespace, options)](./kibana-plugin-core-server.savedobjectsrepository.deletebynamespace.md) | | Deletes all objects from the provided namespace. | | [deleteFromNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) | | Removes one or more namespaces from a given multi-namespace saved object. If no namespaces remain, the saved object is deleted entirely. This method and \[addToNamespaces\][SavedObjectsRepository.addToNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to. | -| [find({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, })](./kibana-plugin-core-server.savedobjectsrepository.find.md) | | | +| [find({ search, defaultSearchOperator, searchFields, rawSearchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, })](./kibana-plugin-core-server.savedobjectsrepository.find.md) | | | | [get(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.get.md) | | Gets a single object | | [incrementCounter(type, id, counterFieldName, options)](./kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md) | | Increases a counter field by one. Creates the document if one doesn't exist for the given id. | | [update(type, id, attributes, options)](./kibana-plugin-core-server.savedobjectsrepository.update.md) | | Updates an object | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md index c701b0a6d9bf7..4a01dd3161b37 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md @@ -21,5 +21,5 @@ export interface SavedObjectsResolveImportErrorsOptions | [readStream](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.readstream.md) | Readable | The stream of [saved objects](./kibana-plugin-core-server.savedobject.md) to resolve errors from | | [retries](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.retries.md) | SavedObjectsImportRetry[] | saved object import references to retry | | [savedObjectsClient](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.savedobjectsclient.md) | SavedObjectsClientContract | client to use to perform the import operation | -| [supportedTypes](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.supportedtypes.md) | string[] | the list of allowed types to import | +| [typeRegistry](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.typeregistry.md) | ISavedObjectTypeRegistry | The registry of all known saved object types | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.supportedtypes.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.typeregistry.md similarity index 54% rename from docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.supportedtypes.md rename to docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.typeregistry.md index f5b7c3692b017..f06d3eb08c0ac 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.supportedtypes.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.typeregistry.md @@ -1,13 +1,13 @@ -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsResolveImportErrorsOptions](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md) > [supportedTypes](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.supportedtypes.md) +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsResolveImportErrorsOptions](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md) > [typeRegistry](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.typeregistry.md) -## SavedObjectsResolveImportErrorsOptions.supportedTypes property +## SavedObjectsResolveImportErrorsOptions.typeRegistry property -the list of allowed types to import +The registry of all known saved object types Signature: ```typescript -supportedTypes: string[]; +typeRegistry: ISavedObjectTypeRegistry; ``` diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 4ccded9b9afec..cf9c47a64887a 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -1009,12 +1009,15 @@ export interface SavedObject { attributes: T; // (undocumented) error?: { + error: string; message: string; statusCode: number; + metadata?: Record; }; id: string; migrationVersion?: SavedObjectsMigrationVersion; namespaces?: string[]; + originId?: string; references: SavedObjectReference[]; type: string; updated_at?: string; @@ -1132,6 +1135,7 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { page?: number; // (undocumented) perPage?: number; + rawSearchFields?: string[]; search?: string; searchFields?: string[]; // (undocumented) @@ -1152,6 +1156,24 @@ export interface SavedObjectsFindResponsePublic extends SavedObject total: number; } +// @public +export interface SavedObjectsImportAmbiguousConflictError { + // (undocumented) + destinations: Array<{ + id: string; + title?: string; + updatedAt?: string; + }>; + // (undocumented) + sources: Array<{ + id: string; + title?: string; + updatedAt?: string; + }>; + // (undocumented) + type: 'ambiguous_conflict'; +} + // @public export interface SavedObjectsImportConflictError { // (undocumented) @@ -1161,7 +1183,7 @@ export interface SavedObjectsImportConflictError { // @public export interface SavedObjectsImportError { // (undocumented) - error: SavedObjectsImportConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError; + error: SavedObjectsImportConflictError | SavedObjectsImportAmbiguousConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError; // (undocumented) id: string; // (undocumented) @@ -1194,13 +1216,16 @@ export interface SavedObjectsImportResponse { success: boolean; // (undocumented) successCount: number; + // (undocumented) + successResults?: SavedObjectsImportSuccess[]; } // @public export interface SavedObjectsImportRetry { + duplicate?: boolean; // (undocumented) id: string; - // (undocumented) + idToOverwrite?: string; overwrite: boolean; // (undocumented) replaceReferences: Array<{ @@ -1212,6 +1237,15 @@ export interface SavedObjectsImportRetry { type: string; } +// @public +export interface SavedObjectsImportSuccess { + // (undocumented) + id: string; + newId?: string; + // (undocumented) + type: string; +} + // @public export interface SavedObjectsImportUnknownError { // (undocumented) diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index fcf9a9e2dedc2..cf06a0bb2a29e 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1031,7 +1031,7 @@ export interface ImageValidation { } // @public -export function importSavedObjectsFromStream({ readStream, objectLimit, overwrite, savedObjectsClient, supportedTypes, namespace, }: SavedObjectsImportOptions): Promise; +export function importSavedObjectsFromStream({ readStream, objectLimit, overwrite, savedObjectsClient, typeRegistry, namespace, }: SavedObjectsImportOptions): Promise; // @public (undocumented) export interface IndexSettingsDeprecationInfo { @@ -1595,7 +1595,7 @@ export type RequestHandlerContextProvider(handler: RequestHandler) => RequestHandler; // @public -export function resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, supportedTypes, namespace, }: SavedObjectsResolveImportErrorsOptions): Promise; +export function resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, typeRegistry, namespace, }: SavedObjectsResolveImportErrorsOptions): Promise; // @public export type ResponseError = string | Error | { @@ -1699,12 +1699,15 @@ export interface SavedObject { attributes: T; // (undocumented) error?: { + error: string; message: string; statusCode: number; + metadata?: Record; }; id: string; migrationVersion?: SavedObjectsMigrationVersion; namespaces?: string[]; + originId?: string; references: SavedObjectReference[]; type: string; updated_at?: string; @@ -1771,6 +1774,7 @@ export interface SavedObjectsBulkCreateObject { // (undocumented) id?: string; migrationVersion?: SavedObjectsMigrationVersion; + originId?: string; // (undocumented) references?: SavedObjectReference[]; // (undocumented) @@ -1902,6 +1906,7 @@ export interface SavedObjectsCoreFieldMapping { export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions { id?: string; migrationVersion?: SavedObjectsMigrationVersion; + originId?: string; overwrite?: boolean; // (undocumented) references?: SavedObjectReference[]; @@ -2019,6 +2024,7 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { page?: number; // (undocumented) perPage?: number; + rawSearchFields?: string[]; search?: string; searchFields?: string[]; // (undocumented) @@ -2041,6 +2047,24 @@ export interface SavedObjectsFindResponse { total: number; } +// @public +export interface SavedObjectsImportAmbiguousConflictError { + // (undocumented) + destinations: Array<{ + id: string; + title?: string; + updatedAt?: string; + }>; + // (undocumented) + sources: Array<{ + id: string; + title?: string; + updatedAt?: string; + }>; + // (undocumented) + type: 'ambiguous_conflict'; +} + // @public export interface SavedObjectsImportConflictError { // (undocumented) @@ -2050,7 +2074,7 @@ export interface SavedObjectsImportConflictError { // @public export interface SavedObjectsImportError { // (undocumented) - error: SavedObjectsImportConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError; + error: SavedObjectsImportConflictError | SavedObjectsImportAmbiguousConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError; // (undocumented) id: string; // (undocumented) @@ -2082,7 +2106,7 @@ export interface SavedObjectsImportOptions { overwrite: boolean; readStream: Readable; savedObjectsClient: SavedObjectsClientContract; - supportedTypes: string[]; + typeRegistry: ISavedObjectTypeRegistry; } // @public @@ -2093,13 +2117,16 @@ export interface SavedObjectsImportResponse { success: boolean; // (undocumented) successCount: number; + // (undocumented) + successResults?: SavedObjectsImportSuccess[]; } // @public export interface SavedObjectsImportRetry { + duplicate?: boolean; // (undocumented) id: string; - // (undocumented) + idToOverwrite?: string; overwrite: boolean; // (undocumented) replaceReferences: Array<{ @@ -2111,6 +2138,15 @@ export interface SavedObjectsImportRetry { type: string; } +// @public +export interface SavedObjectsImportSuccess { + // (undocumented) + id: string; + newId?: string; + // (undocumented) + type: string; +} + // @public export interface SavedObjectsImportUnknownError { // (undocumented) @@ -2221,16 +2257,9 @@ export class SavedObjectsRepository { deleteByNamespace(namespace: string, options?: SavedObjectsDeleteByNamespaceOptions): Promise; deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<{}>; // (undocumented) - find({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, }: SavedObjectsFindOptions): Promise>; + find({ search, defaultSearchOperator, searchFields, rawSearchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, }: SavedObjectsFindOptions): Promise>; get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; - incrementCounter(type: string, id: string, counterFieldName: string, options?: SavedObjectsIncrementCounterOptions): Promise<{ - id: string; - type: string; - updated_at: string; - references: any; - version: string; - attributes: any; - }>; + incrementCounter(type: string, id: string, counterFieldName: string, options?: SavedObjectsIncrementCounterOptions): Promise; update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; } @@ -2247,7 +2276,7 @@ export interface SavedObjectsResolveImportErrorsOptions { readStream: Readable; retries: SavedObjectsImportRetry[]; savedObjectsClient: SavedObjectsClientContract; - supportedTypes: string[]; + typeRegistry: ISavedObjectTypeRegistry; } // @internal @deprecated (undocumented) From 2d84ae211c8e5a79c64f303d654a3862e6f141a1 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Wed, 13 May 2020 09:42:22 -0400 Subject: [PATCH 13/55] Add `originId` field to default saved object mappings --- .../core/__snapshots__/build_active_mappings.test.ts.snap | 8 ++++++++ .../migrations/core/build_active_mappings.ts | 3 +++ .../saved_objects/migrations/core/index_migrator.test.ts | 4 ++++ .../kibana/__snapshots__/kibana_migrator.test.ts.snap | 4 ++++ 4 files changed, 19 insertions(+) diff --git a/src/core/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap b/src/core/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap index bc9a66926e880..f8ef47cae8944 100644 --- a/src/core/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap +++ b/src/core/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap @@ -9,6 +9,7 @@ Object { "migrationVersion": "4a1746014a75ade3a714e1db5763276f", "namespace": "2f4316de49999235636386fe51dc06c1", "namespaces": "2f4316de49999235636386fe51dc06c1", + "originId": "2f4316de49999235636386fe51dc06c1", "references": "7997cf5a56cc02bdc9c93361bde732b0", "type": "2f4316de49999235636386fe51dc06c1", "updated_at": "00da57df13e94e9d98437d13ace4bfe0", @@ -32,6 +33,9 @@ Object { "namespaces": Object { "type": "keyword", }, + "originId": Object { + "type": "keyword", + }, "references": Object { "properties": Object { "id": Object { @@ -64,6 +68,7 @@ Object { "migrationVersion": "4a1746014a75ade3a714e1db5763276f", "namespace": "2f4316de49999235636386fe51dc06c1", "namespaces": "2f4316de49999235636386fe51dc06c1", + "originId": "2f4316de49999235636386fe51dc06c1", "references": "7997cf5a56cc02bdc9c93361bde732b0", "secondType": "72d57924f415fbadb3ee293b67d233ab", "thirdType": "510f1f0adb69830cf8a1c5ce2923ed82", @@ -91,6 +96,9 @@ Object { "namespaces": Object { "type": "keyword", }, + "originId": Object { + "type": "keyword", + }, "references": Object { "properties": Object { "id": Object { diff --git a/src/core/server/saved_objects/migrations/core/build_active_mappings.ts b/src/core/server/saved_objects/migrations/core/build_active_mappings.ts index c2a7b11e057cd..d875ed970620b 100644 --- a/src/core/server/saved_objects/migrations/core/build_active_mappings.ts +++ b/src/core/server/saved_objects/migrations/core/build_active_mappings.ts @@ -142,6 +142,9 @@ function defaultMapping(): IndexMapping { namespaces: { type: 'keyword', }, + originId: { + type: 'keyword', + }, updated_at: { type: 'date', }, diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts index 392089c69f5a0..f2d189f34cc6c 100644 --- a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts @@ -62,6 +62,7 @@ describe('IndexMigrator', () => { migrationVersion: '4a1746014a75ade3a714e1db5763276f', namespace: '2f4316de49999235636386fe51dc06c1', namespaces: '2f4316de49999235636386fe51dc06c1', + originId: '2f4316de49999235636386fe51dc06c1', references: '7997cf5a56cc02bdc9c93361bde732b0', type: '2f4316de49999235636386fe51dc06c1', updated_at: '00da57df13e94e9d98437d13ace4bfe0', @@ -72,6 +73,7 @@ describe('IndexMigrator', () => { migrationVersion: { dynamic: 'true', type: 'object' }, namespace: { type: 'keyword' }, namespaces: { type: 'keyword' }, + originId: { type: 'keyword' }, type: { type: 'keyword' }, updated_at: { type: 'date' }, references: { @@ -181,6 +183,7 @@ describe('IndexMigrator', () => { migrationVersion: '4a1746014a75ade3a714e1db5763276f', namespace: '2f4316de49999235636386fe51dc06c1', namespaces: '2f4316de49999235636386fe51dc06c1', + originId: '2f4316de49999235636386fe51dc06c1', references: '7997cf5a56cc02bdc9c93361bde732b0', type: '2f4316de49999235636386fe51dc06c1', updated_at: '00da57df13e94e9d98437d13ace4bfe0', @@ -192,6 +195,7 @@ describe('IndexMigrator', () => { migrationVersion: { dynamic: 'true', type: 'object' }, namespace: { type: 'keyword' }, namespaces: { type: 'keyword' }, + originId: { type: 'keyword' }, type: { type: 'keyword' }, updated_at: { type: 'date' }, references: { diff --git a/src/core/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap b/src/core/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap index 3453f3fc80310..9311292a6a0ed 100644 --- a/src/core/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap +++ b/src/core/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap @@ -9,6 +9,7 @@ Object { "migrationVersion": "4a1746014a75ade3a714e1db5763276f", "namespace": "2f4316de49999235636386fe51dc06c1", "namespaces": "2f4316de49999235636386fe51dc06c1", + "originId": "2f4316de49999235636386fe51dc06c1", "references": "7997cf5a56cc02bdc9c93361bde732b0", "type": "2f4316de49999235636386fe51dc06c1", "updated_at": "00da57df13e94e9d98437d13ace4bfe0", @@ -40,6 +41,9 @@ Object { "namespaces": Object { "type": "keyword", }, + "originId": Object { + "type": "keyword", + }, "references": Object { "properties": Object { "id": Object { From 79544839a2d61d21d12e81e247baf87d2c9a6de6 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 19 May 2020 09:45:37 -0400 Subject: [PATCH 14/55] Add `destinationId?` to `SavedObjectsImportConflictError` type The implementation already included this field in some cases when using this as a return type, for some reason the type checker did not prohibit that though. --- ...c.savedobjectsimportconflicterror.destinationid.md | 11 +++++++++++ ...gin-core-public.savedobjectsimportconflicterror.md | 1 + ...r.savedobjectsimportconflicterror.destinationid.md | 11 +++++++++++ ...gin-core-server.savedobjectsimportconflicterror.md | 1 + src/core/public/public.api.md | 2 ++ src/core/server/saved_objects/import/types.ts | 1 + src/core/server/server.api.md | 2 ++ 7 files changed, 29 insertions(+) create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportconflicterror.destinationid.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportconflicterror.destinationid.md diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportconflicterror.destinationid.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportconflicterror.destinationid.md new file mode 100644 index 0000000000000..ba4002d932f57 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportconflicterror.destinationid.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportConflictError](./kibana-plugin-core-public.savedobjectsimportconflicterror.md) > [destinationId](./kibana-plugin-core-public.savedobjectsimportconflicterror.destinationid.md) + +## SavedObjectsImportConflictError.destinationId property + +Signature: + +```typescript +destinationId?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportconflicterror.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportconflicterror.md index a54cdac56c218..b0320b05ecadc 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportconflicterror.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportconflicterror.md @@ -16,5 +16,6 @@ export interface SavedObjectsImportConflictError | Property | Type | Description | | --- | --- | --- | +| [destinationId](./kibana-plugin-core-public.savedobjectsimportconflicterror.destinationid.md) | string | | | [type](./kibana-plugin-core-public.savedobjectsimportconflicterror.type.md) | 'conflict' | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportconflicterror.destinationid.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportconflicterror.destinationid.md new file mode 100644 index 0000000000000..858f171223472 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportconflicterror.destinationid.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportConflictError](./kibana-plugin-core-server.savedobjectsimportconflicterror.md) > [destinationId](./kibana-plugin-core-server.savedobjectsimportconflicterror.destinationid.md) + +## SavedObjectsImportConflictError.destinationId property + +Signature: + +```typescript +destinationId?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportconflicterror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportconflicterror.md index a3e946eccb984..153cd55c9199e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportconflicterror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportconflicterror.md @@ -16,5 +16,6 @@ export interface SavedObjectsImportConflictError | Property | Type | Description | | --- | --- | --- | +| [destinationId](./kibana-plugin-core-server.savedobjectsimportconflicterror.destinationid.md) | string | | | [type](./kibana-plugin-core-server.savedobjectsimportconflicterror.type.md) | 'conflict' | | diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index cf9c47a64887a..bda54c98a7135 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -1176,6 +1176,8 @@ export interface SavedObjectsImportAmbiguousConflictError { // @public export interface SavedObjectsImportConflictError { + // (undocumented) + destinationId?: string; // (undocumented) type: 'conflict'; } diff --git a/src/core/server/saved_objects/import/types.ts b/src/core/server/saved_objects/import/types.ts index 26b08c92afeb3..8ac893b86b194 100644 --- a/src/core/server/saved_objects/import/types.ts +++ b/src/core/server/saved_objects/import/types.ts @@ -55,6 +55,7 @@ export interface SavedObjectsImportRetry { */ export interface SavedObjectsImportConflictError { type: 'conflict'; + destinationId?: string; } /** diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index cf06a0bb2a29e..3f987f42427c5 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2067,6 +2067,8 @@ export interface SavedObjectsImportAmbiguousConflictError { // @public export interface SavedObjectsImportConflictError { + // (undocumented) + destinationId?: string; // (undocumented) type: 'conflict'; } From f9cb833f3e5bc7ebb6aa7af21b8680d3786972f5 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 2 Jun 2020 13:48:04 -0400 Subject: [PATCH 15/55] Remove "duplicate" resolution of import conflicts This reverts commit 924ef9667c9489311a238ab2f073369c8af82c9b. It also updates API docs and integration tests accordingly. --- ...ublic.savedobjectsimportretry.duplicate.md | 13 --- ...gin-core-public.savedobjectsimportretry.md | 3 +- ...ublic.savedobjectsimportretry.overwrite.md | 2 - ...erver.savedobjectsimportretry.duplicate.md | 13 --- ...gin-core-server.savedobjectsimportretry.md | 3 +- ...erver.savedobjectsimportretry.overwrite.md | 2 - src/core/public/public.api.md | 2 +- .../import/check_conflicts.test.ts | 20 +--- .../saved_objects/import/check_conflicts.ts | 7 +- .../import/create_saved_objects.test.ts | 8 +- .../import/create_saved_objects.ts | 4 +- src/core/server/saved_objects/import/types.ts | 9 -- .../routes/resolve_import_errors.ts | 3 - src/core/server/server.api.md | 2 +- .../routes/api/external/copy_to_space.test.ts | 24 ----- .../routes/api/external/copy_to_space.ts | 3 - .../common/suites/resolve_import_errors.ts | 26 ++--- .../apis/resolve_import_errors.ts | 102 ++++++++---------- .../apis/resolve_import_errors.ts | 62 ++++------- .../spaces_only/apis/resolve_import_errors.ts | 53 ++++----- .../suites/resolve_copy_to_space_conflicts.ts | 75 ++++++------- 21 files changed, 138 insertions(+), 298 deletions(-) delete mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.duplicate.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.duplicate.md diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.duplicate.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.duplicate.md deleted file mode 100644 index 44f3682a8aab2..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.duplicate.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportRetry](./kibana-plugin-core-public.savedobjectsimportretry.md) > [duplicate](./kibana-plugin-core-public.savedobjectsimportretry.duplicate.md) - -## SavedObjectsImportRetry.duplicate property - -Resolve an import conflict by creating a duplicate object with a new (undefined) originId. Note: this attribute is mutually-exclusive with `overwrite`. If both are enabled, `overwrite` takes precedence. - -Signature: - -```typescript -duplicate?: boolean; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.md index 6c083438ccd7f..9ca858c681508 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.md @@ -16,10 +16,9 @@ export interface SavedObjectsImportRetry | Property | Type | Description | | --- | --- | --- | -| [duplicate](./kibana-plugin-core-public.savedobjectsimportretry.duplicate.md) | boolean | Resolve an import conflict by creating a duplicate object with a new (undefined) originId. Note: this attribute is mutually-exclusive with overwrite. If both are enabled, overwrite takes precedence. | | [id](./kibana-plugin-core-public.savedobjectsimportretry.id.md) | string | | | [idToOverwrite](./kibana-plugin-core-public.savedobjectsimportretry.idtooverwrite.md) | string | The object ID that will be overwritten. Only used if overwrite == true. This is required to resolve ambiguous conflicts. | -| [overwrite](./kibana-plugin-core-public.savedobjectsimportretry.overwrite.md) | boolean | Resolve an import conflict by overwriting a destination object. Note: this attribute is mutually-exclusive with duplicate. If both are enabled, overwrite takes precedence. | +| [overwrite](./kibana-plugin-core-public.savedobjectsimportretry.overwrite.md) | boolean | | | [replaceReferences](./kibana-plugin-core-public.savedobjectsimportretry.replacereferences.md) | Array<{
type: string;
from: string;
to: string;
}> | | | [type](./kibana-plugin-core-public.savedobjectsimportretry.type.md) | string | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.overwrite.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.overwrite.md index 2c389faabf812..51ea151a9cdb3 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.overwrite.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.overwrite.md @@ -4,8 +4,6 @@ ## SavedObjectsImportRetry.overwrite property -Resolve an import conflict by overwriting a destination object. Note: this attribute is mutually-exclusive with `duplicate`. If both are enabled, `overwrite` takes precedence. - Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.duplicate.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.duplicate.md deleted file mode 100644 index 2b589d47a11a7..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.duplicate.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportRetry](./kibana-plugin-core-server.savedobjectsimportretry.md) > [duplicate](./kibana-plugin-core-server.savedobjectsimportretry.duplicate.md) - -## SavedObjectsImportRetry.duplicate property - -Resolve an import conflict by creating a duplicate object with a new (undefined) originId. Note: this attribute is mutually-exclusive with `overwrite`. If both are enabled, `overwrite` takes precedence. - -Signature: - -```typescript -duplicate?: boolean; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.md index a5adc8a1f9f2d..130bd98a8ea79 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.md @@ -16,10 +16,9 @@ export interface SavedObjectsImportRetry | Property | Type | Description | | --- | --- | --- | -| [duplicate](./kibana-plugin-core-server.savedobjectsimportretry.duplicate.md) | boolean | Resolve an import conflict by creating a duplicate object with a new (undefined) originId. Note: this attribute is mutually-exclusive with overwrite. If both are enabled, overwrite takes precedence. | | [id](./kibana-plugin-core-server.savedobjectsimportretry.id.md) | string | | | [idToOverwrite](./kibana-plugin-core-server.savedobjectsimportretry.idtooverwrite.md) | string | The object ID that will be overwritten. Only used if overwrite == true. This is required to resolve ambiguous conflicts. | -| [overwrite](./kibana-plugin-core-server.savedobjectsimportretry.overwrite.md) | boolean | Resolve an import conflict by overwriting a destination object. Note: this attribute is mutually-exclusive with duplicate. If both are enabled, overwrite takes precedence. | +| [overwrite](./kibana-plugin-core-server.savedobjectsimportretry.overwrite.md) | boolean | | | [replaceReferences](./kibana-plugin-core-server.savedobjectsimportretry.replacereferences.md) | Array<{
type: string;
from: string;
to: string;
}> | | | [type](./kibana-plugin-core-server.savedobjectsimportretry.type.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.overwrite.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.overwrite.md index 46fd44f17bf73..f4358ab986563 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.overwrite.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.overwrite.md @@ -4,8 +4,6 @@ ## SavedObjectsImportRetry.overwrite property -Resolve an import conflict by overwriting a destination object. Note: this attribute is mutually-exclusive with `duplicate`. If both are enabled, `overwrite` takes precedence. - Signature: ```typescript diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 20903c9d4e2d3..3decc1f197dec 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -1248,10 +1248,10 @@ export interface SavedObjectsImportResponse { // @public export interface SavedObjectsImportRetry { - duplicate?: boolean; // (undocumented) id: string; idToOverwrite?: string; + // (undocumented) overwrite: boolean; // (undocumented) replaceReferences: Array<{ diff --git a/src/core/server/saved_objects/import/check_conflicts.test.ts b/src/core/server/saved_objects/import/check_conflicts.test.ts index 36aef5549ccde..9481292b326ae 100644 --- a/src/core/server/saved_objects/import/check_conflicts.test.ts +++ b/src/core/server/saved_objects/import/check_conflicts.test.ts @@ -17,7 +17,6 @@ * under the License. */ -import { mockUuidv4 } from './__mocks__'; import { SavedObjectsClientContract, SavedObjectReference, @@ -461,9 +460,6 @@ describe('#getImportIdMapForRetries', () => { ): SavedObjectsImportRetry => { return { type, id, overwrite: true, idToOverwrite, replaceReferences: [] }; }; - const createDuplicateRetry = (obj: { type: string; id: string }): SavedObjectsImportRetry => { - return { type: obj.type, id: obj.id, overwrite: false, duplicate: true, replaceReferences: [] }; - }; test('returns expected results', async () => { const obj1 = createObject(OTHER_TYPE, 'id-1'); @@ -472,9 +468,7 @@ describe('#getImportIdMapForRetries', () => { const obj4 = createObject(MULTI_NS_TYPE, 'id-4'); const obj5 = createObject(MULTI_NS_TYPE, 'id-5'); const obj6 = createObject(MULTI_NS_TYPE, 'id-6'); - const obj7 = createObject(OTHER_TYPE, 'id-7'); - const obj8 = createObject(MULTI_NS_TYPE, 'id-8'); - const objects = [obj1, obj2, obj3, obj4, obj5, obj6, obj7, obj8]; + const objects = [obj1, obj2, obj3, obj4, obj5, obj6]; const retries = [ // all three overwrite retries for non-multi-namespace types are ignored; // retries for non-multi-namespace these should not have `idToOverwrite` specified, but we test it here for posterity @@ -484,20 +478,10 @@ describe('#getImportIdMapForRetries', () => { createOverwriteRetry(obj4), // retries that do not have `idToOverwrite` specified are ignored createOverwriteRetry(obj5, obj5.id), // retries that have `id` that matches `idToOverwrite` are ignored createOverwriteRetry(obj6, 'id-Y'), // this retry will get added to the `importIdMap`! - createDuplicateRetry(obj7), // this retry will get added to the `importIdMap`! - createDuplicateRetry(obj8), // this retry will get added to the `importIdMap`! ]; const options = setupOptions(retries); - mockUuidv4.mockReturnValueOnce(`new-id-for-${obj7.id}`); - mockUuidv4.mockReturnValueOnce(`new-id-for-${obj8.id}`); const checkConflictsResult = await getImportIdMapForRetries(objects, options); - expect(checkConflictsResult).toEqual( - new Map([ - [`${obj6.type}:${obj6.id}`, { id: 'id-Y' }], - [`${obj7.type}:${obj7.id}`, { id: `new-id-for-${obj7.id}`, omitOriginId: true }], - [`${obj8.type}:${obj8.id}`, { id: `new-id-for-${obj8.id}`, omitOriginId: true }], - ]) - ); + expect(checkConflictsResult).toEqual(new Map([[`${obj6.type}:${obj6.id}`, { id: 'id-Y' }]])); }); }); diff --git a/src/core/server/saved_objects/import/check_conflicts.ts b/src/core/server/saved_objects/import/check_conflicts.ts index 1735dbb2b6dc6..26f9795a46e80 100644 --- a/src/core/server/saved_objects/import/check_conflicts.ts +++ b/src/core/server/saved_objects/import/check_conflicts.ts @@ -19,7 +19,6 @@ /* eslint-disable @typescript-eslint/consistent-type-definitions */ -import { v4 as uuidv4 } from 'uuid'; import { SavedObject, SavedObjectsClientContract, @@ -197,12 +196,12 @@ export async function getImportIdMapForRetries( (acc, cur) => acc.set(`${cur.type}:${cur.id}`, cur), new Map() ); - const importIdMap = new Map(); + const importIdMap = new Map(); objects.forEach(({ type, id }) => { const retry = retryMap.get(`${type}:${id}`); if (retry) { - const { overwrite, idToOverwrite, duplicate } = retry; + const { overwrite, idToOverwrite } = retry; if ( overwrite && idToOverwrite && @@ -210,8 +209,6 @@ export async function getImportIdMapForRetries( typeRegistry.isMultiNamespace(type) ) { importIdMap.set(`${type}:${id}`, { id: idToOverwrite }); - } else if (!overwrite && duplicate) { - importIdMap.set(`${type}:${id}`, { id: uuidv4(), omitOriginId: true }); } } }); diff --git a/src/core/server/saved_objects/import/create_saved_objects.test.ts b/src/core/server/saved_objects/import/create_saved_objects.test.ts index c808928909b2b..b1129ec574270 100644 --- a/src/core/server/saved_objects/import/create_saved_objects.test.ts +++ b/src/core/server/saved_objects/import/create_saved_objects.test.ts @@ -45,7 +45,7 @@ const OTHER_TYPE = 'other'; */ const obj1 = createObject(MULTI_NS_TYPE, 'id-1', 'originId-a'); // -> success const obj2 = createObject(MULTI_NS_TYPE, 'id-2', 'originId-b'); // -> conflict -const obj3 = createObject(MULTI_NS_TYPE, 'id-3', 'originId-c'); // -> conflict (with known importId and omitOriginId=true) +const obj3 = createObject(MULTI_NS_TYPE, 'id-3', 'originId-c'); // -> conflict (with known importId) const obj4 = createObject(MULTI_NS_TYPE, 'id-4', 'originId-d'); // -> conflict (with known importId) const obj5 = createObject(MULTI_NS_TYPE, 'id-5', 'originId-e'); // -> unresolvable conflict const obj6 = createObject(MULTI_NS_TYPE, 'id-6'); // -> success @@ -62,7 +62,7 @@ const importId3 = 'id-foo'; const importId4 = 'id-bar'; const importId8 = 'id-baz'; const importIdMap = new Map([ - [`${obj3.type}:${obj3.id}`, { id: importId3, omitOriginId: true }], + [`${obj3.type}:${obj3.id}`, { id: importId3 }], [`${obj4.type}:${obj4.id}`, { id: importId4 }], [`${obj8.type}:${obj8.id}`, { id: importId8 }], ]); @@ -190,7 +190,7 @@ describe('#createSavedObjects', () => { await createSavedObjects(objs, options); expect(bulkCreate).toHaveBeenCalledTimes(1); // these three objects are transformed before being created, because they are included in the `importIdMap` - const x3 = { ...obj3, id: importId3, originId: undefined }; // this import object already has an originId, but the retry has omitOriginId=true + const x3 = { ...obj3, id: importId3 }; // this import object already has an originId const x4 = { ...obj4, id: importId4 }; // this import object already has an originId const x8 = { ...obj8, id: importId8, originId: obj8.id }; // this import object doesn't have an originId, so it is set before create const argObjs = [obj1, obj2, x3, x4, obj6, obj7, x8, obj10, obj11, obj12, obj13]; @@ -283,7 +283,7 @@ describe('#createSavedObjects', () => { await createSavedObjects(objs, options); expect(bulkCreate).toHaveBeenCalledTimes(2); // these three objects are transformed before being created, because they are included in the `importIdMap` - const x3 = { ...obj3, id: importId3, originId: undefined }; // this import object already has an originId, but the retry has omitOriginId=true + const x3 = { ...obj3, id: importId3 }; // this import object already has an originId const x4 = { ...obj4, id: importId4 }; // this import object already has an originId const x8 = { ...obj8, id: importId8, originId: obj8.id }; // this import object doesn't have an originId, so it is set before create const argObjs = [obj1, obj2, x3, x4, obj5, obj6, obj7, x8, obj9, obj10, obj11, obj12, obj13]; diff --git a/src/core/server/saved_objects/import/create_saved_objects.ts b/src/core/server/saved_objects/import/create_saved_objects.ts index 5904c30221d9c..e847f603e50f2 100644 --- a/src/core/server/saved_objects/import/create_saved_objects.ts +++ b/src/core/server/saved_objects/import/create_saved_objects.ts @@ -25,7 +25,7 @@ import { extractErrors } from './extract_errors'; interface CreateSavedObjectsOptions { savedObjectsClient: SavedObjectsClientContract; - importIdMap: Map; + importIdMap: Map; namespace?: string; overwrite?: boolean; } @@ -74,7 +74,7 @@ export const createSavedObjects = async ( const importIdEntry = importIdMap.get(`${object.type}:${object.id}`); if (importIdEntry) { objectIdMap.set(`${object.type}:${importIdEntry.id}`, object); - const originId = importIdEntry.omitOriginId ? undefined : object.originId ?? object.id; + const originId = object.originId ?? object.id; return { ...object, id: importIdEntry.id, originId }; } return object; diff --git a/src/core/server/saved_objects/import/types.ts b/src/core/server/saved_objects/import/types.ts index 8ac893b86b194..0838ce5ba3af8 100644 --- a/src/core/server/saved_objects/import/types.ts +++ b/src/core/server/saved_objects/import/types.ts @@ -28,20 +28,11 @@ import { ISavedObjectTypeRegistry } from '..'; export interface SavedObjectsImportRetry { type: string; id: string; - /** - * Resolve an import conflict by overwriting a destination object. - * Note: this attribute is mutually-exclusive with `duplicate`. If both are enabled, `overwrite` takes precedence. - */ overwrite: boolean; /** * The object ID that will be overwritten. Only used if `overwrite` == true. This is required to resolve ambiguous conflicts. */ idToOverwrite?: string; - /** - * Resolve an import conflict by creating a duplicate object with a new (undefined) originId. - * Note: this attribute is mutually-exclusive with `overwrite`. If both are enabled, `overwrite` takes precedence. - */ - duplicate?: boolean; replaceReferences: Array<{ type: string; from: string; diff --git a/src/core/server/saved_objects/routes/resolve_import_errors.ts b/src/core/server/saved_objects/routes/resolve_import_errors.ts index a192c60bf70d6..6e505cb862689 100644 --- a/src/core/server/saved_objects/routes/resolve_import_errors.ts +++ b/src/core/server/saved_objects/routes/resolve_import_errors.ts @@ -54,7 +54,6 @@ export const registerResolveImportErrorsRoute = (router: IRouter, config: SavedO id: schema.string(), overwrite: schema.boolean({ defaultValue: false }), idToOverwrite: schema.maybe(schema.string()), - duplicate: schema.boolean({ defaultValue: false }), replaceReferences: schema.arrayOf( schema.object({ type: schema.string(), @@ -68,8 +67,6 @@ export const registerResolveImportErrorsRoute = (router: IRouter, config: SavedO validate: (object) => { if (object.idToOverwrite && !object.overwrite) { return 'cannot use [idToOverwrite] without [overwrite]'; - } else if (object.overwrite && object.duplicate) { - return 'cannot use [overwrite] with [duplicate]'; } }, } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index c6d9389c3e34b..2f1645e4e61e3 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2125,10 +2125,10 @@ export interface SavedObjectsImportResponse { // @public export interface SavedObjectsImportRetry { - duplicate?: boolean; // (undocumented) id: string; idToOverwrite?: string; + // (undocumented) overwrite: boolean; // (undocumented) replaceReferences: Array<{ diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts index bb68e9272b923..da57dd3a62f33 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts @@ -332,30 +332,6 @@ describe('copy to space', () => { ); }); - it(`does not allow "overwrite" to be used with "duplicate"`, async () => { - const payload = { - retries: { - ['a-space']: [ - { - type: 'foo', - id: 'bar', - overwrite: true, - duplicate: true, - }, - ], - }, - objects: [{ type: 'foo', id: 'bar' }], - }; - - const { resolveConflicts } = await setup(); - - expect(() => - (resolveConflicts.routeValidation.body as ObjectType).validate(payload) - ).toThrowErrorMatchingInlineSnapshot( - `"[retries.a-space.0]: cannot use [overwrite] with [duplicate]"` - ); - }); - it(`requires well-formed space ids`, async () => { const payload = { retries: { diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts index 3a2080111dbc1..39d4c29416956 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts @@ -107,14 +107,11 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { id: schema.string(), overwrite: schema.boolean({ defaultValue: false }), idToOverwrite: schema.maybe(schema.string()), - duplicate: schema.boolean({ defaultValue: false }), }, { validate: (object) => { if (object.idToOverwrite && !object.overwrite) { return 'cannot use [idToOverwrite] without [overwrite]'; - } else if (object.overwrite && object.duplicate) { - return 'cannot use [overwrite] with [duplicate]'; } }, } diff --git a/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts index 04fc6969b95d7..241441998d8f5 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts @@ -15,15 +15,10 @@ export interface ResolveImportErrorsTestDefinition extends TestDefinition { request: { objects: Array<{ type: string; id: string; originId?: string }>; retries: Array< - { type: string; id: string } & ( - | { overwrite: true; idToOverwrite?: string } - | { duplicate: true } - | {} - ) + { type: string; id: string } & ({ overwrite: true; idToOverwrite?: string } | {}) >; }; overwrite: boolean; - duplicate: boolean; } export type ResolveImportErrorsTestSuite = TestSuite; export interface ResolveImportErrorsTestCase extends TestCase { @@ -79,14 +74,11 @@ export const TEST_CASES = Object.freeze({ */ const createRequest = ( { type, id, originId, idToOverwrite }: ResolveImportErrorsTestCase, - overwrite: boolean, - duplicate: boolean + overwrite: boolean ): ResolveImportErrorsTestDefinition['request'] => ({ objects: [{ type, id, ...(originId && { originId }) }], retries: overwrite ? [{ type, id, overwrite, ...(idToOverwrite && { idToOverwrite }) }] - : duplicate - ? [{ type, id, duplicate: true }] : [{ type, id }], }); @@ -99,7 +91,6 @@ export function resolveImportErrorsTestSuiteFactory( const expectResponseBody = ( testCases: ResolveImportErrorsTestCase | ResolveImportErrorsTestCase[], statusCode: 200 | 403, - duplicate: boolean, spaceId = SPACES.DEFAULT.spaceId ): ExpectResponseBody => async (response: Record) => { const testCaseArray = Array.isArray(testCases) ? testCases : [testCases]; @@ -130,7 +121,7 @@ export function resolveImportErrorsTestSuiteFactory( // Kibana created the object with a different ID than what was specified in the import // This can happen due to an unresolvable conflict (so the new ID will be random), or due to an inexact match (so the new ID will // be equal to the ID or originID of the existing object that it inexactly matched) - if (idToOverwrite && !duplicate) { + if (idToOverwrite) { expect(newId).to.be(idToOverwrite); } else { // the new ID was randomly generated @@ -162,7 +153,6 @@ export function resolveImportErrorsTestSuiteFactory( testCases: ResolveImportErrorsTestCase | ResolveImportErrorsTestCase[], forbidden: boolean, overwrite: boolean, - duplicate: boolean, options?: { spaceId?: string; singleRequest?: boolean; @@ -176,13 +166,12 @@ export function resolveImportErrorsTestSuiteFactory( // this ensures that multiple test cases of a single type will each result in a forbidden error return cases.map((x) => ({ title: getTestTitle(x, responseStatusCode), - request: createRequest(x, overwrite, duplicate), + request: createRequest(x, overwrite), responseStatusCode, responseBody: options?.responseBodyOverride || - expectResponseBody(x, responseStatusCode, duplicate, options?.spaceId), + expectResponseBody(x, responseStatusCode, options?.spaceId), overwrite, - duplicate, })); } // batch into a single request to save time during test execution @@ -190,7 +179,7 @@ export function resolveImportErrorsTestSuiteFactory( { title: getTestTitle(cases, responseStatusCode), request: cases - .map((x) => createRequest(x, overwrite, duplicate)) + .map((x) => createRequest(x, overwrite)) .reduce((acc, cur) => ({ objects: [...acc.objects, ...cur.objects], retries: [...acc.retries, ...cur.retries], @@ -198,9 +187,8 @@ export function resolveImportErrorsTestSuiteFactory( responseStatusCode, responseBody: options?.responseBodyOverride || - expectResponseBody(cases, responseStatusCode, duplicate, options?.spaceId), + expectResponseBody(cases, responseStatusCode, options?.spaceId), overwrite, - duplicate, }, ]; }; diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts index 1adbad321c656..842a994d17536 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts @@ -22,7 +22,7 @@ const { const { fail400, fail409 } = testCaseFailures; const newId = (condition?: boolean) => (condition !== false ? { successParam: 'newId' } : {}); -const createTestCases = (overwrite: boolean, duplicate: boolean, spaceId: string) => { +const createTestCases = (overwrite: boolean, spaceId: string) => { // for each permitted (non-403) outcome, if failure !== undefined then we expect // to receive an error; otherwise, we expect to receive a success result const singleNamespaceObject = @@ -32,38 +32,36 @@ const createTestCases = (overwrite: boolean, duplicate: boolean, spaceId: string ? CASES.SINGLE_NAMESPACE_SPACE_1 : CASES.SINGLE_NAMESPACE_SPACE_2; const group1Importable = [ - { ...singleNamespaceObject, ...fail409(!overwrite && !duplicate), ...newId(duplicate) }, - { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite && !duplicate), ...newId(duplicate) }, + { ...singleNamespaceObject, ...fail409(!overwrite) }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, ]; const group1NonImportable = [{ ...CASES.HIDDEN, ...fail400() }]; const group1All = [...group1Importable, ...group1NonImportable]; const group2 = [ { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, - ...fail409( - !overwrite && !duplicate && (spaceId === DEFAULT_SPACE_ID || spaceId === SPACE_1_ID) - ), - ...newId(duplicate || (spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID)), + ...fail409(!overwrite && (spaceId === DEFAULT_SPACE_ID || spaceId === SPACE_1_ID)), + ...newId(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), }, { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, - ...fail409(!overwrite && !duplicate && spaceId === SPACE_1_ID), - ...newId(duplicate || spaceId !== SPACE_1_ID), + ...fail409(!overwrite && spaceId === SPACE_1_ID), + ...newId(spaceId !== SPACE_1_ID), }, { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, - ...fail409(!overwrite && !duplicate && spaceId === SPACE_2_ID), - ...newId(duplicate || spaceId !== SPACE_2_ID), + ...fail409(!overwrite && spaceId === SPACE_2_ID), + ...newId(spaceId !== SPACE_2_ID), }, // all of the cases below represent imports that had an inexact match conflict or an ambiguous conflict // if we call _resolve_import_errors and don't specify overwrite or duplicate, each of these will not result in a conflict because they // will skip the preflight search results; so the objects will be created instead. - { ...CASES.CONFLICT_1A_OBJ, ...newId(overwrite || duplicate) }, // "ambiguous source" conflict; if overwrite=true, will overwrite 'conflict_1' - { ...CASES.CONFLICT_1B_OBJ, ...newId(duplicate) }, // "ambiguous source" conflict; if overwrite=true, will create a new object (since 'conflict_1a' is overwriting 'conflict_1') - { ...CASES.CONFLICT_2C_OBJ, ...newId(overwrite || duplicate) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2a' - { ...CASES.CONFLICT_2D_OBJ, ...newId(overwrite || duplicate) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2b' - { ...CASES.CONFLICT_3A_OBJ, ...newId(overwrite || duplicate) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3' - { ...CASES.CONFLICT_4_OBJ, ...newId(overwrite || duplicate) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a' + { ...CASES.CONFLICT_1A_OBJ, ...newId(overwrite) }, // "ambiguous source" conflict; if overwrite=true, will overwrite 'conflict_1' + CASES.CONFLICT_1B_OBJ, // "ambiguous source" conflict; if overwrite=true, will create a new object (since 'conflict_1a' is overwriting 'conflict_1') + { ...CASES.CONFLICT_2C_OBJ, ...newId(overwrite) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2a' + { ...CASES.CONFLICT_2D_OBJ, ...newId(overwrite) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2b' + { ...CASES.CONFLICT_3A_OBJ, ...newId(overwrite) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3' + { ...CASES.CONFLICT_4_OBJ, ...newId(overwrite) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a' ]; return { group1Importable, group1NonImportable, group1All, group2 }; }; @@ -78,66 +76,54 @@ export default function ({ getService }: FtrProviderContext) { esArchiver, supertest ); - const createTests = (overwrite: boolean, duplicate: boolean, spaceId: string) => { + const createTests = (overwrite: boolean, spaceId: string) => { const { group1Importable, group1NonImportable, group1All, group2 } = createTestCases( overwrite, - duplicate, spaceId ); const singleRequest = true; // use singleRequest to reduce execution time and/or test combined cases return { unauthorized: [ - createTestDefinitions(group1Importable, true, overwrite, duplicate, { spaceId }), - createTestDefinitions(group1NonImportable, false, overwrite, duplicate, { - spaceId, - singleRequest, - }), - createTestDefinitions(group1All, true, overwrite, duplicate, { + createTestDefinitions(group1Importable, true, overwrite, { spaceId }), + createTestDefinitions(group1NonImportable, false, overwrite, { spaceId, singleRequest }), + createTestDefinitions(group1All, true, overwrite, { spaceId, singleRequest, responseBodyOverride: expectForbidden(['globaltype', 'isolatedtype']), }), - createTestDefinitions(group2, true, overwrite, duplicate, { spaceId, singleRequest }), + createTestDefinitions(group2, true, overwrite, { spaceId, singleRequest }), ].flat(), authorized: [ - createTestDefinitions(group1All, false, overwrite, duplicate, { spaceId, singleRequest }), - createTestDefinitions(group2, false, overwrite, duplicate, { spaceId, singleRequest }), + createTestDefinitions(group1All, false, overwrite, { spaceId, singleRequest }), + createTestDefinitions(group2, false, overwrite, { spaceId, singleRequest }), ].flat(), }; }; describe('_resolve_import_errors', () => { - getTestScenarios([ - [false, false], - [false, true], - [true, false], - ]).securityAndSpaces.forEach(({ spaceId, users, modifier }) => { - const [overwrite, duplicate] = modifier!; - const suffix = - ` within the ${spaceId} space` + overwrite - ? ' with overwrite enabled' - : duplicate - ? ' with duplicate enabled' - : ''; - const { unauthorized, authorized } = createTests(overwrite, duplicate, spaceId); - const _addTests = (user: TestUser, tests: ResolveImportErrorsTestDefinition[]) => { - addTests(`${user.description}${suffix}`, { user, spaceId, tests }); - }; + getTestScenarios([false, true]).securityAndSpaces.forEach( + ({ spaceId, users, modifier: overwrite }) => { + const suffix = ` within the ${spaceId} space` + overwrite ? ' with overwrite enabled' : ''; + const { unauthorized, authorized } = createTests(overwrite!, spaceId); + const _addTests = (user: TestUser, tests: ResolveImportErrorsTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; - [ - users.noAccess, - users.legacyAll, - users.dualRead, - users.readGlobally, - users.readAtSpace, - users.allAtOtherSpace, - ].forEach((user) => { - _addTests(user, unauthorized); - }); - [users.dualAll, users.allGlobally, users.allAtSpace, users.superuser].forEach((user) => { - _addTests(user, authorized); - }); - }); + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.readAtSpace, + users.allAtOtherSpace, + ].forEach((user) => { + _addTests(user, unauthorized); + }); + [users.dualAll, users.allGlobally, users.allAtSpace, users.superuser].forEach((user) => { + _addTests(user, authorized); + }); + } + ); }); } diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts index 4d80225d1de52..51bb2308f5e87 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts @@ -16,34 +16,26 @@ import { const { fail400, fail409 } = testCaseFailures; const newId = (condition?: boolean) => (condition !== false ? { successParam: 'newId' } : {}); -const createTestCases = (overwrite: boolean, duplicate: boolean) => { +const createTestCases = (overwrite: boolean) => { // for each permitted (non-403) outcome, if failure !== undefined then we expect // to receive an error; otherwise, we expect to receive a success result const group1Importable = [ - { - ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, - ...fail409(!overwrite && !duplicate), - ...newId(duplicate), - }, - { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite && !duplicate), ...newId(duplicate) }, + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(!overwrite) }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, ]; const group1NonImportable = [{ ...CASES.HIDDEN, ...fail400() }]; const group1All = [...group1Importable, ...group1NonImportable]; const group2 = [ - { - ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, - ...fail409(!overwrite && !duplicate), - ...newId(duplicate), - }, + { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite) }, // all of the cases below represent imports that had an inexact match conflict or an ambiguous conflict // if we call _resolve_import_errors and don't specify overwrite or duplicate, each of these will not result in a conflict because they // will skip the preflight search results; so the objects will be created instead. - { ...CASES.CONFLICT_1A_OBJ, ...newId(overwrite || duplicate) }, // "ambiguous source" conflict; if overwrite=true, will overwrite 'conflict_1' - { ...CASES.CONFLICT_1B_OBJ, ...newId(duplicate) }, // "ambiguous source" conflict; if overwrite=true, will create a new object (since 'conflict_1a' is overwriting 'conflict_1') - { ...CASES.CONFLICT_2C_OBJ, ...newId(overwrite || duplicate) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2a' - { ...CASES.CONFLICT_2D_OBJ, ...newId(overwrite || duplicate) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2b' - { ...CASES.CONFLICT_3A_OBJ, ...newId(overwrite || duplicate) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3' - { ...CASES.CONFLICT_4_OBJ, ...newId(overwrite || duplicate) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a' + { ...CASES.CONFLICT_1A_OBJ, ...newId(overwrite) }, // "ambiguous source" conflict; if overwrite=true, will overwrite 'conflict_1' + CASES.CONFLICT_1B_OBJ, // "ambiguous source" conflict; if overwrite=true, will create a new object (since 'conflict_1a' is overwriting 'conflict_1') + { ...CASES.CONFLICT_2C_OBJ, ...newId(overwrite) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2a' + { ...CASES.CONFLICT_2D_OBJ, ...newId(overwrite) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2b' + { ...CASES.CONFLICT_3A_OBJ, ...newId(overwrite) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3' + { ...CASES.CONFLICT_4_OBJ, ...newId(overwrite) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a' ]; return { group1Importable, group1NonImportable, group1All, group2 }; }; @@ -58,44 +50,32 @@ export default function ({ getService }: FtrProviderContext) { esArchiver, supertest ); - const createTests = (overwrite: boolean, duplicate: boolean) => { - const { group1Importable, group1NonImportable, group1All, group2 } = createTestCases( - overwrite, - duplicate - ); + const createTests = (overwrite: boolean) => { + const { group1Importable, group1NonImportable, group1All, group2 } = createTestCases(overwrite); // use singleRequest to reduce execution time and/or test combined cases return { unauthorized: [ - createTestDefinitions(group1Importable, true, overwrite, duplicate), - createTestDefinitions(group1NonImportable, false, overwrite, duplicate, { + createTestDefinitions(group1Importable, true, overwrite), + createTestDefinitions(group1NonImportable, false, overwrite, { singleRequest: true, }), - createTestDefinitions(group1All, true, overwrite, duplicate, { + createTestDefinitions(group1All, true, overwrite, { singleRequest: true, responseBodyOverride: expectForbidden(['globaltype', 'isolatedtype']), }), - createTestDefinitions(group2, true, overwrite, duplicate, { singleRequest: true }), + createTestDefinitions(group2, true, overwrite, { singleRequest: true }), ].flat(), authorized: [ - createTestDefinitions(group1All, false, overwrite, duplicate, { singleRequest: true }), - createTestDefinitions(group2, false, overwrite, duplicate, { singleRequest: true }), + createTestDefinitions(group1All, false, overwrite, { singleRequest: true }), + createTestDefinitions(group2, false, overwrite, { singleRequest: true }), ].flat(), }; }; describe('_resolve_import_errors', () => { - getTestScenarios([ - [false, false], - [false, true], - [true, false], - ]).security.forEach(({ users, modifier }) => { - const [overwrite, duplicate] = modifier!; - const suffix = overwrite - ? ' with overwrite enabled' - : duplicate - ? ' with duplicate enabled' - : ''; - const { unauthorized, authorized } = createTests(overwrite, duplicate); + getTestScenarios([false, true]).security.forEach(({ users, modifier: overwrite }) => { + const suffix = overwrite ? ' with overwrite enabled' : ''; + const { unauthorized, authorized } = createTests(overwrite!); const _addTests = (user: TestUser, tests: ResolveImportErrorsTestDefinition[]) => { addTests(`${user.description}${suffix}`, { user, tests }); }; diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts index f06c15775a5c6..06d1c88457b05 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts @@ -20,7 +20,7 @@ const { const { fail400, fail409 } = testCaseFailures; const newId = (condition?: boolean) => (condition !== false ? { successParam: 'newId' } : {}); -const createTestCases = (overwrite: boolean, duplicate: boolean, spaceId: string) => { +const createTestCases = (overwrite: boolean, spaceId: string) => { // for each outcome, if failure !== undefined then we expect to receive // an error; otherwise, we expect to receive a success result const singleNamespaceObject = @@ -30,35 +30,33 @@ const createTestCases = (overwrite: boolean, duplicate: boolean, spaceId: string ? CASES.SINGLE_NAMESPACE_SPACE_1 : CASES.SINGLE_NAMESPACE_SPACE_2; return [ - { ...singleNamespaceObject, ...fail409(!overwrite && !duplicate), ...newId(duplicate) }, + { ...singleNamespaceObject, ...fail409(!overwrite) }, { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, - ...fail409( - !overwrite && !duplicate && (spaceId === DEFAULT_SPACE_ID || spaceId === SPACE_1_ID) - ), - ...newId(duplicate || (spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID)), + ...fail409(!overwrite && (spaceId === DEFAULT_SPACE_ID || spaceId === SPACE_1_ID)), + ...newId(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), }, { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, - ...fail409(!overwrite && !duplicate && spaceId === SPACE_1_ID), - ...newId(duplicate || spaceId !== SPACE_1_ID), + ...fail409(!overwrite && spaceId === SPACE_1_ID), + ...newId(spaceId !== SPACE_1_ID), }, { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, - ...fail409(!overwrite && !duplicate && spaceId === SPACE_2_ID), - ...newId(duplicate || spaceId !== SPACE_2_ID), + ...fail409(!overwrite && spaceId === SPACE_2_ID), + ...newId(spaceId !== SPACE_2_ID), }, - { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite && !duplicate), ...newId(duplicate) }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, { ...CASES.HIDDEN, ...fail400() }, // all of the cases below represent imports that had an inexact match conflict or an ambiguous conflict // if we call _resolve_import_errors and don't specify overwrite or duplicate, each of these will not result in a conflict because they // will skip the preflight search results; so the objects will be created instead. - { ...CASES.CONFLICT_1A_OBJ, ...newId(overwrite || duplicate) }, // "ambiguous source" conflict; if overwrite=true, will overwrite 'conflict_1' - { ...CASES.CONFLICT_1B_OBJ, ...newId(duplicate) }, // "ambiguous source" conflict; if overwrite=true, will create a new object (since 'conflict_1a' is overwriting 'conflict_1') - { ...CASES.CONFLICT_2C_OBJ, ...newId(overwrite || duplicate) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2a' - { ...CASES.CONFLICT_2D_OBJ, ...newId(overwrite || duplicate) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2b' - { ...CASES.CONFLICT_3A_OBJ, ...newId(overwrite || duplicate) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3' - { ...CASES.CONFLICT_4_OBJ, ...newId(overwrite || duplicate) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a' + { ...CASES.CONFLICT_1A_OBJ, ...newId(overwrite) }, // "ambiguous source" conflict; if overwrite=true, will overwrite 'conflict_1' + CASES.CONFLICT_1B_OBJ, // "ambiguous source" conflict; if overwrite=true, will create a new object (since 'conflict_1a' is overwriting 'conflict_1') + { ...CASES.CONFLICT_2C_OBJ, ...newId(overwrite) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2a' + { ...CASES.CONFLICT_2D_OBJ, ...newId(overwrite) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2b' + { ...CASES.CONFLICT_3A_OBJ, ...newId(overwrite) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3' + { ...CASES.CONFLICT_4_OBJ, ...newId(overwrite) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a' ]; }; @@ -72,27 +70,18 @@ export default function ({ getService }: FtrProviderContext) { esArchiver, supertest ); - const createTests = (overwrite: boolean, duplicate: boolean, spaceId: string) => { - const testCases = createTestCases(overwrite, duplicate, spaceId); - return createTestDefinitions(testCases, false, overwrite, duplicate, { + const createTests = (overwrite: boolean, spaceId: string) => { + const testCases = createTestCases(overwrite, spaceId); + return createTestDefinitions(testCases, false, overwrite, { spaceId, singleRequest: true, }); }; describe('_resolve_import_errors', () => { - getTestScenarios([ - [false, false], - [false, true], - [true, false], - ]).spaces.forEach(({ spaceId, modifier }) => { - const [overwrite, duplicate] = modifier!; - const suffix = overwrite - ? ' with overwrite enabled' - : duplicate - ? ' with duplicate enabled' - : ''; - const tests = createTests(overwrite, duplicate, spaceId); + getTestScenarios([false, true]).spaces.forEach(({ spaceId, modifier: overwrite }) => { + const suffix = overwrite ? ' with overwrite enabled' : ''; + const tests = createTests(overwrite!, spaceId); addTests(`within the ${spaceId} space${suffix}`, { spaceId, tests }); }); }); diff --git a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts index e5a3830759db1..47161fc238f8a 100644 --- a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts +++ b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts @@ -32,7 +32,7 @@ interface ResolveCopyToSpaceTests { withoutReferencesOverwriting: ResolveCopyToSpaceTest; withoutReferencesNotOverwriting: ResolveCopyToSpaceTest; nonExistentSpace: ResolveCopyToSpaceTest; - multiNamespaceTestCases: (overwrite: boolean) => ResolveCopyToSpaceMultiNamespaceTest[]; + multiNamespaceTestCases: () => ResolveCopyToSpaceMultiNamespaceTest[]; } interface ResolveCopyToSpaceTestDefinition { @@ -277,20 +277,16 @@ export function resolveCopyToSpaceConflictsSuite( const createMultiNamespaceTestCases = ( spaceId: string, outcome: 'authorized' | 'unauthorizedRead' | 'unauthorizedWrite' | 'noAccess' = 'authorized' - ) => (overwrite: boolean): ResolveCopyToSpaceMultiNamespaceTest[] => { + ) => (): ResolveCopyToSpaceMultiNamespaceTest[] => { // the status code of the HTTP response differs depending on the error type // a 403 error actually comes back as an HTTP 200 response const statusCode = outcome === 'noAccess' ? 404 : 200; const type = 'sharedtype'; - const v4 = new RegExp(/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i); const exactMatchId = 'all_spaces'; const inexactMatchId = `conflict_1_${spaceId}`; const ambiguousConflictId = `conflict_2_${spaceId}`; - const createRetries = ( - overwriteRetry: Record, - duplicateRetry: Record - ) => ({ space_2: [overwrite ? overwriteRetry : duplicateRetry] }); + const createRetries = (overwriteRetry: Record) => ({ space_2: [overwriteRetry] }); const getResult = (response: TestResponse) => (response.body as CopyResponse).space_2; const expectForbiddenResponse = (response: TestResponse) => { expect(response.body).to.eql({ @@ -308,24 +304,14 @@ export function resolveCopyToSpaceConflictsSuite( expect(success).to.eql(true); expect(successCount).to.eql(1); expect(errors).to.be(undefined); - if (overwrite) { - expect(successResults).to.eql([{ type, id, ...(newId && { newId }) }]); - } else { - // duplicate - const duplicateId = successResults![0].newId; - expect(duplicateId).to.match(v4); - expect(successResults).to.eql([{ type, id, newId: duplicateId }]); - } + expect(successResults).to.eql([{ type, id, ...(newId && { newId }) }]); }; return [ { testTitle: 'copying with an exact match conflict', objects: [{ type, id: exactMatchId }], - retries: createRetries( - { type, id: exactMatchId, overwrite }, - { type, id: exactMatchId, duplicate: true } - ), + retries: createRetries({ type, id: exactMatchId, overwrite: true }), statusCode, response: async (response: TestResponse) => { if (outcome === 'authorized') { @@ -341,10 +327,12 @@ export function resolveCopyToSpaceConflictsSuite( { testTitle: 'copying with an inexact match conflict', objects: [{ type, id: inexactMatchId }], - retries: createRetries( - { type, id: inexactMatchId, overwrite, idToOverwrite: 'conflict_1_space_2' }, - { type, id: inexactMatchId, duplicate: true } - ), + retries: createRetries({ + type, + id: inexactMatchId, + overwrite: true, + idToOverwrite: 'conflict_1_space_2', + }), statusCode, response: async (response: TestResponse) => { if (outcome === 'authorized') { @@ -360,10 +348,12 @@ export function resolveCopyToSpaceConflictsSuite( { testTitle: 'copying with an ambiguous conflict', objects: [{ type, id: ambiguousConflictId }], - retries: createRetries( - { type, id: ambiguousConflictId, overwrite, idToOverwrite: 'conflict_2_space_2' }, - { type, id: ambiguousConflictId, duplicate: true } - ), + retries: createRetries({ + type, + id: ambiguousConflictId, + overwrite: true, + idToOverwrite: 'conflict_2_space_2', + }), statusCode, response: async (response: TestResponse) => { if (outcome === 'authorized') { @@ -472,23 +462,20 @@ export function resolveCopyToSpaceConflictsSuite( }); }); - [false, true].forEach((overwrite) => { - const includeReferences = false; - const retryType = overwrite ? 'overwrite' : 'duplicate'; - describe(`multi-namespace types with "${retryType}" retry`, () => { - before(() => esArchiver.load('saved_objects/spaces')); - after(() => esArchiver.unload('saved_objects/spaces')); - - const testCases = tests.multiNamespaceTestCases(overwrite); - testCases.forEach(({ testTitle, objects, retries, statusCode, response }) => { - it(`should return ${statusCode} when ${testTitle}`, async () => { - return supertestWithoutAuth - .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`) - .auth(user.username, user.password) - .send({ objects, includeReferences, retries }) - .expect(statusCode) - .then(response); - }); + const includeReferences = false; + describe(`multi-namespace types with "overwrite" retry`, () => { + before(() => esArchiver.load('saved_objects/spaces')); + after(() => esArchiver.unload('saved_objects/spaces')); + + const testCases = tests.multiNamespaceTestCases(); + testCases.forEach(({ testTitle, objects, retries, statusCode, response }) => { + it(`should return ${statusCode} when ${testTitle}`, async () => { + return supertestWithoutAuth + .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`) + .auth(user.username, user.password) + .send({ objects, includeReferences, retries }) + .expect(statusCode) + .then(response); }); }); }); From 19bdeeadcb6d0b9f99a1406fd1d0ca7907b0287d Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 15 Jun 2020 15:22:59 -0400 Subject: [PATCH 16/55] Address first round of PR review feedback --- .../import/check_conflicts.test.ts | 11 +++++ .../saved_objects/import/check_conflicts.ts | 47 +++++++++++-------- .../import/collect_saved_objects.test.ts | 2 +- .../import/collect_saved_objects.ts | 6 ++- .../import/create_saved_objects.ts | 21 ++++++--- .../import/resolve_import_errors.test.ts | 4 +- .../import/resolve_import_errors.ts | 2 +- .../import/validate_retries.test.ts | 6 ++- .../saved_objects/import/validate_retries.ts | 10 ++-- .../saved_objects/service/lib/repository.ts | 2 + .../lib/search_dsl/query_params.test.ts | 15 ++++++ .../service/lib/search_dsl/query_params.ts | 8 +++- 12 files changed, 95 insertions(+), 39 deletions(-) diff --git a/src/core/server/saved_objects/import/check_conflicts.test.ts b/src/core/server/saved_objects/import/check_conflicts.test.ts index 9481292b326ae..be478ad6a073a 100644 --- a/src/core/server/saved_objects/import/check_conflicts.test.ts +++ b/src/core/server/saved_objects/import/check_conflicts.test.ts @@ -461,6 +461,17 @@ describe('#getImportIdMapForRetries', () => { return { type, id, overwrite: true, idToOverwrite, replaceReferences: [] }; }; + test('throws an error if retry is not found for an object', async () => { + const obj1 = createObject(MULTI_NS_TYPE, 'id-1'); + const obj2 = createObject(MULTI_NS_TYPE, 'id-2'); + const retries = [createOverwriteRetry(obj1)]; + const options = setupOptions(retries); + + expect(() => + getImportIdMapForRetries([obj1, obj2], options) + ).toThrowErrorMatchingInlineSnapshot(`"Retry was expected for \\"multi:id-2\\" but not found"`); + }); + test('returns expected results', async () => { const obj1 = createObject(OTHER_TYPE, 'id-1'); const obj2 = createObject(OTHER_TYPE, 'id-2'); diff --git a/src/core/server/saved_objects/import/check_conflicts.ts b/src/core/server/saved_objects/import/check_conflicts.ts index 26f9795a46e80..ee720a0326bbd 100644 --- a/src/core/server/saved_objects/import/check_conflicts.ts +++ b/src/core/server/saved_objects/import/check_conflicts.ts @@ -17,8 +17,6 @@ * under the License. */ -/* eslint-disable @typescript-eslint/consistent-type-definitions */ - import { SavedObject, SavedObjectsClientContract, @@ -38,25 +36,36 @@ interface GetImportIdMapForRetriesOptions { retries: SavedObjectsImportRetry[]; } -type InexactMatch = { +interface InexactMatch { object: SavedObject; destinations: Array<{ id: string; title?: string; updatedAt?: string }>; -}; -type Left = { tag: 'left'; value: InexactMatch }; -type Right = { tag: 'right'; value: SavedObject }; +} +interface Left { + tag: 'left'; + value: InexactMatch; +} +interface Right { + tag: 'right'; + value: SavedObject; +} type Either = Left | Right; const isLeft = (object: Either): object is Left => object.tag === 'left'; const createQueryTerm = (input: string) => input.replace(/\\/g, '\\\\').replace(/\"/g, '\\"'); const createQuery = (type: string, id: string, rawIdPrefix: string) => `"${createQueryTerm(`${rawIdPrefix}${type}:${id}`)}" | "${createQueryTerm(id)}"`; -const getAmbiguousConflicts = (objects: Array>) => +const transformObjectsToAmbiguousConflictFields = ( + objects: Array> +) => objects .map(({ id, attributes, updated_at: updatedAt }) => ({ id, title: attributes?.title, updatedAt, })) + // Sort for two reasons: 1. consumers may want to identify multiple errors that have the same sources (by stringifying the `sources` + // array of each object they can be compared), and 2. it will be a less confusing experience for end-users if several ambiguous + // conflicts that share the same destinations all show those destinations in the same order. .sort((a, b) => (a.id > b.id ? 1 : b.id > a.id ? -1 : 0)); const getAmbiguousConflictSourceKey = ({ object }: InexactMatch) => `${object.type}:${object.originId || object.id}`; @@ -99,7 +108,7 @@ const checkConflict = async ( } // This is an "inexact match" so far; filter the conflict destination(s) to exclude any that exactly match other objects we are importing. const objects = savedObjects.filter((obj) => !importIds.has(`${obj.type}:${obj.id}`)); - const destinations = getAmbiguousConflicts(objects); + const destinations = transformObjectsToAmbiguousConflictFields(objects); if (destinations.length === 0) { // No conflict destinations remain after filtering, so this is a "no match" result. return { tag: 'right', value: object }; @@ -151,7 +160,9 @@ export async function checkConflicts( return; } const key = getAmbiguousConflictSourceKey(result.value); - const sources = getAmbiguousConflicts(ambiguousConflictSourcesMap.get(key)!); + const sources = transformObjectsToAmbiguousConflictFields( + ambiguousConflictSourcesMap.get(key)! + ); const { object, destinations } = result.value; const { type, id, attributes } = object; if (sources.length === 1 && destinations.length === 1) { @@ -186,7 +197,7 @@ export async function checkConflicts( /** * Assume that all objects exist in the `retries` map (due to filtering at the beginnning of `resolveSavedObjectsImportErrors`). */ -export async function getImportIdMapForRetries( +export function getImportIdMapForRetries( objects: Array>, options: GetImportIdMapForRetriesOptions ) { @@ -200,16 +211,12 @@ export async function getImportIdMapForRetries( objects.forEach(({ type, id }) => { const retry = retryMap.get(`${type}:${id}`); - if (retry) { - const { overwrite, idToOverwrite } = retry; - if ( - overwrite && - idToOverwrite && - idToOverwrite !== id && - typeRegistry.isMultiNamespace(type) - ) { - importIdMap.set(`${type}:${id}`, { id: idToOverwrite }); - } + if (!retry) { + throw new Error(`Retry was expected for "${type}:${id}" but not found`); + } + const { overwrite, idToOverwrite } = retry; + if (overwrite && idToOverwrite && idToOverwrite !== id && typeRegistry.isMultiNamespace(type)) { + importIdMap.set(`${type}:${id}`, { id: idToOverwrite }); } }); diff --git a/src/core/server/saved_objects/import/collect_saved_objects.test.ts b/src/core/server/saved_objects/import/collect_saved_objects.test.ts index b2c62471b13fc..6f8a98e7e3216 100644 --- a/src/core/server/saved_objects/import/collect_saved_objects.test.ts +++ b/src/core/server/saved_objects/import/collect_saved_objects.test.ts @@ -121,7 +121,7 @@ describe('collectSavedObjects()', () => { } catch ({ isBoom, message }) { expect(isBoom).toBe(true); expect(message).toMatchInlineSnapshot( - `"Non-unique import objects detected: [type1:id1,type2:id2]"` + `"Non-unique import objects detected: [type1:id1,type2:id2]: Bad Request"` ); } }); diff --git a/src/core/server/saved_objects/import/collect_saved_objects.ts b/src/core/server/saved_objects/import/collect_saved_objects.ts index ef635bf27e7c8..6983da0231b25 100644 --- a/src/core/server/saved_objects/import/collect_saved_objects.ts +++ b/src/core/server/saved_objects/import/collect_saved_objects.ts @@ -17,7 +17,6 @@ * under the License. */ -import Boom from 'boom'; import { Readable } from 'stream'; import { createConcatStream, @@ -29,6 +28,7 @@ import { SavedObject } from '../types'; import { createLimitStream } from './create_limit_stream'; import { SavedObjectsImportError } from './types'; import { getNonUniqueEntries } from './utilities'; +import { SavedObjectsErrorHelpers } from '..'; interface CollectSavedObjectsOptions { readStream: Readable; @@ -74,7 +74,9 @@ export async function collectSavedObjects({ // throw a BadRequest error if we see the same import object type/id more than once const nonUniqueEntries = getNonUniqueEntries(entries); if (nonUniqueEntries.length > 0) { - throw Boom.badRequest(`Non-unique import objects detected: [${nonUniqueEntries.join()}]`); + throw SavedObjectsErrorHelpers.createBadRequestError( + `Non-unique import objects detected: [${nonUniqueEntries.join()}]` + ); } return { diff --git a/src/core/server/saved_objects/import/create_saved_objects.ts b/src/core/server/saved_objects/import/create_saved_objects.ts index e847f603e50f2..a4eefa343e311 100644 --- a/src/core/server/saved_objects/import/create_saved_objects.ts +++ b/src/core/server/saved_objects/import/create_saved_objects.ts @@ -17,8 +17,6 @@ * under the License. */ -/* eslint-disable @typescript-eslint/consistent-type-definitions */ - import { v4 as uuidv4 } from 'uuid'; import { SavedObject, SavedObjectsClientContract, SavedObjectsImportError } from '../types'; import { extractErrors } from './extract_errors'; @@ -34,9 +32,18 @@ interface CreateSavedObjectsResult { errors: SavedObjectsImportError[]; } -type UnresolvableConflict = { retryIndex: number; retryObject: SavedObject }; -type Left = { tag: 'left'; value: UnresolvableConflict }; -type Right = { tag: 'right'; value: SavedObject }; +interface UnresolvableConflict { + retryIndex: number; + retryObject: SavedObject; +} +interface Left { + tag: 'left'; + value: UnresolvableConflict; +} +interface Right { + tag: 'right'; + value: SavedObject; +} type Either = Left | Right; const isLeft = (object: Either): object is Left => object.tag === 'left'; const isRight = (object: Either): object is Right => object.tag === 'right'; @@ -48,8 +55,8 @@ const isUnresolvableConflict = (object: SavedObject) => * This function abstracts the bulk creation of import objects for two purposes: * 1. The import ID map that was generated by the `checkConflicts` function should dictate the IDs of the objects we create. * 2. Any object create attempt that results in an unresolvable conflict should have its ID regenerated and retry create. This way, when an - * object with a "multi-namespace" type is exported from one space and imported to another, it does not result in an error, but instead - * a new object is created. + * object with a "multi-namespace" type is exported from one namespace and imported to another, it does not result in an error, but + * instead a new object is created. */ export const createSavedObjects = async ( objects: Array>, 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 6f401d10f87db..20644becb7f18 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 @@ -58,7 +58,7 @@ describe('#importSavedObjectsFromStream', () => { getMockFn(createObjectsFilter).mockReturnValue(() => false); getMockFn(collectSavedObjects).mockResolvedValue({ errors: [], collectedObjects: [] }); getMockFn(validateReferences).mockResolvedValue({ errors: [], filteredObjects: [] }); - getMockFn(getImportIdMapForRetries).mockResolvedValue(new Map()); + getMockFn(getImportIdMapForRetries).mockReturnValue(new Map()); getMockFn(splitOverwrites).mockReturnValue({ objectsToOverwrite: [], objectsToNotOverwrite: [], @@ -197,7 +197,7 @@ describe('#importSavedObjectsFromStream', () => { test('creates saved objects', async () => { const options = setupOptions(); const importIdMap = new Map(); - getMockFn(getImportIdMapForRetries).mockResolvedValue(importIdMap); + getMockFn(getImportIdMapForRetries).mockReturnValue(importIdMap); const objectsToOverwrite = [createObject()]; const objectsToNotOverwrite = [createObject()]; getMockFn(splitOverwrites).mockReturnValue({ objectsToOverwrite, objectsToNotOverwrite }); 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 bfecd04a4c9d3..97a1fec075dc2 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.ts @@ -96,7 +96,7 @@ export async function resolveSavedObjectsImportErrors({ errorAccumulator = [...errorAccumulator, ...referenceErrors]; // Check multi-namespace object types for regular conflicts and ambiguous conflicts - const importIdMap = await getImportIdMapForRetries(filteredObjects, { typeRegistry, retries }); + const importIdMap = getImportIdMapForRetries(filteredObjects, { typeRegistry, retries }); // Bulk create in two batches, overwrites and non-overwrites let successResults: Array<{ type: string; id: string; newId?: string }> = []; diff --git a/src/core/server/saved_objects/import/validate_retries.test.ts b/src/core/server/saved_objects/import/validate_retries.test.ts index 4272c6a33cb2d..09a10efea13cc 100644 --- a/src/core/server/saved_objects/import/validate_retries.test.ts +++ b/src/core/server/saved_objects/import/validate_retries.test.ts @@ -70,7 +70,9 @@ describe('#validateRetries', () => { validateRetries([]); } catch ({ isBoom, message }) { expect(isBoom).toBe(true); - expect(message).toMatchInlineSnapshot(`"Non-unique retry objects: [type1:id1,type2:id2]"`); + expect(message).toMatchInlineSnapshot( + `"Non-unique retry objects: [type1:id1,type2:id2]: Bad Request"` + ); } }); @@ -83,7 +85,7 @@ describe('#validateRetries', () => { } catch ({ isBoom, message }) { expect(isBoom).toBe(true); expect(message).toMatchInlineSnapshot( - `"Non-unique retry overwrites: [type1:id1,type2:id2]"` + `"Non-unique retry overwrites: [type1:id1,type2:id2]: Bad Request"` ); } }); diff --git a/src/core/server/saved_objects/import/validate_retries.ts b/src/core/server/saved_objects/import/validate_retries.ts index 3ea143c0711c9..275f2ca6b746a 100644 --- a/src/core/server/saved_objects/import/validate_retries.ts +++ b/src/core/server/saved_objects/import/validate_retries.ts @@ -17,14 +17,16 @@ * under the License. */ -import Boom from 'boom'; import { SavedObjectsImportRetry } from './types'; import { getNonUniqueEntries } from './utilities'; +import { SavedObjectsErrorHelpers } from '..'; export const validateRetries = (retries: SavedObjectsImportRetry[]) => { const nonUniqueRetryObjects = getNonUniqueEntries(retries); if (nonUniqueRetryObjects.length > 0) { - throw Boom.badRequest(`Non-unique retry objects: [${nonUniqueRetryObjects.join()}]`); + throw SavedObjectsErrorHelpers.createBadRequestError( + `Non-unique retry objects: [${nonUniqueRetryObjects.join()}]` + ); } const overwriteEntries = retries @@ -32,6 +34,8 @@ export const validateRetries = (retries: SavedObjectsImportRetry[]) => { .map(({ type, idToOverwrite }) => ({ type, id: idToOverwrite! })); const nonUniqueRetryOverwrites = getNonUniqueEntries(overwriteEntries); if (nonUniqueRetryOverwrites.length > 0) { - throw Boom.badRequest(`Non-unique retry overwrites: [${nonUniqueRetryOverwrites.join()}]`); + throw SavedObjectsErrorHelpers.createBadRequestError( + `Non-unique retry overwrites: [${nonUniqueRetryOverwrites.join()}]` + ); } }; diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 2d40ec046b0c6..ea91ee7dbfbc4 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -1198,6 +1198,8 @@ export class SavedObjectsRepository { const { type, id, namespaces, documentToSave, esRequestIndex } = expectedResult.value; const response = bulkUpdateResponse.items[esRequestIndex]; + // When a bulk update operation is completed, any fields specified in `_sourceIncludes` will be found in the "get" value of the + // returned object. We need to retrieve the `originId` if it exists so we can return it to the consumer. const { error, _seq_no: seqNo, _primary_term: primaryTerm, get } = Object.values( response )[0] as any; 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 14967a785c14b..7b66f650c11c6 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 @@ -321,6 +321,21 @@ describe('#getQueryParams', () => { expectResult(result, expect.objectContaining({ fields })); }; + it('throws an error if a raw search field contains a "." character', () => { + expect(() => + getQueryParams({ + mappings, + registry, + type: undefined, + search, + searchFields: undefined, + rawSearchFields: ['foo', 'bar.baz'], + }) + ).toThrowErrorMatchingInlineSnapshot( + `"rawSearchFields entry \\"bar.baz\\" is invalid: cannot contain \\".\\" character"` + ); + }); + it('includes lenient flag and all fields when `searchFields` and `rawSearchFields` are not specified', () => { const result = getQueryParams({ mappings, diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts index 269fc8899ea2e..663b1f056b353 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts @@ -53,7 +53,13 @@ function getFieldsForTypes( }; } - let fields: string[] = rawSearchFields; + let fields = [...rawSearchFields]; + fields.forEach((field) => { + if (field.indexOf('.') !== -1) { + throw new Error(`rawSearchFields entry "${field}" is invalid: cannot contain "." character`); + } + }); + for (const field of searchFields) { fields = fields.concat(types.map((prefix) => `${prefix}.${field}`)); } From 16fc13a2a37014e8ff3bc172e0ec8b1a3ae782f5 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Thu, 18 Jun 2020 10:18:47 -0400 Subject: [PATCH 17/55] Add `checkConflicts` API to Saved Objects Client This is only exposed on the server-side, as we will need to use it for imports. --- ...na-plugin-core-public.savedobject.error.md | 7 +- .../kibana-plugin-core-public.savedobject.md | 2 +- .../core/server/kibana-plugin-core-server.md | 2 + ...na-plugin-core-server.savedobject.error.md | 7 +- .../kibana-plugin-core-server.savedobject.md | 2 +- ...ver.savedobjectscheckconflictsobject.id.md | 11 ++ ...server.savedobjectscheckconflictsobject.md | 20 +++ ...r.savedobjectscheckconflictsobject.type.md | 11 ++ ...vedobjectscheckconflictsresponse.errors.md | 15 +++ ...rver.savedobjectscheckconflictsresponse.md | 19 +++ ...erver.savedobjectsclient.checkconflicts.md | 25 ++++ ...a-plugin-core-server.savedobjectsclient.md | 1 + ...r.savedobjectsrepository.checkconflicts.md | 25 ++++ ...ugin-core-server.savedobjectsrepository.md | 1 + src/core/public/public.api.md | 9 +- src/core/server/index.ts | 2 + .../service/lib/repository.mock.ts | 1 + .../service/lib/repository.test.js | 117 ++++++++++++++++++ .../saved_objects/service/lib/repository.ts | 47 +++++++ .../service/saved_objects_client.mock.ts | 1 + .../service/saved_objects_client.test.js | 15 +++ .../service/saved_objects_client.ts | 36 ++++++ src/core/server/saved_objects/types.ts | 1 + src/core/server/server.api.md | 29 ++++- src/core/types/saved_objects.ts | 14 ++- ...ypted_saved_objects_client_wrapper.test.ts | 13 ++ .../encrypted_saved_objects_client_wrapper.ts | 8 ++ ...ecure_saved_objects_client_wrapper.test.ts | 30 +++++ .../secure_saved_objects_client_wrapper.ts | 14 +++ .../lib/copy_to_spaces/copy_to_spaces.test.ts | 3 + .../resolve_copy_conflicts.test.ts | 3 + .../spaces_saved_objects_client.test.ts | 28 +++++ .../spaces_saved_objects_client.ts | 20 +++ 33 files changed, 507 insertions(+), 32 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectscheckconflictsobject.id.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectscheckconflictsobject.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectscheckconflictsobject.type.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectscheckconflictsresponse.errors.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectscheckconflictsresponse.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.checkconflicts.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.checkconflicts.md diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobject.error.md b/docs/development/core/public/kibana-plugin-core-public.savedobject.error.md index 3c089cf8c7c91..ab9a611fc3a5c 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobject.error.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobject.error.md @@ -7,10 +7,5 @@ Signature: ```typescript -error?: { - error: string; - message: string; - statusCode: number; - metadata?: Record; - }; +error?: SavedObjectError; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobject.md b/docs/development/core/public/kibana-plugin-core-public.savedobject.md index 0dda7fa11637d..eb6059747426d 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobject.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobject.md @@ -15,7 +15,7 @@ export interface SavedObject | Property | Type | Description | | --- | --- | --- | | [attributes](./kibana-plugin-core-public.savedobject.attributes.md) | T | The data for a Saved Object is stored as an object in the attributes property. | -| [error](./kibana-plugin-core-public.savedobject.error.md) | {
error: string;
message: string;
statusCode: number;
metadata?: Record<string, unknown>;
} | | +| [error](./kibana-plugin-core-public.savedobject.error.md) | SavedObjectError | | | [id](./kibana-plugin-core-public.savedobject.id.md) | string | The ID of this Saved Object, guaranteed to be unique for all objects of the same type | | [migrationVersion](./kibana-plugin-core-public.savedobject.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | | [namespaces](./kibana-plugin-core-public.savedobject.namespaces.md) | string[] | Namespace(s) that this saved object exists in. This attribute is only used for multi-namespace saved object types. | diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 37d2eca3ed4c9..454f99978118b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -145,6 +145,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsBulkUpdateObject](./kibana-plugin-core-server.savedobjectsbulkupdateobject.md) | | | [SavedObjectsBulkUpdateOptions](./kibana-plugin-core-server.savedobjectsbulkupdateoptions.md) | | | [SavedObjectsBulkUpdateResponse](./kibana-plugin-core-server.savedobjectsbulkupdateresponse.md) | | +| [SavedObjectsCheckConflictsObject](./kibana-plugin-core-server.savedobjectscheckconflictsobject.md) | | +| [SavedObjectsCheckConflictsResponse](./kibana-plugin-core-server.savedobjectscheckconflictsresponse.md) | | | [SavedObjectsClientProviderOptions](./kibana-plugin-core-server.savedobjectsclientprovideroptions.md) | Options to control the creation of the Saved Objects Client. | | [SavedObjectsClientWrapperOptions](./kibana-plugin-core-server.savedobjectsclientwrapperoptions.md) | Options passed to each SavedObjectsClientWrapperFactory to aid in creating the wrapper instance. | | [SavedObjectsComplexFieldMapping](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.md) | See [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) for documentation. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobject.error.md b/docs/development/core/server/kibana-plugin-core-server.savedobject.error.md index 2182b2970a3eb..ef42053e38626 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobject.error.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobject.error.md @@ -7,10 +7,5 @@ Signature: ```typescript -error?: { - error: string; - message: string; - statusCode: number; - metadata?: Record; - }; +error?: SavedObjectError; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobject.md index 904b667210144..5aefc55736cd1 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobject.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobject.md @@ -15,7 +15,7 @@ export interface SavedObject | Property | Type | Description | | --- | --- | --- | | [attributes](./kibana-plugin-core-server.savedobject.attributes.md) | T | The data for a Saved Object is stored as an object in the attributes property. | -| [error](./kibana-plugin-core-server.savedobject.error.md) | {
error: string;
message: string;
statusCode: number;
metadata?: Record<string, unknown>;
} | | +| [error](./kibana-plugin-core-server.savedobject.error.md) | SavedObjectError | | | [id](./kibana-plugin-core-server.savedobject.id.md) | string | The ID of this Saved Object, guaranteed to be unique for all objects of the same type | | [migrationVersion](./kibana-plugin-core-server.savedobject.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | | [namespaces](./kibana-plugin-core-server.savedobject.namespaces.md) | string[] | Namespace(s) that this saved object exists in. This attribute is only used for multi-namespace saved object types. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscheckconflictsobject.id.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscheckconflictsobject.id.md new file mode 100644 index 0000000000000..2b7cd5cc486a8 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscheckconflictsobject.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCheckConflictsObject](./kibana-plugin-core-server.savedobjectscheckconflictsobject.md) > [id](./kibana-plugin-core-server.savedobjectscheckconflictsobject.id.md) + +## SavedObjectsCheckConflictsObject.id property + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscheckconflictsobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscheckconflictsobject.md new file mode 100644 index 0000000000000..c327cc4a20551 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscheckconflictsobject.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCheckConflictsObject](./kibana-plugin-core-server.savedobjectscheckconflictsobject.md) + +## SavedObjectsCheckConflictsObject interface + + +Signature: + +```typescript +export interface SavedObjectsCheckConflictsObject +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [id](./kibana-plugin-core-server.savedobjectscheckconflictsobject.id.md) | string | | +| [type](./kibana-plugin-core-server.savedobjectscheckconflictsobject.type.md) | string | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscheckconflictsobject.type.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscheckconflictsobject.type.md new file mode 100644 index 0000000000000..82f89536e4189 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscheckconflictsobject.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCheckConflictsObject](./kibana-plugin-core-server.savedobjectscheckconflictsobject.md) > [type](./kibana-plugin-core-server.savedobjectscheckconflictsobject.type.md) + +## SavedObjectsCheckConflictsObject.type property + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscheckconflictsresponse.errors.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscheckconflictsresponse.errors.md new file mode 100644 index 0000000000000..80bd61d8906e3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscheckconflictsresponse.errors.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCheckConflictsResponse](./kibana-plugin-core-server.savedobjectscheckconflictsresponse.md) > [errors](./kibana-plugin-core-server.savedobjectscheckconflictsresponse.errors.md) + +## SavedObjectsCheckConflictsResponse.errors property + +Signature: + +```typescript +errors: Array<{ + id: string; + type: string; + error: SavedObjectError; + }>; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscheckconflictsresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscheckconflictsresponse.md new file mode 100644 index 0000000000000..499398586e7dd --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscheckconflictsresponse.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCheckConflictsResponse](./kibana-plugin-core-server.savedobjectscheckconflictsresponse.md) + +## SavedObjectsCheckConflictsResponse interface + + +Signature: + +```typescript +export interface SavedObjectsCheckConflictsResponse +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [errors](./kibana-plugin-core-server.savedobjectscheckconflictsresponse.errors.md) | Array<{
id: string;
type: string;
error: SavedObjectError;
}> | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.checkconflicts.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.checkconflicts.md new file mode 100644 index 0000000000000..5cffb0c498b0b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.checkconflicts.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) > [checkConflicts](./kibana-plugin-core-server.savedobjectsclient.checkconflicts.md) + +## SavedObjectsClient.checkConflicts() method + +Check what conflicts will result when creating a given array of saved objects. This includes "unresolvable conflicts", which are multi-namespace objects that exist in a different namespace; such conflicts cannot be resolved/overwritten. + +Signature: + +```typescript +checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsBaseOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| objects | SavedObjectsCheckConflictsObject[] | | +| options | SavedObjectsBaseOptions | | + +Returns: + +`Promise` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md index 7038c0c07012f..7c1273e63d24b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md @@ -29,6 +29,7 @@ The constructor for this class is marked as internal. Third-party code should no | [bulkCreate(objects, options)](./kibana-plugin-core-server.savedobjectsclient.bulkcreate.md) | | Persists multiple documents batched together as a single request | | [bulkGet(objects, options)](./kibana-plugin-core-server.savedobjectsclient.bulkget.md) | | Returns an array of objects by id | | [bulkUpdate(objects, options)](./kibana-plugin-core-server.savedobjectsclient.bulkupdate.md) | | Bulk Updates multiple SavedObject at once | +| [checkConflicts(objects, options)](./kibana-plugin-core-server.savedobjectsclient.checkconflicts.md) | | Check what conflicts will result when creating a given array of saved objects. This includes "unresolvable conflicts", which are multi-namespace objects that exist in a different namespace; such conflicts cannot be resolved/overwritten. | | [create(type, attributes, options)](./kibana-plugin-core-server.savedobjectsclient.create.md) | | Persists a SavedObject | | [delete(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.delete.md) | | Deletes a SavedObject | | [deleteFromNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md) | | Removes namespaces from a SavedObject | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.checkconflicts.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.checkconflicts.md new file mode 100644 index 0000000000000..6e44bd704d6a7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.checkconflicts.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) > [checkConflicts](./kibana-plugin-core-server.savedobjectsrepository.checkconflicts.md) + +## SavedObjectsRepository.checkConflicts() method + +Check what conflicts will result when creating a given array of saved objects. This includes "unresolvable conflicts", which are multi-namespace objects that exist in a different namespace; such conflicts cannot be resolved/overwritten. + +Signature: + +```typescript +checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsBaseOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| objects | SavedObjectsCheckConflictsObject[] | | +| options | SavedObjectsBaseOptions | | + +Returns: + +`Promise` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md index 60071054de244..63845498d5b05 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md @@ -19,6 +19,7 @@ export declare class SavedObjectsRepository | [bulkCreate(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.bulkcreate.md) | | Creates multiple documents at once | | [bulkGet(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.bulkget.md) | | Returns an array of objects by id | | [bulkUpdate(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.bulkupdate.md) | | Updates multiple objects in bulk | +| [checkConflicts(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.checkconflicts.md) | | Check what conflicts will result when creating a given array of saved objects. This includes "unresolvable conflicts", which are multi-namespace objects that exist in a different namespace; such conflicts cannot be resolved/overwritten. | | [create(type, attributes, options)](./kibana-plugin-core-server.savedobjectsrepository.create.md) | | Persists an object | | [delete(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.delete.md) | | Deletes an object | | [deleteByNamespace(namespace, options)](./kibana-plugin-core-server.savedobjectsrepository.deletebynamespace.md) | | Deletes all objects from the provided namespace. | diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index ed95c16911a6c..e2f8759e328b8 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -1162,13 +1162,10 @@ export type RecursiveReadonly = T extends (...args: any[]) => any ? T : T ext // @public (undocumented) export interface SavedObject { attributes: T; + // Warning: (ae-forgotten-export) The symbol "SavedObjectError" needs to be exported by the entry point index.d.ts + // // (undocumented) - error?: { - error: string; - message: string; - statusCode: number; - metadata?: Record; - }; + error?: SavedObjectError; id: string; migrationVersion?: SavedObjectsMigrationVersion; namespaces?: string[]; diff --git a/src/core/server/index.ts b/src/core/server/index.ts index ad559c3f743c3..1fd35b7324d53 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -207,6 +207,8 @@ export { SavedObjectsBulkUpdateOptions, SavedObjectsBulkResponse, SavedObjectsBulkUpdateResponse, + SavedObjectsCheckConflictsObject, + SavedObjectsCheckConflictsResponse, SavedObjectsClient, SavedObjectsClientProviderOptions, SavedObjectsClientWrapperFactory, diff --git a/src/core/server/saved_objects/service/lib/repository.mock.ts b/src/core/server/saved_objects/service/lib/repository.mock.ts index afef378b7307b..c5fd260b78a9f 100644 --- a/src/core/server/saved_objects/service/lib/repository.mock.ts +++ b/src/core/server/saved_objects/service/lib/repository.mock.ts @@ -20,6 +20,7 @@ import { ISavedObjectsRepository } from './repository'; const create = (): jest.Mocked => ({ + checkConflicts: jest.fn(), create: jest.fn(), bulkCreate: jest.fn(), bulkUpdate: jest.fn(), 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 e081b6448dc29..4338d89f909ef 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -1433,6 +1433,123 @@ describe('SavedObjectsRepository', () => { }); }); + describe('#checkConflicts', () => { + const obj1 = { type: 'dashboard', id: 'one' }; + const obj2 = { type: 'dashboard', id: 'two' }; + const obj3 = { type: MULTI_NAMESPACE_TYPE, id: 'three' }; + const obj4 = { type: MULTI_NAMESPACE_TYPE, id: 'four' }; + const obj5 = { type: MULTI_NAMESPACE_TYPE, id: 'five' }; + const obj6 = { type: NAMESPACE_AGNOSTIC_TYPE, id: 'six' }; + const obj7 = { type: NAMESPACE_AGNOSTIC_TYPE, id: 'seven' }; + const namespace = 'foo-namespace'; + + const checkConflicts = async (objects, options) => + savedObjectsRepository.checkConflicts( + objects.map(({ type, id }) => ({ type, id })), // checkConflicts only uses type and id + options + ); + const checkConflictsSuccess = async (objects, options) => { + const response = getMockMgetResponse(objects, options?.namespace); + callAdminCluster.mockResolvedValue(response); // this._callCluster('mget', ...) + const result = await checkConflicts(objects, options); + expect(callAdminCluster).toHaveBeenCalledTimes(1); + return result; + }; + + const _expectClusterCallArgs = ( + objects, + { _index = expect.any(String), getId = () => expect.any(String) } + ) => { + expectClusterCallArgs({ + body: { + docs: objects.map(({ type, id }) => + expect.objectContaining({ + _index, + _id: getId(type, id), + }) + ), + }, + }); + }; + + describe('cluster calls', () => { + it(`doesn't make a cluster call if the objects array is empty`, async () => { + await checkConflicts([]); + expect(callAdminCluster).not.toHaveBeenCalled(); + }); + + it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { + const getId = (type, id) => `${namespace}:${type}:${id}`; + await checkConflictsSuccess([obj1, obj2], { namespace }); + _expectClusterCallArgs([obj1, obj2], { getId }); + }); + + it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { + const getId = (type, id) => `${type}:${id}`; + await checkConflictsSuccess([obj1, obj2]); + _expectClusterCallArgs([obj1, obj2], { getId }); + }); + + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { + const getId = (type, id) => `${type}:${id}`; + // obj3 is multi-namespace, and obj6 is namespace-agnostic + await checkConflictsSuccess([obj3, obj6], { namespace }); + _expectClusterCallArgs([obj3, obj6], { getId }); + }); + }); + + describe('migration', () => { + it(`waits until migrations are complete before proceeding`, async () => { + let callAdminClusterCount = 0; + migrator.runMigrations = jest.fn(async () => + // runMigrations should resolve before callAdminCluster is initiated + expect(callAdminCluster).toHaveBeenCalledTimes(callAdminClusterCount++) + ); + await expect(checkConflictsSuccess([obj1, obj2])).resolves.toBeDefined(); + expect(migrator.runMigrations).toHaveReturnedTimes(1); + }); + }); + + describe('returns', () => { + it(`expected results`, async () => { + const objects = [obj1, obj2, obj3, obj4, obj5, obj6, obj7]; + const response = { + status: 200, + docs: [ + getMockGetResponse(obj1), + { found: false }, + getMockGetResponse(obj3), + getMockGetResponse({ ...obj4, namespace: 'bar-namespace' }), + { found: false }, + getMockGetResponse(obj6), + { found: false }, + ], + }; + callAdminCluster.mockResolvedValueOnce(response); // this._callCluster('mget', ...) + + const result = await checkConflicts(objects); + expectClusterCalls('mget'); + expect(result).toEqual({ + errors: [ + { ...obj1, error: createConflictError(obj1.type, obj1.id) }, + // obj2 was not found so it does not result in a conflict error + { ...obj3, error: createConflictError(obj3.type, obj3.id) }, + { + ...obj4, + error: { + ...createConflictError(obj4.type, obj4.id), + metadata: { isNotOverwritable: true }, + }, + }, + // obj5 was not found so it does not result in a conflict error + { ...obj6, error: createConflictError(obj6.type, obj6.id) }, + // obj7 was not found so it does not result in a conflict error + ], + }); + }); + }); + }); + describe('#create', () => { beforeEach(() => { callAdminCluster.mockImplementation((method, params) => ({ diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index ea91ee7dbfbc4..8d70da07e604e 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -39,6 +39,8 @@ import { SavedObjectsBulkGetObject, SavedObjectsBulkResponse, SavedObjectsBulkUpdateResponse, + SavedObjectsCheckConflictsObject, + SavedObjectsCheckConflictsResponse, SavedObjectsCreateOptions, SavedObjectsFindResponse, SavedObjectsUpdateOptions, @@ -435,6 +437,51 @@ export class SavedObjectsRepository { }; } + /** + * Check what conflicts will result when creating a given array of saved objects. This includes "unresolvable conflicts", which are + * multi-namespace objects that exist in a different namespace; such conflicts cannot be resolved/overwritten. + */ + async checkConflicts( + objects: SavedObjectsCheckConflictsObject[] = [], + options: SavedObjectsBaseOptions = {} + ): Promise { + if (objects.length === 0) { + return { errors: [] }; + } + + const { namespace } = options; + const bulkGetDocs = objects.map(({ type, id }) => ({ + _id: this._serializer.generateRawId(namespace, type, id), + _index: this.getIndexForType(type), + _source: ['type', 'namespaces'], + })); + const bulkGetResponse = await this._callCluster('mget', { + body: { docs: bulkGetDocs }, + ignore: [404], + }); + + const errors: SavedObjectsCheckConflictsResponse['errors'] = []; + const indexFound = bulkGetResponse.status !== 404; + objects.forEach(({ type, id }, index) => { + const actualResult = indexFound ? bulkGetResponse.docs[index] : undefined; + const docFound = actualResult?.found === true; + if (docFound) { + errors.push({ + id, + type, + error: { + ...SavedObjectsErrorHelpers.createConflictError(type, id).output.payload, + ...(!this.rawDocExistsInNamespace(actualResult, namespace) && { + metadata: { isNotOverwritable: true }, + }), + }, + }); + } + }); + + return { errors }; + } + /** * Deletes an object * diff --git a/src/core/server/saved_objects/service/saved_objects_client.mock.ts b/src/core/server/saved_objects/service/saved_objects_client.mock.ts index b209c9ca54f63..3b0789970cc6b 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.mock.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.mock.ts @@ -25,6 +25,7 @@ const create = () => errors: SavedObjectsErrorHelpers, create: jest.fn(), bulkCreate: jest.fn(), + checkConflicts: jest.fn(), bulkUpdate: jest.fn(), delete: jest.fn(), bulkGet: jest.fn(), diff --git a/src/core/server/saved_objects/service/saved_objects_client.test.js b/src/core/server/saved_objects/service/saved_objects_client.test.js index 53bb31369adbf..47011414cbc7f 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.test.js +++ b/src/core/server/saved_objects/service/saved_objects_client.test.js @@ -35,6 +35,21 @@ test(`#create`, async () => { expect(result).toBe(returnValue); }); +test(`#checkConflicts`, async () => { + const returnValue = Symbol(); + const mockRepository = { + checkConflicts: jest.fn().mockResolvedValue(returnValue), + }; + const client = new SavedObjectsClient(mockRepository); + + const objects = Symbol(); + const options = Symbol(); + const result = await client.checkConflicts(objects, options); + + expect(mockRepository.checkConflicts).toHaveBeenCalledWith(objects, options); + expect(result).toBe(returnValue); +}); + test(`#bulkCreate`, async () => { const returnValue = Symbol(); const mockRepository = { 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 2c7f3fdf7c84b..7f48ead63c63d 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -20,6 +20,7 @@ import { ISavedObjectsRepository } from './lib'; import { SavedObject, + SavedObjectError, SavedObjectReference, SavedObjectsMigrationVersion, SavedObjectsBaseOptions, @@ -98,6 +99,27 @@ export interface SavedObjectsFindResponse { page: number; } +/** + * + * @public + */ +export interface SavedObjectsCheckConflictsObject { + id: string; + type: string; +} + +/** + * + * @public + */ +export interface SavedObjectsCheckConflictsResponse { + errors: Array<{ + id: string; + type: string; + error: SavedObjectError; + }>; +} + /** * * @public @@ -225,6 +247,20 @@ export class SavedObjectsClient { return await this._repository.bulkCreate(objects, options); } + /** + * Check what conflicts will result when creating a given array of saved objects. This includes "unresolvable conflicts", which are + * multi-namespace objects that exist in a different namespace; such conflicts cannot be resolved/overwritten. + * + * @param objects + * @param options + */ + async checkConflicts( + objects: SavedObjectsCheckConflictsObject[] = [], + options: SavedObjectsBaseOptions = {} + ): Promise { + return await this._repository.checkConflicts(objects, options); + } + /** * Deletes a SavedObject * diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index 9651c0dc11c7b..92e33bd5bac51 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -44,6 +44,7 @@ export { SavedObjectAttribute, SavedObjectAttributeSingle, SavedObject, + SavedObjectError, SavedObjectReference, SavedObjectsMigrationVersion, } from '../../types'; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 92f831fc0f4e7..f4bab1a9f03ea 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1697,13 +1697,10 @@ export type SafeRouteMethod = 'get' | 'options'; // @public (undocumented) export interface SavedObject { attributes: T; + // Warning: (ae-forgotten-export) The symbol "SavedObjectError" needs to be exported by the entry point index.d.ts + // // (undocumented) - error?: { - error: string; - message: string; - statusCode: number; - metadata?: Record; - }; + error?: SavedObjectError; id: string; migrationVersion?: SavedObjectsMigrationVersion; namespaces?: string[]; @@ -1820,6 +1817,24 @@ export interface SavedObjectsBulkUpdateResponse { saved_objects: Array>; } +// @public (undocumented) +export interface SavedObjectsCheckConflictsObject { + // (undocumented) + id: string; + // (undocumented) + type: string; +} + +// @public (undocumented) +export interface SavedObjectsCheckConflictsResponse { + // (undocumented) + errors: Array<{ + id: string; + type: string; + error: SavedObjectError; + }>; +} + // @public (undocumented) export class SavedObjectsClient { // @internal @@ -1828,6 +1843,7 @@ export class SavedObjectsClient { bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>; bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; + checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsBaseOptions): Promise; create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>; deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<{}>; @@ -2251,6 +2267,7 @@ export class SavedObjectsRepository { bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>; bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; + checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsBaseOptions): Promise; create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; // Warning: (ae-forgotten-export) The symbol "KibanaMigrator" needs to be exported by the entry point index.d.ts // diff --git a/src/core/types/saved_objects.ts b/src/core/types/saved_objects.ts index c43e7e9f8ac9d..9abc093c74fb3 100644 --- a/src/core/types/saved_objects.ts +++ b/src/core/types/saved_objects.ts @@ -86,12 +86,7 @@ export interface SavedObject { version?: string; /** Timestamp of the last time this document had been updated. */ updated_at?: string; - error?: { - error: string; - message: string; - statusCode: number; - metadata?: Record; - }; + error?: SavedObjectError; /** {@inheritdoc SavedObjectAttributes} */ attributes: T; /** {@inheritdoc SavedObjectReference} */ @@ -108,3 +103,10 @@ export interface SavedObject { */ originId?: string; } + +export interface SavedObjectError { + error: string; + message: string; + statusCode: number; + metadata?: Record; +} diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts index 7098f611defa0..eba0b1b4beb46 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts @@ -42,6 +42,19 @@ beforeEach(() => { afterEach(() => jest.clearAllMocks()); +describe('#checkConflicts', () => { + it('redirects request to underlying base client', async () => { + const objects = [{ type: 'foo', id: 'bar' }]; + const options = { namespace: 'some-namespace' }; + const mockedResponse = { errors: [] }; + mockBaseClient.checkConflicts.mockResolvedValue(mockedResponse); + + await expect(wrapper.checkConflicts(objects, options)).resolves.toEqual(mockedResponse); + expect(mockBaseClient.checkConflicts).toHaveBeenCalledTimes(1); + expect(mockBaseClient.checkConflicts).toHaveBeenCalledWith(objects, options); + }); +}); + describe('#create', () => { it('redirects request to underlying base client if type is not registered', async () => { const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }; diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts index bdc2b6cb2e667..e1819367b4c58 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts @@ -13,6 +13,7 @@ import { SavedObjectsBulkUpdateObject, SavedObjectsBulkResponse, SavedObjectsBulkUpdateResponse, + SavedObjectsCheckConflictsObject, SavedObjectsClientContract, SavedObjectsCreateOptions, SavedObjectsFindOptions, @@ -51,6 +52,13 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon private getDescriptorNamespace = (type: string, namespace?: string) => this.options.baseTypeRegistry.isSingleNamespace(type) ? namespace : undefined; + public async checkConflicts( + objects: SavedObjectsCheckConflictsObject[] = [], + options?: SavedObjectsBaseOptions + ) { + return await this.options.baseClient.checkConflicts(objects, options); + } + public async create( type: string, attributes: T = {} as T, diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts index c646cd95228f0..ffa727f27c037 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts @@ -474,6 +474,36 @@ describe('#bulkUpdate', () => { }); }); +describe('#checkConflicts', () => { + const obj1 = Object.freeze({ type: 'foo', otherThing: 'sup' }); + const obj2 = Object.freeze({ type: 'bar', otherThing: 'everyone' }); + const options = Object.freeze({ namespace: 'some-ns' }); + + test(`throws decorated GeneralError when checkPrivileges.globally rejects promise`, async () => { + const objects = [obj1, obj2]; + await expectGeneralError(client.checkConflicts, { objects }); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + const objects = [obj1, obj2]; + await expectForbiddenError(client.checkConflicts, { objects, options }); + }); + + test(`returns result of baseClient.create when authorized`, async () => { + const apiCallReturnValue = Symbol(); + clientOpts.baseClient.checkConflicts.mockResolvedValue(apiCallReturnValue as any); + + const objects = [obj1, obj2]; + const result = await expectSuccess(client.checkConflicts, { objects, options }); + expect(result).toBe(apiCallReturnValue); + }); + + test(`checks privileges for user, actions, and namespace`, async () => { + const objects = [obj1, obj2]; + await expectPrivilegeCheck(client.checkConflicts, { objects, options }); + }); +}); + describe('#create', () => { const type = 'foo'; const attributes = { some_attr: 's' }; diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts index 969344afae5e3..8b38199063650 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts @@ -9,6 +9,7 @@ import { SavedObjectsBulkCreateObject, SavedObjectsBulkGetObject, SavedObjectsBulkUpdateObject, + SavedObjectsCheckConflictsObject, SavedObjectsClientContract, SavedObjectsCreateOptions, SavedObjectsFindOptions, @@ -77,6 +78,19 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra return await this.redactSavedObjectNamespaces(savedObject); } + public async checkConflicts( + objects: SavedObjectsCheckConflictsObject[] = [], + options: SavedObjectsBaseOptions = {} + ) { + await this.ensureAuthorized(this.getUniqueObjectTypes(objects), 'create', options.namespace, { + objects, + options, + }); + + const response = await this.baseClient.checkConflicts(objects, options); + return response; + } + public async bulkCreate( objects: Array>, options: SavedObjectsBaseOptions = {} diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts index 9f1edadb97760..9c8ee231a08a9 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts @@ -174,6 +174,7 @@ describe('copySavedObjectsToSpaces', () => { "bulkCreate": [MockFunction], "bulkGet": [MockFunction], "bulkUpdate": [MockFunction], + "checkConflicts": [MockFunction], "create": [MockFunction], "delete": [MockFunction], "deleteFromNamespaces": [MockFunction], @@ -240,6 +241,7 @@ describe('copySavedObjectsToSpaces', () => { "bulkCreate": [MockFunction], "bulkGet": [MockFunction], "bulkUpdate": [MockFunction], + "checkConflicts": [MockFunction], "create": [MockFunction], "delete": [MockFunction], "deleteFromNamespaces": [MockFunction], @@ -313,6 +315,7 @@ describe('copySavedObjectsToSpaces', () => { "bulkCreate": [MockFunction], "bulkGet": [MockFunction], "bulkUpdate": [MockFunction], + "checkConflicts": [MockFunction], "create": [MockFunction], "delete": [MockFunction], "deleteFromNamespaces": [MockFunction], diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts index f8664349fc9cd..1b9d42a3af4e8 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts @@ -190,6 +190,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { "bulkCreate": [MockFunction], "bulkGet": [MockFunction], "bulkUpdate": [MockFunction], + "checkConflicts": [MockFunction], "create": [MockFunction], "delete": [MockFunction], "deleteFromNamespaces": [MockFunction], @@ -263,6 +264,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { "bulkCreate": [MockFunction], "bulkGet": [MockFunction], "bulkUpdate": [MockFunction], + "checkConflicts": [MockFunction], "create": [MockFunction], "delete": [MockFunction], "deleteFromNamespaces": [MockFunction], @@ -343,6 +345,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { "bulkCreate": [MockFunction], "bulkGet": [MockFunction], "bulkUpdate": [MockFunction], + "checkConflicts": [MockFunction], "create": [MockFunction], "delete": [MockFunction], "deleteFromNamespaces": [MockFunction], diff --git a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts index 75cd501a1a9ae..2e95b9635c22a 100644 --- a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts +++ b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts @@ -176,6 +176,34 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; }); }); + describe('#checkConflicts', () => { + test(`throws error if options.namespace is specified`, async () => { + const { client } = await createSpacesSavedObjectsClient(); + + await expect( + // @ts-ignore + client.checkConflicts(null, { namespace: 'bar' }) + ).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED); + }); + + test(`supplements options with the current namespace`, async () => { + const { client, baseClient } = await createSpacesSavedObjectsClient(); + const expectedReturnValue = { errors: [] }; + baseClient.checkConflicts.mockReturnValue(Promise.resolve(expectedReturnValue)); + + const objects = Symbol(); + const options = Object.freeze({ foo: 'bar' }); + // @ts-ignore + const actualReturnValue = await client.checkConflicts(objects, options); + + expect(actualReturnValue).toBe(expectedReturnValue); + expect(baseClient.checkConflicts).toHaveBeenCalledWith(objects, { + foo: 'bar', + namespace: currentSpace.expectedNamespace, + }); + }); + }); + describe('#create', () => { test(`throws error if options.namespace is specified`, async () => { const { client } = await createSpacesSavedObjectsClient(); diff --git a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts index 6611725be8b67..acb3c8dbd9551 100644 --- a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts +++ b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts @@ -9,6 +9,7 @@ import { SavedObjectsBulkCreateObject, SavedObjectsBulkGetObject, SavedObjectsBulkUpdateObject, + SavedObjectsCheckConflictsObject, SavedObjectsClientContract, SavedObjectsCreateOptions, SavedObjectsFindOptions, @@ -56,6 +57,25 @@ export class SpacesSavedObjectsClient implements SavedObjectsClientContract { this.errors = baseClient.errors; } + /** + * Check what conflicts will result when creating a given array of saved objects. This includes "unresolvable conflicts", which are + * multi-namespace objects that exist in a different namespace; such conflicts cannot be resolved/overwritten. + * + * @param objects + * @param options + */ + public async checkConflicts( + objects: SavedObjectsCheckConflictsObject[] = [], + options: SavedObjectsBaseOptions = {} + ) { + throwErrorIfNamespaceSpecified(options); + + return await this.client.checkConflicts(objects, { + ...options, + namespace: spaceIdToNamespace(this.spaceId), + }); + } + /** * Persists an object * From 1dfe7eba777dc00622e95d76cbffcfe6d70772c9 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Thu, 18 Jun 2020 13:16:15 -0400 Subject: [PATCH 18/55] Rename `newId` to `destinationId` --- ...savedobjectsimportsuccess.destinationid.md | 13 +++++++++++++ ...n-core-public.savedobjectsimportsuccess.md | 2 +- ...-public.savedobjectsimportsuccess.newid.md | 13 ------------- ...savedobjectsimportsuccess.destinationid.md | 13 +++++++++++++ ...n-core-server.savedobjectsimportsuccess.md | 2 +- ...-server.savedobjectsimportsuccess.newid.md | 13 ------------- src/core/public/public.api.md | 2 +- .../import/create_saved_objects.test.ts | 4 ++-- .../import/create_saved_objects.ts | 8 ++++---- .../import/extract_errors.test.ts | 4 ++-- .../saved_objects/import/extract_errors.ts | 6 +++--- .../import/import_saved_objects.test.ts | 6 +++--- .../import/import_saved_objects.ts | 4 ++-- .../import/resolve_import_errors.test.ts | 6 +++--- .../import/resolve_import_errors.ts | 8 ++++++-- src/core/server/saved_objects/import/types.ts | 4 ++-- src/core/server/server.api.md | 2 +- .../common/suites/import.ts | 17 +++++++++++------ .../common/suites/resolve_import_errors.ts | 17 +++++++++++------ .../security_and_spaces/apis/import.ts | 13 +++++++------ .../apis/resolve_import_errors.ts | 19 ++++++++++--------- .../security_only/apis/import.ts | 11 ++++++----- .../apis/resolve_import_errors.ts | 13 +++++++------ .../spaces_only/apis/import.ts | 13 +++++++------ .../spaces_only/apis/resolve_import_errors.ts | 19 ++++++++++--------- .../common/suites/copy_to_space.ts | 12 ++++++------ .../suites/resolve_copy_to_space_conflicts.ts | 4 ++-- 27 files changed, 134 insertions(+), 114 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.destinationid.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.newid.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.destinationid.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.newid.md diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.destinationid.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.destinationid.md new file mode 100644 index 0000000000000..55611a77aeb67 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.destinationid.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportSuccess](./kibana-plugin-core-public.savedobjectsimportsuccess.md) > [destinationId](./kibana-plugin-core-public.savedobjectsimportsuccess.destinationid.md) + +## SavedObjectsImportSuccess.destinationId property + +If `destinationId` is specified, the new object has a new ID that is different from the import ID. + +Signature: + +```typescript +destinationId?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.md index f7e697d773de4..407e1d9c5efdd 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.md @@ -16,7 +16,7 @@ export interface SavedObjectsImportSuccess | Property | Type | Description | | --- | --- | --- | +| [destinationId](./kibana-plugin-core-public.savedobjectsimportsuccess.destinationid.md) | string | If destinationId is specified, the new object has a new ID that is different from the import ID. | | [id](./kibana-plugin-core-public.savedobjectsimportsuccess.id.md) | string | | -| [newId](./kibana-plugin-core-public.savedobjectsimportsuccess.newid.md) | string | If newId is specified, the new object has a new ID that is different from the import ID. | | [type](./kibana-plugin-core-public.savedobjectsimportsuccess.type.md) | string | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.newid.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.newid.md deleted file mode 100644 index e8d301dcdef06..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.newid.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportSuccess](./kibana-plugin-core-public.savedobjectsimportsuccess.md) > [newId](./kibana-plugin-core-public.savedobjectsimportsuccess.newid.md) - -## SavedObjectsImportSuccess.newId property - -If `newId` is specified, the new object has a new ID that is different from the import ID. - -Signature: - -```typescript -newId?: string; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.destinationid.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.destinationid.md new file mode 100644 index 0000000000000..c5acc51c3ec99 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.destinationid.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportSuccess](./kibana-plugin-core-server.savedobjectsimportsuccess.md) > [destinationId](./kibana-plugin-core-server.savedobjectsimportsuccess.destinationid.md) + +## SavedObjectsImportSuccess.destinationId property + +If `destinationId` is specified, the new object has a new ID that is different from the import ID. + +Signature: + +```typescript +destinationId?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.md index 875dd0989de9e..9f5bc40cb8c7c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.md @@ -16,7 +16,7 @@ export interface SavedObjectsImportSuccess | Property | Type | Description | | --- | --- | --- | +| [destinationId](./kibana-plugin-core-server.savedobjectsimportsuccess.destinationid.md) | string | If destinationId is specified, the new object has a new ID that is different from the import ID. | | [id](./kibana-plugin-core-server.savedobjectsimportsuccess.id.md) | string | | -| [newId](./kibana-plugin-core-server.savedobjectsimportsuccess.newid.md) | string | If newId is specified, the new object has a new ID that is different from the import ID. | | [type](./kibana-plugin-core-server.savedobjectsimportsuccess.type.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.newid.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.newid.md deleted file mode 100644 index abbaced760b7b..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.newid.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportSuccess](./kibana-plugin-core-server.savedobjectsimportsuccess.md) > [newId](./kibana-plugin-core-server.savedobjectsimportsuccess.newid.md) - -## SavedObjectsImportSuccess.newId property - -If `newId` is specified, the new object has a new ID that is different from the import ID. - -Signature: - -```typescript -newId?: string; -``` diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index e2f8759e328b8..1cf00a3c33dab 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -1396,9 +1396,9 @@ export interface SavedObjectsImportRetry { // @public export interface SavedObjectsImportSuccess { + destinationId?: string; // (undocumented) id: string; - newId?: string; // (undocumented) type: string; } diff --git a/src/core/server/saved_objects/import/create_saved_objects.test.ts b/src/core/server/saved_objects/import/create_saved_objects.test.ts index b1129ec574270..9dc2f07f74ee3 100644 --- a/src/core/server/saved_objects/import/create_saved_objects.test.ts +++ b/src/core/server/saved_objects/import/create_saved_objects.test.ts @@ -213,7 +213,7 @@ describe('#createSavedObjects', () => { const resultSavedObjects = (await bulkCreate.mock.results[0].value).saved_objects; const [r1, r2, r3, r4, r6, r7, r8, r10, r11, r12, r13] = resultSavedObjects; // these three results are transformed before being returned, because the bulkCreate attempt used different IDs for them - const [x3, x4, x8] = [r3, r4, r8].map((x: SavedObject) => ({ ...x, newId: x.id })); + const [x3, x4, x8] = [r3, r4, r8].map((x: SavedObject) => ({ ...x, destinationId: x.id })); const transformedResults = [r1, r2, x3, x4, r6, r7, x8, r10, r11, r12, r13]; const expectedResults = getExpectedResults(transformedResults, objs); expect(results).toEqual(expectedResults); @@ -311,7 +311,7 @@ describe('#createSavedObjects', () => { // these five results are transformed before being returned, because the bulkCreate attempt used different IDs for them const [x3, x4, x5, x8, x9] = [r3, r4, r5, r8, r9].map((x: SavedObject) => ({ ...x, - newId: x.id, + destinationId: x.id, })); const transformedResults = [r1, r2, x3, x4, x5, r6, r7, x8, x9, r10, r11, r12, r13]; const expectedResults = getExpectedResults(transformedResults, objs); diff --git a/src/core/server/saved_objects/import/create_saved_objects.ts b/src/core/server/saved_objects/import/create_saved_objects.ts index a4eefa343e311..5364cdbd8e72d 100644 --- a/src/core/server/saved_objects/import/create_saved_objects.ts +++ b/src/core/server/saved_objects/import/create_saved_objects.ts @@ -28,7 +28,7 @@ interface CreateSavedObjectsOptions { overwrite?: boolean; } interface CreateSavedObjectsResult { - createdObjects: Array & { newId?: string }>; + createdObjects: Array & { destinationId?: string }>; errors: SavedObjectsImportError[]; } @@ -125,10 +125,10 @@ export const createSavedObjects = async ( // remap results to reflect the object IDs that were submitted for import // this ensures that consumers understand the results - const remappedResults = results.map & { newId?: string }>((result) => { + const remappedResults = results.map & { destinationId?: string }>((result) => { const { id } = objectIdMap.get(`${result.type}:${result.id}`)!; - // also, include a `newId` field if the object create attempt was made with a different ID - return { ...result, id, ...(id !== result.id && { newId: result.id }) }; + // also, include a `destinationId` field if the object create attempt was made with a different ID + return { ...result, id, ...(id !== result.id && { destinationId: result.id }) }; }); return { diff --git a/src/core/server/saved_objects/import/extract_errors.test.ts b/src/core/server/saved_objects/import/extract_errors.test.ts index e75dca2626648..1e061d9fb5055 100644 --- a/src/core/server/saved_objects/import/extract_errors.test.ts +++ b/src/core/server/saved_objects/import/extract_errors.test.ts @@ -29,7 +29,7 @@ describe('extractErrors()', () => { }); test('extracts errors from saved objects', () => { - const savedObjects: Array = [ + const savedObjects: Array = [ { id: '1', type: 'dashboard', @@ -64,7 +64,7 @@ describe('extractErrors()', () => { }, references: [], error: SavedObjectsErrorHelpers.createConflictError('dashboard', '4').output.payload, - newId: 'foo', + destinationId: 'foo', }, ]; const result = extractErrors(savedObjects, savedObjects); diff --git a/src/core/server/saved_objects/import/extract_errors.ts b/src/core/server/saved_objects/import/extract_errors.ts index d1cc912eb67b1..92196afc47ecd 100644 --- a/src/core/server/saved_objects/import/extract_errors.ts +++ b/src/core/server/saved_objects/import/extract_errors.ts @@ -21,7 +21,7 @@ import { SavedObjectsImportError } from './types'; export function extractErrors( // TODO: define saved object type - savedObjectResults: Array & { newId?: string }>, + savedObjectResults: Array & { destinationId?: string }>, savedObjectsToImport: Array> ) { const errors: SavedObjectsImportError[] = []; @@ -35,7 +35,7 @@ export function extractErrors( `${savedObject.type}:${savedObject.id}` ); const title = originalSavedObject?.attributes?.title; - const { newId } = savedObject; + const { destinationId } = savedObject; if (savedObject.error.statusCode === 409) { errors.push({ id: savedObject.id, @@ -43,7 +43,7 @@ export function extractErrors( title, error: { type: 'conflict', - ...(newId && { destinationId: newId }), + ...(destinationId && { destinationId }), }, }); continue; 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 30db655476b8c..b4cad00c95e7d 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 @@ -153,14 +153,14 @@ describe('#importSavedObjectsFromStream', () => { const options = setupOptions(); const errors = [createError()]; const obj1 = createObject(); - const obj2 = { ...createObject(), newId: 'some-newId' }; + const obj2 = { ...createObject(), destinationId: 'some-destinationId' }; getMockFn(createSavedObjects).mockResolvedValue({ errors, createdObjects: [obj1, obj2] }); const result = await importSavedObjectsFromStream(options); - // successResults only includes the imported object's type, id, and newId (if a new one was generated) + // successResults only includes the imported object's type, id, and destinationId (if a new one was generated) const successResults = [ { type: obj1.type, id: obj1.id }, - { type: obj2.type, id: obj2.id, newId: 'some-newId' }, + { type: obj2.type, id: obj2.id, destinationId: 'some-destinationId' }, ]; expect(result).toEqual({ success: false, successCount: 2, successResults, errors }); }); 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 745d170ba0525..c3339b6ca4847 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.ts @@ -75,8 +75,8 @@ export async function importSavedObjectsFromStream({ ); errorAccumulator = [...errorAccumulator, ...bulkCreateErrors]; - const successResults = createdObjects.map(({ type, id, newId }) => { - return { type, id, ...(newId && { newId }) }; + const successResults = createdObjects.map(({ type, id, destinationId }) => { + return { type, id, ...(destinationId && { destinationId }) }; }); return { 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 20644becb7f18..12394cb1d6c75 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 @@ -237,7 +237,7 @@ describe('#importSavedObjectsFromStream', () => { const options = setupOptions(); const errors = [createError()]; const obj1 = createObject(); - const obj2 = { ...createObject(), newId: 'some-newId' }; + const obj2 = { ...createObject(), destinationId: 'some-destinationId' }; getMockFn(createSavedObjects).mockResolvedValueOnce({ errors, createdObjects: [obj1], @@ -248,10 +248,10 @@ describe('#importSavedObjectsFromStream', () => { }); const result = await resolveSavedObjectsImportErrors(options); - // successResults only includes the imported object's type, id, and newId (if a new one was generated) + // successResults only includes the imported object's type, id, and destinationId (if a new one was generated) const successResults = [ { type: obj1.type, id: obj1.id }, - { type: obj2.type, id: obj2.id, newId: obj2.newId }, + { type: obj2.type, id: obj2.id, destinationId: obj2.destinationId }, ]; expect(result).toEqual({ success: false, successCount: 2, successResults, errors }); }); 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 97a1fec075dc2..fa571b7e3b6ef 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.ts @@ -99,7 +99,7 @@ export async function resolveSavedObjectsImportErrors({ const importIdMap = getImportIdMapForRetries(filteredObjects, { typeRegistry, retries }); // Bulk create in two batches, overwrites and non-overwrites - let successResults: Array<{ type: string; id: string; newId?: string }> = []; + let successResults: Array<{ type: string; id: string; destinationId?: string }> = []; const bulkCreateObjects = async (objects: Array>, overwrite?: boolean) => { const options = { savedObjectsClient, importIdMap, namespace, overwrite }; const { createdObjects, errors: bulkCreateErrors } = await createSavedObjects(objects, options); @@ -107,7 +107,11 @@ export async function resolveSavedObjectsImportErrors({ successCount += createdObjects.length; successResults = [ ...successResults, - ...createdObjects.map(({ type, id, newId }) => ({ type, id, ...(newId && { newId }) })), + ...createdObjects.map(({ type, id, destinationId }) => ({ + type, + id, + ...(destinationId && { destinationId }), + })), ]; }; const { objectsToOverwrite, objectsToNotOverwrite } = splitOverwrites(filteredObjects, retries); diff --git a/src/core/server/saved_objects/import/types.ts b/src/core/server/saved_objects/import/types.ts index 0838ce5ba3af8..3fe27ff0b762e 100644 --- a/src/core/server/saved_objects/import/types.ts +++ b/src/core/server/saved_objects/import/types.ts @@ -117,9 +117,9 @@ export interface SavedObjectsImportSuccess { id: string; type: string; /** - * If `newId` is specified, the new object has a new ID that is different from the import ID. + * If `destinationId` is specified, the new object has a new ID that is different from the import ID. */ - newId?: string; + destinationId?: string; } /** diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index f4bab1a9f03ea..c9b5f6cd53111 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2159,9 +2159,9 @@ export interface SavedObjectsImportRetry { // @public export interface SavedObjectsImportSuccess { + destinationId?: string; // (undocumented) id: string; - newId?: string; // (undocumented) type: string; } diff --git a/x-pack/test/saved_object_api_integration/common/suites/import.ts b/x-pack/test/saved_object_api_integration/common/suites/import.ts index f12671b551bc2..d53840bfc537c 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/import.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/import.ts @@ -106,21 +106,26 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe (x) => x.type === type && x.id === id ); expect(object).not.to.be(undefined); - const newId = object!.newId as string; - if (successParam === 'newId') { + const destinationId = object!.destinationId as string; + if (successParam === 'destinationId') { // Kibana created the object with a different ID than what was specified in the import // This can happen due to an unresolvable conflict (so the new ID will be random), or due to an inexact match (so the new ID will // be equal to the ID or originID of the existing object that it inexactly matched) if (expectedNewId) { - expect(newId).to.be(expectedNewId); + expect(destinationId).to.be(expectedNewId); } else { // the new ID was randomly generated - expect(newId).to.match(/^[0-9a-f-]{36}$/); + expect(destinationId).to.match(/^[0-9a-f-]{36}$/); } } else { - expect(newId).to.be(undefined); + expect(destinationId).to.be(undefined); } - const { _source } = await expectResponses.successCreated(es, spaceId, type, newId ?? id); + const { _source } = await expectResponses.successCreated( + es, + spaceId, + type, + destinationId ?? id + ); expect(_source[type][NEW_ATTRIBUTE_KEY]).to.eql(NEW_ATTRIBUTE_VAL); } for (let i = 0; i < expectedFailures.length; i++) { diff --git a/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts index 241441998d8f5..519d49167e37b 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts @@ -116,21 +116,26 @@ export function resolveImportErrorsTestSuiteFactory( (x) => x.type === type && x.id === id ); expect(object).not.to.be(undefined); - const newId = object!.newId as string; - if (successParam === 'newId') { + const destinationId = object!.destinationId as string; + if (successParam === 'destinationId') { // Kibana created the object with a different ID than what was specified in the import // This can happen due to an unresolvable conflict (so the new ID will be random), or due to an inexact match (so the new ID will // be equal to the ID or originID of the existing object that it inexactly matched) if (idToOverwrite) { - expect(newId).to.be(idToOverwrite); + expect(destinationId).to.be(idToOverwrite); } else { // the new ID was randomly generated - expect(newId).to.match(/^[0-9a-f-]{36}$/); + expect(destinationId).to.match(/^[0-9a-f-]{36}$/); } } else { - expect(newId).to.be(undefined); + expect(destinationId).to.be(undefined); } - const { _source } = await expectResponses.successCreated(es, spaceId, type, newId ?? id); + const { _source } = await expectResponses.successCreated( + es, + spaceId, + type, + destinationId ?? id + ); expect(_source[type][NEW_ATTRIBUTE_KEY]).to.eql(NEW_ATTRIBUTE_VAL); } for (let i = 0; i < expectedFailures.length; i++) { diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts index 2a8db4dd7b671..14850a4483353 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts @@ -20,7 +20,8 @@ const { SPACE_2: { spaceId: SPACE_2_ID }, } = SPACES; const { fail400, fail409 } = testCaseFailures; -const newId = (condition?: boolean) => (condition !== false ? { successParam: 'newId' } : {}); +const destinationId = (condition?: boolean) => + condition !== false ? { successParam: 'destinationId' } : {}; const ambiguousConflict = (suffix: string) => ({ failure: 409 as 409, fail409Param: `ambiguous_conflict_${suffix}`, @@ -47,23 +48,23 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite && (spaceId === DEFAULT_SPACE_ID || spaceId === SPACE_1_ID)), - ...newId(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + ...destinationId(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), }, { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID), - ...newId(spaceId !== SPACE_1_ID), + ...destinationId(spaceId !== SPACE_1_ID), }, { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID), - ...newId(spaceId !== SPACE_2_ID), + ...destinationId(spaceId !== SPACE_2_ID), }, { ...CASES.CONFLICT_1A_OBJ, ...ambiguousConflict('1a1b') }, // "ambiguous source" conflict { ...CASES.CONFLICT_1B_OBJ, ...ambiguousConflict('1a1b') }, // "ambiguous source" conflict { ...CASES.CONFLICT_2C_OBJ, ...ambiguousConflict('2c') }, // "ambiguous destination" conflict - { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...newId() }, // "inexact match" conflict - { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...newId() }, // "inexact match" conflict + { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict + { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict ]; const group3 = [ { ...CASES.CONFLICT_1_OBJ, ...fail409(!overwrite) }, // "exact match" conflict diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts index 842a994d17536..bf1cad80e8b94 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts @@ -20,7 +20,8 @@ const { SPACE_2: { spaceId: SPACE_2_ID }, } = SPACES; const { fail400, fail409 } = testCaseFailures; -const newId = (condition?: boolean) => (condition !== false ? { successParam: 'newId' } : {}); +const destinationId = (condition?: boolean) => + condition !== false ? { successParam: 'destinationId' } : {}; const createTestCases = (overwrite: boolean, spaceId: string) => { // for each permitted (non-403) outcome, if failure !== undefined then we expect @@ -41,27 +42,27 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite && (spaceId === DEFAULT_SPACE_ID || spaceId === SPACE_1_ID)), - ...newId(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + ...destinationId(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), }, { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID), - ...newId(spaceId !== SPACE_1_ID), + ...destinationId(spaceId !== SPACE_1_ID), }, { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID), - ...newId(spaceId !== SPACE_2_ID), + ...destinationId(spaceId !== SPACE_2_ID), }, // all of the cases below represent imports that had an inexact match conflict or an ambiguous conflict // if we call _resolve_import_errors and don't specify overwrite or duplicate, each of these will not result in a conflict because they // will skip the preflight search results; so the objects will be created instead. - { ...CASES.CONFLICT_1A_OBJ, ...newId(overwrite) }, // "ambiguous source" conflict; if overwrite=true, will overwrite 'conflict_1' + { ...CASES.CONFLICT_1A_OBJ, ...destinationId(overwrite) }, // "ambiguous source" conflict; if overwrite=true, will overwrite 'conflict_1' CASES.CONFLICT_1B_OBJ, // "ambiguous source" conflict; if overwrite=true, will create a new object (since 'conflict_1a' is overwriting 'conflict_1') - { ...CASES.CONFLICT_2C_OBJ, ...newId(overwrite) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2a' - { ...CASES.CONFLICT_2D_OBJ, ...newId(overwrite) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2b' - { ...CASES.CONFLICT_3A_OBJ, ...newId(overwrite) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3' - { ...CASES.CONFLICT_4_OBJ, ...newId(overwrite) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a' + { ...CASES.CONFLICT_2C_OBJ, ...destinationId(overwrite) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2a' + { ...CASES.CONFLICT_2D_OBJ, ...destinationId(overwrite) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2b' + { ...CASES.CONFLICT_3A_OBJ, ...destinationId(overwrite) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3' + { ...CASES.CONFLICT_4_OBJ, ...destinationId(overwrite) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a' ]; return { group1Importable, group1NonImportable, group1All, group2 }; }; diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/import.ts b/x-pack/test/saved_object_api_integration/security_only/apis/import.ts index 74e569195f351..3c957111bf717 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/import.ts @@ -14,7 +14,8 @@ import { } from '../../common/suites/import'; const { fail400, fail409 } = testCaseFailures; -const newId = (condition?: boolean) => (condition !== false ? { successParam: 'newId' } : {}); +const destinationId = (condition?: boolean) => + condition !== false ? { successParam: 'destinationId' } : {}; const ambiguousConflict = (suffix: string) => ({ failure: 409 as 409, fail409Param: `ambiguous_conflict_${suffix}`, @@ -36,13 +37,13 @@ const createTestCases = (overwrite: boolean) => { const group2 = [ CASES.NEW_MULTI_NAMESPACE_OBJ, { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite) }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...newId() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...newId() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...destinationId() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...destinationId() }, { ...CASES.CONFLICT_1A_OBJ, ...ambiguousConflict('1a1b') }, // "ambiguous source" conflict { ...CASES.CONFLICT_1B_OBJ, ...ambiguousConflict('1a1b') }, // "ambiguous source" conflict { ...CASES.CONFLICT_2C_OBJ, ...ambiguousConflict('2c') }, // "ambiguous destination" conflict - { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...newId() }, // "inexact match" conflict - { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...newId() }, // "inexact match" conflict + { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict + { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict ]; const group3 = [ { ...CASES.CONFLICT_1_OBJ, ...fail409(!overwrite) }, // "exact match" conflict diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts index 51bb2308f5e87..77235690c8784 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts @@ -14,7 +14,8 @@ import { } from '../../common/suites/resolve_import_errors'; const { fail400, fail409 } = testCaseFailures; -const newId = (condition?: boolean) => (condition !== false ? { successParam: 'newId' } : {}); +const destinationId = (condition?: boolean) => + condition !== false ? { successParam: 'destinationId' } : {}; const createTestCases = (overwrite: boolean) => { // for each permitted (non-403) outcome, if failure !== undefined then we expect @@ -30,12 +31,12 @@ const createTestCases = (overwrite: boolean) => { // all of the cases below represent imports that had an inexact match conflict or an ambiguous conflict // if we call _resolve_import_errors and don't specify overwrite or duplicate, each of these will not result in a conflict because they // will skip the preflight search results; so the objects will be created instead. - { ...CASES.CONFLICT_1A_OBJ, ...newId(overwrite) }, // "ambiguous source" conflict; if overwrite=true, will overwrite 'conflict_1' + { ...CASES.CONFLICT_1A_OBJ, ...destinationId(overwrite) }, // "ambiguous source" conflict; if overwrite=true, will overwrite 'conflict_1' CASES.CONFLICT_1B_OBJ, // "ambiguous source" conflict; if overwrite=true, will create a new object (since 'conflict_1a' is overwriting 'conflict_1') - { ...CASES.CONFLICT_2C_OBJ, ...newId(overwrite) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2a' - { ...CASES.CONFLICT_2D_OBJ, ...newId(overwrite) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2b' - { ...CASES.CONFLICT_3A_OBJ, ...newId(overwrite) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3' - { ...CASES.CONFLICT_4_OBJ, ...newId(overwrite) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a' + { ...CASES.CONFLICT_2C_OBJ, ...destinationId(overwrite) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2a' + { ...CASES.CONFLICT_2D_OBJ, ...destinationId(overwrite) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2b' + { ...CASES.CONFLICT_3A_OBJ, ...destinationId(overwrite) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3' + { ...CASES.CONFLICT_4_OBJ, ...destinationId(overwrite) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a' ]; return { group1Importable, group1NonImportable, group1All, group2 }; }; diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts index 7712ac8561389..22d5c79ec3b05 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts @@ -15,7 +15,8 @@ const { SPACE_2: { spaceId: SPACE_2_ID }, } = SPACES; const { fail400, fail409 } = testCaseFailures; -const newId = (condition?: boolean) => (condition !== false ? { successParam: 'newId' } : {}); +const destinationId = (condition?: boolean) => + condition !== false ? { successParam: 'destinationId' } : {}; const ambiguousConflict = (suffix: string) => ({ failure: 409 as 409, fail409Param: `ambiguous_conflict_${suffix}`, @@ -34,25 +35,25 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite && (spaceId === DEFAULT_SPACE_ID || spaceId === SPACE_1_ID)), - ...newId(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + ...destinationId(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), }, { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID), - ...newId(spaceId !== SPACE_1_ID), + ...destinationId(spaceId !== SPACE_1_ID), }, { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID), - ...newId(spaceId !== SPACE_2_ID), + ...destinationId(spaceId !== SPACE_2_ID), }, { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, { ...CASES.HIDDEN, ...fail400() }, { ...CASES.CONFLICT_1A_OBJ, ...ambiguousConflict('1a1b') }, // "ambiguous source" conflict { ...CASES.CONFLICT_1B_OBJ, ...ambiguousConflict('1a1b') }, // "ambiguous source" conflict { ...CASES.CONFLICT_2C_OBJ, ...ambiguousConflict('2c') }, // "ambiguous destination" conflict - { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...newId() }, // "inexact match" conflict - { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...newId() }, // "inexact match" conflict + { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict + { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict CASES.NEW_SINGLE_NAMESPACE_OBJ, CASES.NEW_MULTI_NAMESPACE_OBJ, CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts index 06d1c88457b05..2ccbfce91a123 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts @@ -18,7 +18,8 @@ const { SPACE_2: { spaceId: SPACE_2_ID }, } = SPACES; const { fail400, fail409 } = testCaseFailures; -const newId = (condition?: boolean) => (condition !== false ? { successParam: 'newId' } : {}); +const destinationId = (condition?: boolean) => + condition !== false ? { successParam: 'destinationId' } : {}; const createTestCases = (overwrite: boolean, spaceId: string) => { // for each outcome, if failure !== undefined then we expect to receive @@ -34,29 +35,29 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite && (spaceId === DEFAULT_SPACE_ID || spaceId === SPACE_1_ID)), - ...newId(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + ...destinationId(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), }, { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID), - ...newId(spaceId !== SPACE_1_ID), + ...destinationId(spaceId !== SPACE_1_ID), }, { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID), - ...newId(spaceId !== SPACE_2_ID), + ...destinationId(spaceId !== SPACE_2_ID), }, { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, { ...CASES.HIDDEN, ...fail400() }, // all of the cases below represent imports that had an inexact match conflict or an ambiguous conflict // if we call _resolve_import_errors and don't specify overwrite or duplicate, each of these will not result in a conflict because they // will skip the preflight search results; so the objects will be created instead. - { ...CASES.CONFLICT_1A_OBJ, ...newId(overwrite) }, // "ambiguous source" conflict; if overwrite=true, will overwrite 'conflict_1' + { ...CASES.CONFLICT_1A_OBJ, ...destinationId(overwrite) }, // "ambiguous source" conflict; if overwrite=true, will overwrite 'conflict_1' CASES.CONFLICT_1B_OBJ, // "ambiguous source" conflict; if overwrite=true, will create a new object (since 'conflict_1a' is overwriting 'conflict_1') - { ...CASES.CONFLICT_2C_OBJ, ...newId(overwrite) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2a' - { ...CASES.CONFLICT_2D_OBJ, ...newId(overwrite) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2b' - { ...CASES.CONFLICT_3A_OBJ, ...newId(overwrite) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3' - { ...CASES.CONFLICT_4_OBJ, ...newId(overwrite) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a' + { ...CASES.CONFLICT_2C_OBJ, ...destinationId(overwrite) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2a' + { ...CASES.CONFLICT_2D_OBJ, ...destinationId(overwrite) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2b' + { ...CASES.CONFLICT_3A_OBJ, ...destinationId(overwrite) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3' + { ...CASES.CONFLICT_4_OBJ, ...destinationId(overwrite) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a' ]; }; diff --git a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts index 2c016c4682d23..f7543589f97da 100644 --- a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts @@ -374,9 +374,9 @@ export function copyToSpaceTestSuiteFactory( const { success, successCount, successResults, errors } = getResult(response); expect(success).to.eql(true); expect(successCount).to.eql(1); - const newId = successResults![0].newId; - expect(newId).to.match(v4); - expect(successResults).to.eql([{ type, id: noConflictId, newId }]); + const destinationId = successResults![0].destinationId; + expect(destinationId).to.match(v4); + expect(successResults).to.eql([{ type, id: noConflictId, destinationId }]); expect(errors).to.be(undefined); } else if (outcome === 'noAccess') { expectNotFoundResponse(response); @@ -426,11 +426,11 @@ export function copyToSpaceTestSuiteFactory( response: async (response: TestResponse) => { if (outcome === 'authorized') { const { success, successCount, successResults, errors } = getResult(response); - const newId = 'conflict_1_space_2'; + const destinationId = 'conflict_1_space_2'; if (overwrite) { expect(success).to.eql(true); expect(successCount).to.eql(1); - expect(successResults).to.eql([{ type, id: inexactMatchId, newId }]); + expect(successResults).to.eql([{ type, id: inexactMatchId, destinationId }]); expect(errors).to.be(undefined); } else { expect(success).to.eql(false); @@ -438,7 +438,7 @@ export function copyToSpaceTestSuiteFactory( expect(successResults).to.be(undefined); expect(errors).to.eql([ { - error: { type: 'conflict', destinationId: newId }, + error: { type: 'conflict', destinationId }, type, id: inexactMatchId, title: 'A shared saved-object in one space', diff --git a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts index 47161fc238f8a..300e8b4b0bab0 100644 --- a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts +++ b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts @@ -299,12 +299,12 @@ export function resolveCopyToSpaceConflictsSuite( }, }); }; - const expectSuccessResponse = (response: TestResponse, id: string, newId?: string) => { + const expectSuccessResponse = (response: TestResponse, id: string, destinationId?: string) => { const { success, successCount, successResults, errors } = getResult(response); expect(success).to.eql(true); expect(successCount).to.eql(1); expect(errors).to.be(undefined); - expect(successResults).to.eql([{ type, id, ...(newId && { newId }) }]); + expect(successResults).to.eql([{ type, id, ...(destinationId && { destinationId }) }]); }; return [ From bb2c7d36bb31cbf9c318dd8f8f3aeba0b0a226fb Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Thu, 18 Jun 2020 14:57:21 -0400 Subject: [PATCH 19/55] Suppress "ambiguous source" conflict errors Now, if an ambiguous source conflict is detected, it will result in true copies being created (with a newly generated `id` and a reset `originId`). --- ...avedobjectsimportambiguousconflicterror.md | 1 - ...ctsimportambiguousconflicterror.sources.md | 15 ---- ...avedobjectsimportambiguousconflicterror.md | 1 - ...ctsimportambiguousconflicterror.sources.md | 15 ---- src/core/public/public.api.md | 6 -- .../import/check_conflicts.test.ts | 76 +++++++++++-------- .../saved_objects/import/check_conflicts.ts | 11 ++- .../import/create_saved_objects.test.ts | 8 +- .../import/create_saved_objects.ts | 4 +- src/core/server/saved_objects/import/types.ts | 1 - src/core/server/server.api.md | 6 -- .../common/suites/import.ts | 8 -- .../common/suites/resolve_import_errors.ts | 32 +++----- .../security_and_spaces/apis/import.ts | 8 +- .../security_and_spaces/apis/index.ts | 18 ++--- .../apis/resolve_import_errors.ts | 9 +-- .../security_only/apis/import.ts | 8 +- .../apis/resolve_import_errors.ts | 9 +-- .../spaces_only/apis/import.ts | 8 +- .../spaces_only/apis/resolve_import_errors.ts | 9 +-- .../common/suites/copy_to_space.ts | 5 +- 21 files changed, 101 insertions(+), 157 deletions(-) delete mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.sources.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.sources.md diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.md index e0ecf7ae3fc58..76dfacf132f0a 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.md @@ -17,6 +17,5 @@ export interface SavedObjectsImportAmbiguousConflictError | Property | Type | Description | | --- | --- | --- | | [destinations](./kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.destinations.md) | Array<{
id: string;
title?: string;
updatedAt?: string;
}> | | -| [sources](./kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.sources.md) | Array<{
id: string;
title?: string;
updatedAt?: string;
}> | | | [type](./kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.type.md) | 'ambiguous_conflict' | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.sources.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.sources.md deleted file mode 100644 index f0d1dbd9c3efb..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.sources.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportAmbiguousConflictError](./kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.md) > [sources](./kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.sources.md) - -## SavedObjectsImportAmbiguousConflictError.sources property - -Signature: - -```typescript -sources: Array<{ - id: string; - title?: string; - updatedAt?: string; - }>; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.md index 2b32825354364..d2c0a397ebe8a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.md @@ -17,6 +17,5 @@ export interface SavedObjectsImportAmbiguousConflictError | Property | Type | Description | | --- | --- | --- | | [destinations](./kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.destinations.md) | Array<{
id: string;
title?: string;
updatedAt?: string;
}> | | -| [sources](./kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.sources.md) | Array<{
id: string;
title?: string;
updatedAt?: string;
}> | | | [type](./kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.type.md) | 'ambiguous_conflict' | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.sources.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.sources.md deleted file mode 100644 index 4edefec1ad100..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.sources.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportAmbiguousConflictError](./kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.md) > [sources](./kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.sources.md) - -## SavedObjectsImportAmbiguousConflictError.sources property - -Signature: - -```typescript -sources: Array<{ - id: string; - title?: string; - updatedAt?: string; - }>; -``` diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 1cf00a3c33dab..203a25f3f7bd4 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -1320,12 +1320,6 @@ export interface SavedObjectsImportAmbiguousConflictError { updatedAt?: string; }>; // (undocumented) - sources: Array<{ - id: string; - title?: string; - updatedAt?: string; - }>; - // (undocumented) type: 'ambiguous_conflict'; } diff --git a/src/core/server/saved_objects/import/check_conflicts.test.ts b/src/core/server/saved_objects/import/check_conflicts.test.ts index be478ad6a073a..7d7b14eb6b1a9 100644 --- a/src/core/server/saved_objects/import/check_conflicts.test.ts +++ b/src/core/server/saved_objects/import/check_conflicts.test.ts @@ -17,6 +17,7 @@ * under the License. */ +import { mockUuidv4 } from './__mocks__'; import { SavedObjectsClientContract, SavedObjectReference, @@ -47,6 +48,10 @@ const createObject = (type: string, id: string, originId?: string): SavedObjectT const MULTI_NS_TYPE = 'multi'; const OTHER_TYPE = 'other'; +beforeEach(() => { + mockUuidv4.mockClear(); +}); + describe('#checkConflicts', () => { let savedObjectsClient: jest.Mocked; let typeRegistry: jest.Mocked; @@ -151,7 +156,6 @@ describe('#checkConflicts', () => { .sort((a: { id: string }, b: { id: string }) => (a.id > b.id ? 1 : b.id > a.id ? -1 : 0)); const createAmbiguousConflictError = ( object: SavedObjectType, - sources: SavedObjectType[], destinations: SavedObjectType[] ): SavedObjectsImportError => ({ type: object.type, @@ -159,7 +163,6 @@ describe('#checkConflicts', () => { title: object.attributes?.title, error: { type: 'ambiguous_conflict', - sources: getAmbiguousConflicts(sources), destinations: getAmbiguousConflicts(destinations), }, }); @@ -320,8 +323,8 @@ describe('#checkConflicts', () => { }); }); - describe('error result (ambiguous conflict)', () => { - test('returns ambiguous_conflict error when multiple inexact matches are detected that target the same single destination', async () => { + describe('ambiguous conflicts', () => { + test('returns object with a `importIdMap` entry when multiple inexact matches are detected that target the same single destination', async () => { // objA and objB exist in this space // try to import obj1, obj2, obj3, and obj4 const obj1 = createObject(MULTI_NS_TYPE, 'id-1'); @@ -338,15 +341,16 @@ describe('#checkConflicts', () => { const checkConflictsResult = await checkConflicts([obj1, obj2, obj3, obj4], options); const expectedResult = { - filteredObjects: [], - importIdMap: new Map(), - errors: [ - createAmbiguousConflictError(obj1, [obj1, obj2], [objA]), - createAmbiguousConflictError(obj2, [obj1, obj2], [objA]), - createAmbiguousConflictError(obj3, [obj3, obj4], [objB]), - createAmbiguousConflictError(obj4, [obj3, obj4], [objB]), - ], + filteredObjects: [obj1, obj2, obj3, obj4], + importIdMap: new Map([ + [`${obj1.type}:${obj1.id}`, { id: 'uuidv4', omitOriginId: true }], + [`${obj2.type}:${obj2.id}`, { id: 'uuidv4', omitOriginId: true }], + [`${obj3.type}:${obj3.id}`, { id: 'uuidv4', omitOriginId: true }], + [`${obj4.type}:${obj4.id}`, { id: 'uuidv4', omitOriginId: true }], + ]), + errors: [], }; + expect(mockUuidv4).toHaveBeenCalledTimes(4); expect(checkConflictsResult).toEqual(expectedResult); }); @@ -368,14 +372,15 @@ describe('#checkConflicts', () => { filteredObjects: [], importIdMap: new Map(), errors: [ - createAmbiguousConflictError(obj1, [obj1], [objA, objB]), - createAmbiguousConflictError(obj2, [obj2], [objC, objD]), + createAmbiguousConflictError(obj1, [objA, objB]), + createAmbiguousConflictError(obj2, [objC, objD]), ], }; + expect(mockUuidv4).not.toHaveBeenCalled(); expect(checkConflictsResult).toEqual(expectedResult); }); - test('returns ambiguous_conflict error when multiple inexact matches are detected that target the same multiple destinations', async () => { + test('returns object with a `importIdMap` entry when multiple inexact matches are detected that target the same multiple destinations', async () => { // objA, objB, objC, and objD exist in this space // try to import obj1, obj2, obj3, and obj4 const obj1 = createObject(MULTI_NS_TYPE, 'id-1'); @@ -394,15 +399,16 @@ describe('#checkConflicts', () => { const checkConflictsResult = await checkConflicts([obj1, obj2, obj3, obj4], options); const expectedResult = { - filteredObjects: [], - importIdMap: new Map(), - errors: [ - createAmbiguousConflictError(obj1, [obj1, obj2], [objA, objB]), - createAmbiguousConflictError(obj2, [obj1, obj2], [objA, objB]), - createAmbiguousConflictError(obj3, [obj3, obj4], [objC, objD]), - createAmbiguousConflictError(obj4, [obj3, obj4], [objC, objD]), - ], + filteredObjects: [obj1, obj2, obj3, obj4], + importIdMap: new Map([ + [`${obj1.type}:${obj1.id}`, { id: 'uuidv4', omitOriginId: true }], + [`${obj2.type}:${obj2.id}`, { id: 'uuidv4', omitOriginId: true }], + [`${obj3.type}:${obj3.id}`, { id: 'uuidv4', omitOriginId: true }], + [`${obj4.type}:${obj4.id}`, { id: 'uuidv4', omitOriginId: true }], + ]), + errors: [], }; + expect(mockUuidv4).toHaveBeenCalledTimes(4); expect(checkConflictsResult).toEqual(expectedResult); }); }); @@ -417,10 +423,13 @@ describe('#checkConflicts', () => { const obj4 = createObject(MULTI_NS_TYPE, 'id-4', obj3.id); const obj5 = createObject(MULTI_NS_TYPE, 'id-5'); const obj6 = createObject(MULTI_NS_TYPE, 'id-6'); - const obj7 = createObject(MULTI_NS_TYPE, 'id-7', obj6.id); + const obj7 = createObject(MULTI_NS_TYPE, 'id-7'); + const obj8 = createObject(MULTI_NS_TYPE, 'id-8', obj7.id); const objA = createObject(MULTI_NS_TYPE, 'id-A', obj5.id); const objB = createObject(MULTI_NS_TYPE, 'id-B', obj6.id); const objC = createObject(MULTI_NS_TYPE, 'id-C', obj6.id); + const objD = createObject(MULTI_NS_TYPE, 'id-D', obj7.id); + const objE = createObject(MULTI_NS_TYPE, 'id-E', obj7.id); const options = setupOptions(); // obj1 is a non-multi-namespace type, so it is skipped while searching mockFindResult(); // find for obj2: the result is no match @@ -428,18 +437,21 @@ describe('#checkConflicts', () => { mockFindResult(obj3); // find for obj4: the result is an inexact match with one destination that is exactly matched by obj3 so it is ignored -- accordingly, obj4 has no match mockFindResult(objA); // find for obj5: the result is an inexact match with one destination mockFindResult(objB, objC); // find for obj6: the result is an inexact match with two destinations - mockFindResult(objB, objC); // find for obj7: the result is an inexact match with two destinations + mockFindResult(objD, objE); // find for obj7: the result is an inexact match with two destinations + mockFindResult(objD, objE); // find for obj8: the result is an inexact match with two destinations - const objects = [obj1, obj2, obj3, obj4, obj5, obj6, obj7]; + const objects = [obj1, obj2, obj3, obj4, obj5, obj6, obj7, obj8]; const checkConflictsResult = await checkConflicts(objects, options); const expectedResult = { - filteredObjects: [obj1, obj2, obj3, obj4, obj5], - importIdMap: new Map([[`${obj5.type}:${obj5.id}`, { id: objA.id }]]), - errors: [ - createAmbiguousConflictError(obj6, [obj6, obj7], [objB, objC]), - createAmbiguousConflictError(obj7, [obj6, obj7], [objB, objC]), - ], + filteredObjects: [obj1, obj2, obj3, obj4, obj5, obj7, obj8], + importIdMap: new Map([ + [`${obj5.type}:${obj5.id}`, { id: objA.id }], + [`${obj7.type}:${obj7.id}`, { id: 'uuidv4', omitOriginId: true }], + [`${obj8.type}:${obj8.id}`, { id: 'uuidv4', omitOriginId: true }], + ]), + errors: [createAmbiguousConflictError(obj6, [objB, objC])], }; + expect(mockUuidv4).toHaveBeenCalledTimes(2); expect(checkConflictsResult).toEqual(expectedResult); }); }); diff --git a/src/core/server/saved_objects/import/check_conflicts.ts b/src/core/server/saved_objects/import/check_conflicts.ts index ee720a0326bbd..1320cdd682cce 100644 --- a/src/core/server/saved_objects/import/check_conflicts.ts +++ b/src/core/server/saved_objects/import/check_conflicts.ts @@ -17,6 +17,7 @@ * under the License. */ +import { v4 as uuidv4 } from 'uuid'; import { SavedObject, SavedObjectsClientContract, @@ -153,7 +154,7 @@ export async function checkConflicts( const errors: SavedObjectsImportError[] = []; const filteredObjects: Array> = []; - const importIdMap = new Map(); + const importIdMap = new Map(); checkConflictResults.forEach((result) => { if (!isLeft(result)) { filteredObjects.push(result.value); @@ -175,13 +176,19 @@ export async function checkConflicts( // - a single import object has 2+ destination conflicts ("ambiguous destination") // - 2+ import objects have the same single destination conflict ("ambiguous source") // - 2+ import objects have the same 2+ destination conflicts ("ambiguous source and destination") + if (sources.length > 1) { + // In the case of ambiguous source conflicts, don't treat them as errors; instead, regenerate the object ID and reset its origin + // (e.g., make a "true copy"). + importIdMap.set(`${type}:${id}`, { id: uuidv4(), omitOriginId: true }); + filteredObjects.push(object); + return; + } errors.push({ type, id, title: attributes?.title, error: { type: 'ambiguous_conflict', - sources, destinations, }, }); diff --git a/src/core/server/saved_objects/import/create_saved_objects.test.ts b/src/core/server/saved_objects/import/create_saved_objects.test.ts index 9dc2f07f74ee3..dda183d854bd7 100644 --- a/src/core/server/saved_objects/import/create_saved_objects.test.ts +++ b/src/core/server/saved_objects/import/create_saved_objects.test.ts @@ -45,7 +45,7 @@ const OTHER_TYPE = 'other'; */ const obj1 = createObject(MULTI_NS_TYPE, 'id-1', 'originId-a'); // -> success const obj2 = createObject(MULTI_NS_TYPE, 'id-2', 'originId-b'); // -> conflict -const obj3 = createObject(MULTI_NS_TYPE, 'id-3', 'originId-c'); // -> conflict (with known importId) +const obj3 = createObject(MULTI_NS_TYPE, 'id-3', 'originId-c'); // -> conflict (with known importId and omitOriginId=true) const obj4 = createObject(MULTI_NS_TYPE, 'id-4', 'originId-d'); // -> conflict (with known importId) const obj5 = createObject(MULTI_NS_TYPE, 'id-5', 'originId-e'); // -> unresolvable conflict const obj6 = createObject(MULTI_NS_TYPE, 'id-6'); // -> success @@ -62,7 +62,7 @@ const importId3 = 'id-foo'; const importId4 = 'id-bar'; const importId8 = 'id-baz'; const importIdMap = new Map([ - [`${obj3.type}:${obj3.id}`, { id: importId3 }], + [`${obj3.type}:${obj3.id}`, { id: importId3, omitOriginId: true }], [`${obj4.type}:${obj4.id}`, { id: importId4 }], [`${obj8.type}:${obj8.id}`, { id: importId8 }], ]); @@ -190,7 +190,7 @@ describe('#createSavedObjects', () => { await createSavedObjects(objs, options); expect(bulkCreate).toHaveBeenCalledTimes(1); // these three objects are transformed before being created, because they are included in the `importIdMap` - const x3 = { ...obj3, id: importId3 }; // this import object already has an originId + const x3 = { ...obj3, id: importId3, originId: undefined }; // this import object already has an originId, but the entry has omitOriginId=true const x4 = { ...obj4, id: importId4 }; // this import object already has an originId const x8 = { ...obj8, id: importId8, originId: obj8.id }; // this import object doesn't have an originId, so it is set before create const argObjs = [obj1, obj2, x3, x4, obj6, obj7, x8, obj10, obj11, obj12, obj13]; @@ -283,7 +283,7 @@ describe('#createSavedObjects', () => { await createSavedObjects(objs, options); expect(bulkCreate).toHaveBeenCalledTimes(2); // these three objects are transformed before being created, because they are included in the `importIdMap` - const x3 = { ...obj3, id: importId3 }; // this import object already has an originId + const x3 = { ...obj3, id: importId3, originId: undefined }; // this import object already has an originId, but the entry has omitOriginId=true const x4 = { ...obj4, id: importId4 }; // this import object already has an originId const x8 = { ...obj8, id: importId8, originId: obj8.id }; // this import object doesn't have an originId, so it is set before create const argObjs = [obj1, obj2, x3, x4, obj5, obj6, obj7, x8, obj9, obj10, obj11, obj12, obj13]; diff --git a/src/core/server/saved_objects/import/create_saved_objects.ts b/src/core/server/saved_objects/import/create_saved_objects.ts index 5364cdbd8e72d..40df11e949d8e 100644 --- a/src/core/server/saved_objects/import/create_saved_objects.ts +++ b/src/core/server/saved_objects/import/create_saved_objects.ts @@ -23,7 +23,7 @@ import { extractErrors } from './extract_errors'; interface CreateSavedObjectsOptions { savedObjectsClient: SavedObjectsClientContract; - importIdMap: Map; + importIdMap: Map; namespace?: string; overwrite?: boolean; } @@ -81,7 +81,7 @@ export const createSavedObjects = async ( const importIdEntry = importIdMap.get(`${object.type}:${object.id}`); if (importIdEntry) { objectIdMap.set(`${object.type}:${importIdEntry.id}`, object); - const originId = object.originId ?? object.id; + const originId = importIdEntry.omitOriginId ? undefined : object.originId ?? object.id; return { ...object, id: importIdEntry.id, originId }; } return object; diff --git a/src/core/server/saved_objects/import/types.ts b/src/core/server/saved_objects/import/types.ts index 3fe27ff0b762e..f2d63812a4c03 100644 --- a/src/core/server/saved_objects/import/types.ts +++ b/src/core/server/saved_objects/import/types.ts @@ -55,7 +55,6 @@ export interface SavedObjectsImportConflictError { */ export interface SavedObjectsImportAmbiguousConflictError { type: 'ambiguous_conflict'; - sources: Array<{ id: string; title?: string; updatedAt?: string }>; destinations: Array<{ id: string; title?: string; updatedAt?: string }>; } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index c9b5f6cd53111..51562dddfa20d 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2073,12 +2073,6 @@ export interface SavedObjectsImportAmbiguousConflictError { updatedAt?: string; }>; // (undocumented) - sources: Array<{ - id: string; - title?: string; - updatedAt?: string; - }>; - // (undocumented) type: 'ambiguous_conflict'; } diff --git a/x-pack/test/saved_object_api_integration/common/suites/import.ts b/x-pack/test/saved_object_api_integration/common/suites/import.ts index d53840bfc537c..f03f78c87dcb5 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/import.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/import.ts @@ -64,11 +64,6 @@ const createRequest = ({ type, id, originId }: ImportTestCase) => ({ ...(originId && { originId }), }); -const getConflictSource = (id: string) => ({ - id, - title: NEW_ATTRIBUTE_VAL, - // our source objects being imported does not include the `updatedAt` field (though they could) -}); const getConflictDest = (id: string) => ({ id, title: 'A shared saved-object in all spaces', @@ -147,21 +142,18 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe // "ambiguous source" conflict error = { type: 'ambiguous_conflict', - sources: [getConflictSource(`${CID}1a`), getConflictSource(`${CID}1b`)], destinations: [getConflictDest(`${CID}1`)], }; } else if (fail409Param === 'ambiguous_conflict_2c') { // "ambiguous destination" conflict error = { type: 'ambiguous_conflict', - sources: [getConflictSource(`${CID}2c`)], destinations: [getConflictDest(`${CID}2a`), getConflictDest(`${CID}2b`)], }; } else if (fail409Param === 'ambiguous_conflict_2c2d') { // "ambiguous source and destination" conflict error = { type: 'ambiguous_conflict', - sources: [getConflictSource(`${CID}2c`), getConflictSource(`${CID}2d`)], destinations: [getConflictDest(`${CID}2a`), getConflictDest(`${CID}2b`)], }; } diff --git a/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts index 519d49167e37b..cc7ab600b9b67 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts @@ -37,36 +37,26 @@ const NEW_ATTRIBUTE_VAL = `New attribute value ${Date.now()}`; // * id: conflict_2b, originId: conflict_2 // * id: conflict_3 // * id: conflict_4a, originId: conflict_4 -// using the six conflict test case objects below, we can exercise various permutations of exact/inexact/ambiguous conflict scenarios -const CID = 'conflict_'; +// using the three conflict test case objects below, we can exercise various permutations of exact/inexact/ambiguous conflict scenarios export const TEST_CASES = Object.freeze({ ...CASES, - CONFLICT_1A_OBJ: Object.freeze({ - type: 'sharedtype', - id: `${CID}1a`, - originId: `${CID}1`, - idToOverwrite: `${CID}1`, - }), - CONFLICT_1B_OBJ: Object.freeze({ type: 'sharedtype', id: `${CID}1b`, originId: `${CID}1` }), CONFLICT_2C_OBJ: Object.freeze({ type: 'sharedtype', - id: `${CID}2c`, - originId: `${CID}2`, - idToOverwrite: `${CID}2a`, + id: `conflict_2c`, + originId: `conflict_2`, + idToOverwrite: `conflict_2a`, }), - CONFLICT_2D_OBJ: Object.freeze({ + CONFLICT_3A_OBJ: Object.freeze({ type: 'sharedtype', - id: `${CID}2d`, - originId: `${CID}2`, - idToOverwrite: `${CID}2b`, + id: `conflict_3a`, + originId: `conflict_3`, + idToOverwrite: `conflict_3`, }), - CONFLICT_3A_OBJ: Object.freeze({ + CONFLICT_4_OBJ: Object.freeze({ type: 'sharedtype', - id: `${CID}3a`, - originId: `${CID}3`, - idToOverwrite: `${CID}3`, + id: `conflict_4`, + idToOverwrite: `conflict_4a`, }), - CONFLICT_4_OBJ: Object.freeze({ type: 'sharedtype', id: `${CID}4`, idToOverwrite: `${CID}4a` }), }); /** diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts index 14850a4483353..90bcdd5824786 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts @@ -60,8 +60,8 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { ...fail409(!overwrite && spaceId === SPACE_2_ID), ...destinationId(spaceId !== SPACE_2_ID), }, - { ...CASES.CONFLICT_1A_OBJ, ...ambiguousConflict('1a1b') }, // "ambiguous source" conflict - { ...CASES.CONFLICT_1B_OBJ, ...ambiguousConflict('1a1b') }, // "ambiguous source" conflict + { ...CASES.CONFLICT_1A_OBJ, ...destinationId() }, // "ambiguous source" conflict which results in a new destination ID + { ...CASES.CONFLICT_1B_OBJ, ...destinationId() }, // "ambiguous source" conflict which results in a new destination ID { ...CASES.CONFLICT_2C_OBJ, ...ambiguousConflict('2c') }, // "ambiguous destination" conflict { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict @@ -70,8 +70,8 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { { ...CASES.CONFLICT_1_OBJ, ...fail409(!overwrite) }, // "exact match" conflict CASES.CONFLICT_1A_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match CASES.CONFLICT_1B_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match - { ...CASES.CONFLICT_2C_OBJ, ...ambiguousConflict('2c2d') }, // "ambiguous source and destination" conflict - { ...CASES.CONFLICT_2D_OBJ, ...ambiguousConflict('2c2d') }, // "ambiguous source and destination" conflict + { ...CASES.CONFLICT_2C_OBJ, ...destinationId() }, // "ambiguous source and destination" conflict which results in a new destination ID + { ...CASES.CONFLICT_2D_OBJ, ...destinationId() }, // "ambiguous source and destination" conflict which results in a new destination ID ]; return { group1Importable, group1NonImportable, group1All, group2, group3 }; }; diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts index 81ffc5eea9220..b75f88ff596c0 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts @@ -18,16 +18,16 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await createUsersAndRoles(es, supertest); }); - loadTestFile(require.resolve('./bulk_create')); - loadTestFile(require.resolve('./bulk_get')); - loadTestFile(require.resolve('./bulk_update')); - loadTestFile(require.resolve('./create')); - loadTestFile(require.resolve('./delete')); - loadTestFile(require.resolve('./export')); - loadTestFile(require.resolve('./find')); - loadTestFile(require.resolve('./get')); + // loadTestFile(require.resolve('./bulk_create')); + // loadTestFile(require.resolve('./bulk_get')); + // loadTestFile(require.resolve('./bulk_update')); + // loadTestFile(require.resolve('./create')); + // loadTestFile(require.resolve('./delete')); + // loadTestFile(require.resolve('./export')); + // loadTestFile(require.resolve('./find')); + // loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./import')); loadTestFile(require.resolve('./resolve_import_errors')); - loadTestFile(require.resolve('./update')); + // loadTestFile(require.resolve('./update')); }); } diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts index bf1cad80e8b94..a61d7cd7ffb7b 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts @@ -55,12 +55,9 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { ...destinationId(spaceId !== SPACE_2_ID), }, // all of the cases below represent imports that had an inexact match conflict or an ambiguous conflict - // if we call _resolve_import_errors and don't specify overwrite or duplicate, each of these will not result in a conflict because they - // will skip the preflight search results; so the objects will be created instead. - { ...CASES.CONFLICT_1A_OBJ, ...destinationId(overwrite) }, // "ambiguous source" conflict; if overwrite=true, will overwrite 'conflict_1' - CASES.CONFLICT_1B_OBJ, // "ambiguous source" conflict; if overwrite=true, will create a new object (since 'conflict_1a' is overwriting 'conflict_1') - { ...CASES.CONFLICT_2C_OBJ, ...destinationId(overwrite) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2a' - { ...CASES.CONFLICT_2D_OBJ, ...destinationId(overwrite) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2b' + // if we call _resolve_import_errors and don't specify overwrite, each of these will not result in a conflict because they will skip the + // preflight search results; so the objects will be created instead. + { ...CASES.CONFLICT_2C_OBJ, ...destinationId(overwrite) }, // "ambiguous destination" conflict; if overwrite=true, will overwrite 'conflict_2a' { ...CASES.CONFLICT_3A_OBJ, ...destinationId(overwrite) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3' { ...CASES.CONFLICT_4_OBJ, ...destinationId(overwrite) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a' ]; diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/import.ts b/x-pack/test/saved_object_api_integration/security_only/apis/import.ts index 3c957111bf717..bd29367aaeeeb 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/import.ts @@ -39,8 +39,8 @@ const createTestCases = (overwrite: boolean) => { { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite) }, { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...destinationId() }, { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...destinationId() }, - { ...CASES.CONFLICT_1A_OBJ, ...ambiguousConflict('1a1b') }, // "ambiguous source" conflict - { ...CASES.CONFLICT_1B_OBJ, ...ambiguousConflict('1a1b') }, // "ambiguous source" conflict + { ...CASES.CONFLICT_1A_OBJ, ...destinationId() }, // "ambiguous source" conflict which results in a new destination ID + { ...CASES.CONFLICT_1B_OBJ, ...destinationId() }, // "ambiguous source" conflict which results in a new destination ID { ...CASES.CONFLICT_2C_OBJ, ...ambiguousConflict('2c') }, // "ambiguous destination" conflict { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict @@ -49,8 +49,8 @@ const createTestCases = (overwrite: boolean) => { { ...CASES.CONFLICT_1_OBJ, ...fail409(!overwrite) }, // "exact match" conflict CASES.CONFLICT_1A_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match CASES.CONFLICT_1B_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match - { ...CASES.CONFLICT_2C_OBJ, ...ambiguousConflict('2c2d') }, // "ambiguous source and destination" conflict - { ...CASES.CONFLICT_2D_OBJ, ...ambiguousConflict('2c2d') }, // "ambiguous source and destination" conflict + { ...CASES.CONFLICT_2C_OBJ, ...destinationId() }, // "ambiguous source and destination" conflict which results in a new destination ID + { ...CASES.CONFLICT_2D_OBJ, ...destinationId() }, // "ambiguous source and destination" conflict which results in a new destination ID ]; return { group1Importable, group1NonImportable, group1All, group2, group3 }; }; diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts index 77235690c8784..0f4fd66939ca6 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts @@ -29,12 +29,9 @@ const createTestCases = (overwrite: boolean) => { const group2 = [ { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite) }, // all of the cases below represent imports that had an inexact match conflict or an ambiguous conflict - // if we call _resolve_import_errors and don't specify overwrite or duplicate, each of these will not result in a conflict because they - // will skip the preflight search results; so the objects will be created instead. - { ...CASES.CONFLICT_1A_OBJ, ...destinationId(overwrite) }, // "ambiguous source" conflict; if overwrite=true, will overwrite 'conflict_1' - CASES.CONFLICT_1B_OBJ, // "ambiguous source" conflict; if overwrite=true, will create a new object (since 'conflict_1a' is overwriting 'conflict_1') - { ...CASES.CONFLICT_2C_OBJ, ...destinationId(overwrite) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2a' - { ...CASES.CONFLICT_2D_OBJ, ...destinationId(overwrite) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2b' + // if we call _resolve_import_errors and don't specify overwrite, each of these will not result in a conflict because they will skip the + // preflight search results; so the objects will be created instead. + { ...CASES.CONFLICT_2C_OBJ, ...destinationId(overwrite) }, // "ambiguous destination" conflict; if overwrite=true, will overwrite 'conflict_2a' { ...CASES.CONFLICT_3A_OBJ, ...destinationId(overwrite) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3' { ...CASES.CONFLICT_4_OBJ, ...destinationId(overwrite) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a' ]; diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts index 22d5c79ec3b05..91edd1088129a 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts @@ -49,8 +49,8 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { }, { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, { ...CASES.HIDDEN, ...fail400() }, - { ...CASES.CONFLICT_1A_OBJ, ...ambiguousConflict('1a1b') }, // "ambiguous source" conflict - { ...CASES.CONFLICT_1B_OBJ, ...ambiguousConflict('1a1b') }, // "ambiguous source" conflict + { ...CASES.CONFLICT_1A_OBJ, ...destinationId() }, // "ambiguous source" conflict which results in a new destination ID + { ...CASES.CONFLICT_1B_OBJ, ...destinationId() }, // "ambiguous source" conflict which results in a new destination ID { ...CASES.CONFLICT_2C_OBJ, ...ambiguousConflict('2c') }, // "ambiguous destination" conflict { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict @@ -62,8 +62,8 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { { ...CASES.CONFLICT_1_OBJ, ...fail409(!overwrite) }, // "exact match" conflict CASES.CONFLICT_1A_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match CASES.CONFLICT_1B_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match - { ...CASES.CONFLICT_2C_OBJ, ...ambiguousConflict('2c2d') }, // "ambiguous source and destination" conflict - { ...CASES.CONFLICT_2D_OBJ, ...ambiguousConflict('2c2d') }, // "ambiguous source and destination" conflict + { ...CASES.CONFLICT_2C_OBJ, ...destinationId() }, // "ambiguous source and destination" conflict which results in a new destination ID + { ...CASES.CONFLICT_2D_OBJ, ...destinationId() }, // "ambiguous source and destination" conflict which results in a new destination ID ]; return { group1, group2 }; }; diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts index 2ccbfce91a123..b8034c053113d 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts @@ -50,12 +50,9 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, { ...CASES.HIDDEN, ...fail400() }, // all of the cases below represent imports that had an inexact match conflict or an ambiguous conflict - // if we call _resolve_import_errors and don't specify overwrite or duplicate, each of these will not result in a conflict because they - // will skip the preflight search results; so the objects will be created instead. - { ...CASES.CONFLICT_1A_OBJ, ...destinationId(overwrite) }, // "ambiguous source" conflict; if overwrite=true, will overwrite 'conflict_1' - CASES.CONFLICT_1B_OBJ, // "ambiguous source" conflict; if overwrite=true, will create a new object (since 'conflict_1a' is overwriting 'conflict_1') - { ...CASES.CONFLICT_2C_OBJ, ...destinationId(overwrite) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2a' - { ...CASES.CONFLICT_2D_OBJ, ...destinationId(overwrite) }, // "ambiguous source and destination" conflict; if overwrite=true, will overwrite 'conflict_2b' + // if we call _resolve_import_errors and don't specify overwrite, each of these will not result in a conflict because they will skip the + // preflight search results; so the objects will be created instead. + { ...CASES.CONFLICT_2C_OBJ, ...destinationId(overwrite) }, // "ambiguous destination" conflict; if overwrite=true, will overwrite 'conflict_2a' { ...CASES.CONFLICT_3A_OBJ, ...destinationId(overwrite) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3' { ...CASES.CONFLICT_4_OBJ, ...destinationId(overwrite) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a' ]; diff --git a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts index f7543589f97da..fd0bb9a967505 100644 --- a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts @@ -463,9 +463,6 @@ export function copyToSpaceTestSuiteFactory( // because of that, a consumer who is authorized to read (but not write) will see the same response as a user who is authorized const { success, successCount, successResults, errors } = getResult(response); const updatedAt = '2017-09-21T18:59:16.270Z'; - const sources = [ - { id: ambiguousConflictId, title: 'A shared saved-object in one space', updatedAt }, - ]; const destinations = [ // response should be sorted by ID in ascending order { id: 'conflict_2_all', title: 'A shared saved-object in all spaces', updatedAt }, @@ -476,7 +473,7 @@ export function copyToSpaceTestSuiteFactory( expect(successResults).to.be(undefined); expect(errors).to.eql([ { - error: { type: 'ambiguous_conflict', sources, destinations }, + error: { type: 'ambiguous_conflict', destinations }, type, id: ambiguousConflictId, title: 'A shared saved-object in one space', From 76885b221e455ebef0d17f83125120823007b391 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 23 Jun 2020 15:51:11 -0400 Subject: [PATCH 20/55] Unskip tests These were accidentally skipped in the prior commit! --- .../security_and_spaces/apis/index.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts index b75f88ff596c0..81ffc5eea9220 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts @@ -18,16 +18,16 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await createUsersAndRoles(es, supertest); }); - // loadTestFile(require.resolve('./bulk_create')); - // loadTestFile(require.resolve('./bulk_get')); - // loadTestFile(require.resolve('./bulk_update')); - // loadTestFile(require.resolve('./create')); - // loadTestFile(require.resolve('./delete')); - // loadTestFile(require.resolve('./export')); - // loadTestFile(require.resolve('./find')); - // loadTestFile(require.resolve('./get')); + loadTestFile(require.resolve('./bulk_create')); + loadTestFile(require.resolve('./bulk_get')); + loadTestFile(require.resolve('./bulk_update')); + loadTestFile(require.resolve('./create')); + loadTestFile(require.resolve('./delete')); + loadTestFile(require.resolve('./export')); + loadTestFile(require.resolve('./find')); + loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./import')); loadTestFile(require.resolve('./resolve_import_errors')); - // loadTestFile(require.resolve('./update')); + loadTestFile(require.resolve('./update')); }); } From f8776d199251497795062dc4c172369fe8b3f9f1 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 23 Jun 2020 15:52:15 -0400 Subject: [PATCH 21/55] Rename `import/check_conflicts` sub-module Now that the SavedObjectsClient exposes a `checkConflicts` method, I've renamed the sub-module in `import` to `checkOriginConflicts` to be more descriptive. `checkConflicts` checks exact match conflicts across all namespaces (which can result in an "unresolvable conflict"). On the other hand, `checkOriginConflicts` checks inexact match conflicts in the current namespace (which can result in a regular conflict or an "ambiguous conflict"). --- ...test.ts => check_origin_conflicts.test.ts} | 83 +++++++++++-------- ...conflicts.ts => check_origin_conflicts.ts} | 28 ++++--- .../import/create_saved_objects.ts | 6 +- .../import/import_saved_objects.test.ts | 25 ++++-- .../import/import_saved_objects.ts | 8 +- .../import/resolve_import_errors.test.ts | 8 +- .../import/resolve_import_errors.ts | 2 +- 7 files changed, 93 insertions(+), 67 deletions(-) rename src/core/server/saved_objects/import/{check_conflicts.test.ts => check_origin_conflicts.test.ts} (89%) rename src/core/server/saved_objects/import/{check_conflicts.ts => check_origin_conflicts.ts} (92%) diff --git a/src/core/server/saved_objects/import/check_conflicts.test.ts b/src/core/server/saved_objects/import/check_origin_conflicts.test.ts similarity index 89% rename from src/core/server/saved_objects/import/check_conflicts.test.ts rename to src/core/server/saved_objects/import/check_origin_conflicts.test.ts index 7d7b14eb6b1a9..396f3becfed6d 100644 --- a/src/core/server/saved_objects/import/check_conflicts.test.ts +++ b/src/core/server/saved_objects/import/check_origin_conflicts.test.ts @@ -25,13 +25,13 @@ import { SavedObjectsImportRetry, SavedObjectsImportError, } from '../types'; -import { checkConflicts, getImportIdMapForRetries } from './check_conflicts'; +import { checkOriginConflicts, getImportIdMapForRetries } from './check_origin_conflicts'; import { savedObjectsClientMock } from '../../mocks'; import { typeRegistryMock } from '../saved_objects_type_registry.mock'; import { ISavedObjectTypeRegistry } from '..'; type SavedObjectType = SavedObject<{ title?: string }>; -type CheckConflictsOptions = Parameters[1]; +type CheckOriginConflictsOptions = Parameters[1]; type GetImportIdMapForRetriesOptions = Parameters[1]; /** @@ -52,7 +52,7 @@ beforeEach(() => { mockUuidv4.mockClear(); }); -describe('#checkConflicts', () => { +describe('#checkOriginConflicts', () => { let savedObjectsClient: jest.Mocked; let typeRegistry: jest.Mocked; let find: typeof savedObjectsClient['find']; @@ -64,7 +64,7 @@ describe('#checkConflicts', () => { saved_objects: objects, }); - const setupOptions = (namespace?: string): CheckConflictsOptions => { + const setupOptions = (namespace?: string): CheckOriginConflictsOptions => { savedObjectsClient = savedObjectsClientMock.create(); find = savedObjectsClient.find; find.mockResolvedValue(getResultMock()); // mock zero hits response by default @@ -97,7 +97,7 @@ describe('#checkConflicts', () => { const objects = [otherObj, otherObjWithOriginId]; const options = setupOptions(); - await checkConflicts(objects, options); + await checkOriginConflicts(objects, options); expect(find).not.toHaveBeenCalled(); }); @@ -105,14 +105,14 @@ describe('#checkConflicts', () => { const objects = [multiNsObj, otherObj, multiNsObjWithOriginId, otherObjWithOriginId]; const options1 = setupOptions(); - await checkConflicts(objects, options1); + await checkOriginConflicts(objects, options1); expect(find).toHaveBeenCalledTimes(2); expectFindArgs(1, multiNsObj, ''); expectFindArgs(2, multiNsObjWithOriginId, ''); find.mockClear(); const options2 = setupOptions('some-namespace'); - await checkConflicts(objects, options2); + await checkOriginConflicts(objects, options2); expect(find).toHaveBeenCalledTimes(2); expectFindArgs(1, multiNsObj, 'some-namespace:'); expectFindArgs(2, multiNsObjWithOriginId, 'some-namespace:'); @@ -123,7 +123,7 @@ describe('#checkConflicts', () => { const namespace = 'some-namespace'; const options = setupOptions(namespace); - await checkConflicts(objects, options); + await checkOriginConflicts(objects, options); expect(find).toHaveBeenCalledTimes(1); expect(find).toHaveBeenCalledWith(expect.objectContaining({ namespace })); }); @@ -136,7 +136,7 @@ describe('#checkConflicts', () => { ]; const options = setupOptions(); - await checkConflicts(objects, options); + await checkOriginConflicts(objects, options); const escapedId = `some\\"weird\\\\id`; const expectedQuery = `"${MULTI_NS_TYPE}:${escapedId}" | "${escapedId}"`; expect(find).toHaveBeenCalledTimes(2); @@ -178,14 +178,17 @@ describe('#checkConflicts', () => { const options = setupOptions(); // don't need to mock find results for obj3 and obj4, "no match" is the default find result in this test suite - const checkConflictsResult = await checkConflicts([obj1, obj2, obj3, obj4], options); + const checkOriginConflictsResult = await checkOriginConflicts( + [obj1, obj2, obj3, obj4], + options + ); const expectedResult = { filteredObjects: [obj1, obj2, obj3, obj4], importIdMap: new Map(), errors: [], }; - expect(checkConflictsResult).toEqual(expectedResult); + expect(checkOriginConflictsResult).toEqual(expectedResult); }); test('returns object when an exact match is detected (1 hit)', async () => { @@ -197,13 +200,13 @@ describe('#checkConflicts', () => { mockFindResult(obj1); // find for obj1: the result is an exact match mockFindResult(obj2); // find for obj2: the result is an exact match - const checkConflictsResult = await checkConflicts([obj1, obj2], options); + const checkOriginConflictsResult = await checkOriginConflicts([obj1, obj2], options); const expectedResult = { filteredObjects: [obj1, obj2], importIdMap: new Map(), errors: [], }; - expect(checkConflictsResult).toEqual(expectedResult); + expect(checkOriginConflictsResult).toEqual(expectedResult); }); test('returns object when an exact match is detected (2+ hits)', async () => { @@ -217,13 +220,13 @@ describe('#checkConflicts', () => { mockFindResult(obj1, objA); // find for obj1: the first result is an exact match, so the second result is ignored mockFindResult(objB, obj2); // find for obj2: the second result is an exact match, so the first result is ignored - const checkConflictsResult = await checkConflicts([obj1, obj2], options); + const checkOriginConflictsResult = await checkOriginConflicts([obj1, obj2], options); const expectedResult = { filteredObjects: [obj1, obj2], importIdMap: new Map(), errors: [], }; - expect(checkConflictsResult).toEqual(expectedResult); + expect(checkOriginConflictsResult).toEqual(expectedResult); }); test('returns object when an inexact match is detected (1 hit) with a destination that is exactly matched by another object', async () => { @@ -240,13 +243,16 @@ describe('#checkConflicts', () => { mockFindResult(obj3); // find for obj3: the result is an exact match mockFindResult(obj3); // find for obj4: the result is an inexact match with one destination that is exactly matched by obj3 so it is ignored -- accordingly, obj4 has no match - const checkConflictsResult = await checkConflicts([obj1, obj2, obj3, obj4], options); + const checkOriginConflictsResult = await checkOriginConflicts( + [obj1, obj2, obj3, obj4], + options + ); const expectedResult = { filteredObjects: [obj1, obj2, obj3, obj4], importIdMap: new Map(), errors: [], }; - expect(checkConflictsResult).toEqual(expectedResult); + expect(checkOriginConflictsResult).toEqual(expectedResult); }); test('returns object when an inexact match is detected (2+ hits) with destinations that are all exactly matched by another object', async () => { @@ -260,13 +266,13 @@ describe('#checkConflicts', () => { mockFindResult(obj1, obj2); // find for obj2: the second result is an exact match, so the first result is ignored mockFindResult(obj1, obj2); // find for obj3: the result is an inexact match with two destinations that are exactly matched by obj1 and obj2 so they are ignored -- accordingly, obj3 has no match - const checkConflictsResult = await checkConflicts([obj1, obj2, obj3], options); + const checkOriginConflictsResult = await checkOriginConflicts([obj1, obj2, obj3], options); const expectedResult = { filteredObjects: [obj1, obj2, obj3], importIdMap: new Map(), errors: [], }; - expect(checkConflictsResult).toEqual(expectedResult); + expect(checkOriginConflictsResult).toEqual(expectedResult); }); }); @@ -282,7 +288,7 @@ describe('#checkConflicts', () => { mockFindResult(objA); // find for obj1: the result is an inexact match with one destination mockFindResult(objB); // find for obj2: the result is an inexact match with one destination - const checkConflictsResult = await checkConflicts([obj1, obj2], options); + const checkOriginConflictsResult = await checkOriginConflicts([obj1, obj2], options); const expectedResult = { filteredObjects: [obj1, obj2], importIdMap: new Map([ @@ -291,7 +297,7 @@ describe('#checkConflicts', () => { ]), errors: [], }; - expect(checkConflictsResult).toEqual(expectedResult); + expect(checkOriginConflictsResult).toEqual(expectedResult); }); test('returns object with a `importIdMap` entry when an inexact match is detected (2+ hits), with n-1 destinations that are exactly matched by another object', async () => { @@ -310,7 +316,10 @@ describe('#checkConflicts', () => { mockFindResult(objB, obj3); // find for obj3: the second result is an exact match, so the first result is ignored mockFindResult(objB, obj3); // find for obj4: the result is an inexact match with two destinations, but the second destination is exactly matched by obj3 so it is ignored -- accordingly, obj4 has an inexact match with one destination (objB) - const checkConflictsResult = await checkConflicts([obj1, obj2, obj3, obj4], options); + const checkOriginConflictsResult = await checkOriginConflicts( + [obj1, obj2, obj3, obj4], + options + ); const expectedResult = { filteredObjects: [obj1, obj2, obj3, obj4], importIdMap: new Map([ @@ -319,7 +328,7 @@ describe('#checkConflicts', () => { ]), errors: [], }; - expect(checkConflictsResult).toEqual(expectedResult); + expect(checkOriginConflictsResult).toEqual(expectedResult); }); }); @@ -339,7 +348,10 @@ describe('#checkConflicts', () => { mockFindResult(objB); // find for obj3: the result is an inexact match with one destination mockFindResult(objB); // find for obj4: the result is an inexact match with one destination - const checkConflictsResult = await checkConflicts([obj1, obj2, obj3, obj4], options); + const checkOriginConflictsResult = await checkOriginConflicts( + [obj1, obj2, obj3, obj4], + options + ); const expectedResult = { filteredObjects: [obj1, obj2, obj3, obj4], importIdMap: new Map([ @@ -351,7 +363,7 @@ describe('#checkConflicts', () => { errors: [], }; expect(mockUuidv4).toHaveBeenCalledTimes(4); - expect(checkConflictsResult).toEqual(expectedResult); + expect(checkOriginConflictsResult).toEqual(expectedResult); }); test('returns ambiguous_conflict error when an inexact match is detected (2+ hits)', async () => { @@ -367,7 +379,7 @@ describe('#checkConflicts', () => { mockFindResult(objA, objB); // find for obj1: the result is an inexact match with two destinations mockFindResult(objC, objD); // find for obj2: the result is an inexact match with two destinations - const checkConflictsResult = await checkConflicts([obj1, obj2], options); + const checkOriginConflictsResult = await checkOriginConflicts([obj1, obj2], options); const expectedResult = { filteredObjects: [], importIdMap: new Map(), @@ -377,7 +389,7 @@ describe('#checkConflicts', () => { ], }; expect(mockUuidv4).not.toHaveBeenCalled(); - expect(checkConflictsResult).toEqual(expectedResult); + expect(checkOriginConflictsResult).toEqual(expectedResult); }); test('returns object with a `importIdMap` entry when multiple inexact matches are detected that target the same multiple destinations', async () => { @@ -397,7 +409,10 @@ describe('#checkConflicts', () => { mockFindResult(objC, objD); // find for obj3: the result is an inexact match with two destinations mockFindResult(objC, objD); // find for obj4: the result is an inexact match with two destinations - const checkConflictsResult = await checkConflicts([obj1, obj2, obj3, obj4], options); + const checkOriginConflictsResult = await checkOriginConflicts( + [obj1, obj2, obj3, obj4], + options + ); const expectedResult = { filteredObjects: [obj1, obj2, obj3, obj4], importIdMap: new Map([ @@ -409,7 +424,7 @@ describe('#checkConflicts', () => { errors: [], }; expect(mockUuidv4).toHaveBeenCalledTimes(4); - expect(checkConflictsResult).toEqual(expectedResult); + expect(checkOriginConflictsResult).toEqual(expectedResult); }); }); @@ -441,7 +456,7 @@ describe('#checkConflicts', () => { mockFindResult(objD, objE); // find for obj8: the result is an inexact match with two destinations const objects = [obj1, obj2, obj3, obj4, obj5, obj6, obj7, obj8]; - const checkConflictsResult = await checkConflicts(objects, options); + const checkOriginConflictsResult = await checkOriginConflicts(objects, options); const expectedResult = { filteredObjects: [obj1, obj2, obj3, obj4, obj5, obj7, obj8], importIdMap: new Map([ @@ -452,7 +467,7 @@ describe('#checkConflicts', () => { errors: [createAmbiguousConflictError(obj6, [objB, objC])], }; expect(mockUuidv4).toHaveBeenCalledTimes(2); - expect(checkConflictsResult).toEqual(expectedResult); + expect(checkOriginConflictsResult).toEqual(expectedResult); }); }); }); @@ -504,7 +519,9 @@ describe('#getImportIdMapForRetries', () => { ]; const options = setupOptions(retries); - const checkConflictsResult = await getImportIdMapForRetries(objects, options); - expect(checkConflictsResult).toEqual(new Map([[`${obj6.type}:${obj6.id}`, { id: 'id-Y' }]])); + const checkOriginConflictsResult = await getImportIdMapForRetries(objects, options); + expect(checkOriginConflictsResult).toEqual( + new Map([[`${obj6.type}:${obj6.id}`, { id: 'id-Y' }]]) + ); }); }); diff --git a/src/core/server/saved_objects/import/check_conflicts.ts b/src/core/server/saved_objects/import/check_origin_conflicts.ts similarity index 92% rename from src/core/server/saved_objects/import/check_conflicts.ts rename to src/core/server/saved_objects/import/check_origin_conflicts.ts index 1320cdd682cce..f9568066f4934 100644 --- a/src/core/server/saved_objects/import/check_conflicts.ts +++ b/src/core/server/saved_objects/import/check_origin_conflicts.ts @@ -26,7 +26,7 @@ import { } from '../types'; import { ISavedObjectTypeRegistry } from '..'; -interface CheckConflictsOptions { +interface CheckOriginConflictsOptions { savedObjectsClient: SavedObjectsClientContract; typeRegistry: ISavedObjectTypeRegistry; namespace?: string; @@ -79,10 +79,10 @@ const getAmbiguousConflictSourceKey = ({ object }: InexactMatch) => * - A `Left` result indicates that one or more conflict destinations exist in this namespace, none of which exactly match this object's ID * ("inexact match"). */ -const checkConflict = async ( +const checkOriginConflict = async ( object: SavedObject<{ title?: string }>, importIds: Set, - options: CheckConflictsOptions + options: CheckOriginConflictsOptions ): Promise> => { const { savedObjectsClient, typeRegistry, namespace } = options; const { type, originId } = object; @@ -135,27 +135,29 @@ const checkConflict = async ( * will allow `createSavedObjects` to modify the ID before creating the object (thus ensuring a conflict during). * B. Otherwise, this is an "ambiguous conflict" result; return an error. */ -export async function checkConflicts( +export async function checkOriginConflicts( objects: Array>, - options: CheckConflictsOptions + options: CheckOriginConflictsOptions ) { // Check each object for possible destination conflicts. const importIds = new Set(objects.map(({ type, id }) => `${type}:${id}`)); - const checkConflictResults = await Promise.all( - objects.map((object) => checkConflict(object, importIds, options)) + const checkOriginConflictResults = await Promise.all( + objects.map((object) => checkOriginConflict(object, importIds, options)) ); // Get a map of all inexact matches that share the same destination(s). - const ambiguousConflictSourcesMap = checkConflictResults.filter(isLeft).reduce((acc, cur) => { - const key = getAmbiguousConflictSourceKey(cur.value); - const value = acc.get(key) ?? []; - return acc.set(key, [...value, cur.value.object]); - }, new Map>>()); + const ambiguousConflictSourcesMap = checkOriginConflictResults + .filter(isLeft) + .reduce((acc, cur) => { + const key = getAmbiguousConflictSourceKey(cur.value); + const value = acc.get(key) ?? []; + return acc.set(key, [...value, cur.value.object]); + }, new Map>>()); const errors: SavedObjectsImportError[] = []; const filteredObjects: Array> = []; const importIdMap = new Map(); - checkConflictResults.forEach((result) => { + checkOriginConflictResults.forEach((result) => { if (!isLeft(result)) { filteredObjects.push(result.value); return; diff --git a/src/core/server/saved_objects/import/create_saved_objects.ts b/src/core/server/saved_objects/import/create_saved_objects.ts index 40df11e949d8e..840b6daf59d54 100644 --- a/src/core/server/saved_objects/import/create_saved_objects.ts +++ b/src/core/server/saved_objects/import/create_saved_objects.ts @@ -53,7 +53,7 @@ const isUnresolvableConflict = (object: SavedObject) => /** * This function abstracts the bulk creation of import objects for two purposes: - * 1. The import ID map that was generated by the `checkConflicts` function should dictate the IDs of the objects we create. + * 1. The import ID map that was generated by the `checkOriginConflicts` function should dictate the IDs of the objects we create. * 2. Any object create attempt that results in an unresolvable conflict should have its ID regenerated and retry create. This way, when an * object with a "multi-namespace" type is exported from one namespace and imported to another, it does not result in an error, but * instead a new object is created. @@ -75,8 +75,8 @@ export const createSavedObjects = async ( new Map>() ); - // use the import ID map from the `checkConflicts` or `getImportIdMapForRetries` function to ensure that each object is being created with - // the correct ID also, ensure that the `originId` is set on the created object if it did not have one + // use the import ID map from the `checkOriginConflicts` or `getImportIdMapForRetries` function to ensure that each object is being + // created with the correct ID also, ensure that the `originId` is set on the created object if it did not have one const objectsToCreate = objects.map((object) => { const importIdEntry = importIdMap.get(`${object.type}:${object.id}`); if (importIdEntry) { 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 b4cad00c95e7d..253d27ebf8f7d 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 @@ -32,12 +32,12 @@ import { importSavedObjectsFromStream } from './import_saved_objects'; import { collectSavedObjects } from './collect_saved_objects'; import { validateReferences } from './validate_references'; -import { checkConflicts } from './check_conflicts'; +import { checkOriginConflicts } from './check_origin_conflicts'; import { createSavedObjects } from './create_saved_objects'; jest.mock('./collect_saved_objects'); jest.mock('./validate_references'); -jest.mock('./check_conflicts'); +jest.mock('./check_origin_conflicts'); jest.mock('./create_saved_objects'); const getMockFn = any, U>(fn: (...args: Parameters) => U) => @@ -49,7 +49,7 @@ describe('#importSavedObjectsFromStream', () => { // mock empty output of each of these mocked modules so the import doesn't throw an error getMockFn(collectSavedObjects).mockResolvedValue({ errors: [], collectedObjects: [] }); getMockFn(validateReferences).mockResolvedValue({ errors: [], filteredObjects: [] }); - getMockFn(checkConflicts).mockResolvedValue({ + getMockFn(checkOriginConflicts).mockResolvedValue({ errors: [], filteredObjects: [], importIdMap: new Map(), @@ -80,7 +80,7 @@ describe('#importSavedObjectsFromStream', () => { /** * These tests use minimal mocks which don't look realistic, but are sufficient to exercise the code paths correctly. For example, for an * object to be imported successfully it would need to be obtained from `collectSavedObjects`, passed to `validateReferences`, passed to - * `checkConflicts`, passed to `createSavedObjects`, and returned from that. However, for each of the tests below, we skip the + * `checkOriginConflicts`, passed to `createSavedObjects`, and returned from that. However, for each of the tests below, we skip the * intermediate steps in the interest of brevity. */ describe('module calls', () => { @@ -110,21 +110,28 @@ describe('#importSavedObjectsFromStream', () => { ); }); - test('checks conflicts', async () => { + test('checks origin conflicts', async () => { const options = setupOptions(); const filteredObjects = [createObject()]; getMockFn(validateReferences).mockResolvedValue({ errors: [], filteredObjects }); await importSavedObjectsFromStream(options); - const checkConflictsOptions = { savedObjectsClient, typeRegistry, namespace }; - expect(checkConflicts).toHaveBeenCalledWith(filteredObjects, checkConflictsOptions); + const checkOriginConflictsOptions = { savedObjectsClient, typeRegistry, namespace }; + expect(checkOriginConflicts).toHaveBeenCalledWith( + filteredObjects, + checkOriginConflictsOptions + ); }); test('creates saved objects', async () => { const options = setupOptions(); const filteredObjects = [createObject()]; const importIdMap = new Map(); - getMockFn(checkConflicts).mockResolvedValue({ errors: [], filteredObjects, importIdMap }); + getMockFn(checkOriginConflicts).mockResolvedValue({ + errors: [], + filteredObjects, + importIdMap, + }); await importSavedObjectsFromStream(options); const createSavedObjectsOptions = { savedObjectsClient, importIdMap, overwrite, namespace }; @@ -173,7 +180,7 @@ describe('#importSavedObjectsFromStream', () => { collectedObjects: [], }); getMockFn(validateReferences).mockResolvedValue({ errors: [errors[1]], filteredObjects: [] }); - getMockFn(checkConflicts).mockResolvedValue({ + getMockFn(checkOriginConflicts).mockResolvedValue({ errors: [errors[2]], filteredObjects: [], importIdMap: new Map(), 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 c3339b6ca4847..9a1e6b6ae03ef 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.ts @@ -24,7 +24,7 @@ import { SavedObjectsImportOptions, } from './types'; import { validateReferences } from './validate_references'; -import { checkConflicts } from './check_conflicts'; +import { checkOriginConflicts } from './check_origin_conflicts'; import { createSavedObjects } from './create_saved_objects'; /** @@ -60,10 +60,10 @@ export async function importSavedObjectsFromStream({ errorAccumulator = [...errorAccumulator, ...validateReferencesResult.errors]; // Check multi-namespace object types for regular conflicts and ambiguous conflicts - const checkConflictsOptions = { savedObjectsClient, typeRegistry, namespace }; - const { filteredObjects, errors: conflictErrors, importIdMap } = await checkConflicts( + const checkOriginConflictsOptions = { savedObjectsClient, typeRegistry, namespace }; + const { filteredObjects, errors: conflictErrors, importIdMap } = await checkOriginConflicts( validateReferencesResult.filteredObjects, - checkConflictsOptions + checkOriginConflictsOptions ); errorAccumulator = [...errorAccumulator, ...conflictErrors]; 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 12394cb1d6c75..aeccbb89485c9 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 @@ -35,7 +35,7 @@ import { resolveSavedObjectsImportErrors } from './resolve_import_errors'; import { validateRetries } from './validate_retries'; import { collectSavedObjects } from './collect_saved_objects'; import { validateReferences } from './validate_references'; -import { getImportIdMapForRetries } from './check_conflicts'; +import { getImportIdMapForRetries } from './check_origin_conflicts'; import { splitOverwrites } from './split_overwrites'; import { createSavedObjects } from './create_saved_objects'; import { createObjectsFilter } from './create_objects_filter'; @@ -44,7 +44,7 @@ jest.mock('./validate_retries'); jest.mock('./create_objects_filter'); jest.mock('./collect_saved_objects'); jest.mock('./validate_references'); -jest.mock('./check_conflicts'); +jest.mock('./check_origin_conflicts'); jest.mock('./split_overwrites'); jest.mock('./create_saved_objects'); @@ -101,7 +101,7 @@ describe('#importSavedObjectsFromStream', () => { /** * These tests use minimal mocks which don't look realistic, but are sufficient to exercise the code paths correctly. For example, for an * object to be imported successfully it would need to be obtained from `collectSavedObjects`, passed to `validateReferences`, passed to - * `checkConflicts`, passed to `createSavedObjects`, and returned from that. However, for each of the tests below, we skip the + * `getImportIdMapForRetries`, passed to `createSavedObjects`, and returned from that. However, for each of the tests below, we skip the * intermediate steps in the interest of brevity. */ describe('module calls', () => { @@ -170,7 +170,7 @@ describe('#importSavedObjectsFromStream', () => { ); }); - test('checks conflicts', async () => { + test('gets import ID map for retries', async () => { const retries = [createRetry()]; const options = setupOptions(retries); const filteredObjects = [createObject()]; 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 fa571b7e3b6ef..140e472349f66 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.ts @@ -27,7 +27,7 @@ import { import { validateReferences } from './validate_references'; import { validateRetries } from './validate_retries'; import { createSavedObjects } from './create_saved_objects'; -import { getImportIdMapForRetries } from './check_conflicts'; +import { getImportIdMapForRetries } from './check_origin_conflicts'; import { SavedObject } from '../types'; /** From d65912b90f61930bb79c81f184e825ced8a6e52b Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Wed, 24 Jun 2020 14:19:31 -0400 Subject: [PATCH 22/55] Change import APIs to check for conflicts before creating objects Note, this simplifies integration tests; when the security plugin is enabled, any 403 error will always be for the `bulk_create` action. Also updated the `checkConflicts` method for `SavedObjectsClient` to error on invalid types, and updated the security wrapper's AuthZ check to use `bulk_create` and log the action as `checkConflicts`. --- .../import/check_conflicts.test.ts | 169 ++++++++++++ .../saved_objects/import/check_conflicts.ts | 82 ++++++ .../import/check_origin_conflicts.test.ts | 119 ++++----- .../import/check_origin_conflicts.ts | 15 +- .../import/create_saved_objects.test.ts | 248 ++++++------------ .../import/create_saved_objects.ts | 67 +---- .../import/import_saved_objects.test.ts | 59 ++++- .../import/import_saved_objects.ts | 45 +++- .../import/resolve_import_errors.test.ts | 43 ++- .../import/resolve_import_errors.ts | 26 +- .../routes/integration_tests/import.test.ts | 11 +- .../resolve_import_errors.test.ts | 1 + .../service/lib/repository.test.js | 6 +- .../saved_objects/service/lib/repository.ts | 54 +++- ...ecure_saved_objects_client_wrapper.test.ts | 20 +- .../secure_saved_objects_client_wrapper.ts | 7 +- .../common/suites/import.ts | 9 +- .../security_and_spaces/apis/import.ts | 70 ++--- .../security_only/apis/import.ts | 53 ++-- 19 files changed, 640 insertions(+), 464 deletions(-) create mode 100644 src/core/server/saved_objects/import/check_conflicts.test.ts create mode 100644 src/core/server/saved_objects/import/check_conflicts.ts diff --git a/src/core/server/saved_objects/import/check_conflicts.test.ts b/src/core/server/saved_objects/import/check_conflicts.test.ts new file mode 100644 index 0000000000000..9fdd50900fcd4 --- /dev/null +++ b/src/core/server/saved_objects/import/check_conflicts.test.ts @@ -0,0 +1,169 @@ +/* + * 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 { mockUuidv4 } from './__mocks__'; +import { savedObjectsClientMock } from '../../mocks'; +import { SavedObjectReference } from 'kibana/public'; +import { SavedObjectsClientContract, SavedObject } from '../types'; +import { SavedObjectsErrorHelpers } from '..'; +import { checkConflicts } from './check_conflicts'; + +type SavedObjectType = SavedObject<{ title?: string }>; +type CheckConflictsOptions = Parameters[1]; + +/** + * Function to create a realistic-looking import object given a type and ID + */ +const createObject = (type: string, id: string): SavedObjectType => ({ + type, + id, + attributes: { title: 'some-title' }, + references: (Symbol() as unknown) as SavedObjectReference[], +}); + +/** + * Create a variety of different objects to exercise different import / result scenarios + */ +const obj1 = createObject('type-1', 'id-1'); // -> success +const obj2 = createObject('type-2', 'id-2'); // -> conflict +const obj3 = createObject('type-3', 'id-3'); // -> unresolvable conflict +const obj4 = createObject('type-4', 'id-4'); // -> invalid type + +describe('#checkConflicts', () => { + let savedObjectsClient: jest.Mocked; + let socCheckConflicts: typeof savedObjectsClient['checkConflicts']; + + /** + * Creates an options object to be used as an argument for createSavedObjects + * Includes mock savedObjectsClient + */ + const setupOptions = ( + options: { + namespace?: string; + ignoreRegularConflicts?: boolean; + } = {} + ): CheckConflictsOptions => { + const { namespace, ignoreRegularConflicts } = options; + savedObjectsClient = savedObjectsClientMock.create(); + socCheckConflicts = savedObjectsClient.checkConflicts; + socCheckConflicts.mockResolvedValue({ errors: [] }); // by default, mock to empty results + return { savedObjectsClient, namespace, ignoreRegularConflicts }; + }; + + const getResultMock = { + conflict: (type: string, id: string) => { + const error = SavedObjectsErrorHelpers.createConflictError(type, id).output.payload; + return { type, id, error }; + }, + unresolvableConflict: (type: string, id: string) => { + const conflictMock = getResultMock.conflict(type, id); + const metadata = { isNotOverwritable: true }; + return { ...conflictMock, error: { ...conflictMock.error, metadata } }; + }, + invalidType: (type: string, id: string) => { + const error = SavedObjectsErrorHelpers.createUnsupportedTypeError(type).output.payload; + return { type, id, error }; + }, + }; + + it('exits early if there are no objects to check', async () => { + const namespace = 'foo-namespace'; + const options = setupOptions({ namespace }); + + const checkConflictsResult = await checkConflicts([], options); + expect(socCheckConflicts).not.toHaveBeenCalled(); + expect(checkConflictsResult).toEqual({ + filteredObjects: [], + errors: [], + importIdMap: new Map(), + importIds: new Set(), + }); + }); + + it('calls checkConflicts with expected inputs', async () => { + const namespace = 'foo-namespace'; + const objects = [obj1, obj2, obj3, obj4]; + const options = setupOptions({ namespace }); + + await checkConflicts(objects, options); + expect(socCheckConflicts).toHaveBeenCalledTimes(1); + expect(socCheckConflicts).toHaveBeenCalledWith(objects, { namespace }); + }); + + it('returns expected result', async () => { + const namespace = 'foo-namespace'; + const objects = [obj1, obj2, obj3, obj4]; + const options = setupOptions({ namespace }); + const obj2Error = getResultMock.conflict(obj2.type, obj2.id); + const obj3Error = getResultMock.unresolvableConflict(obj3.type, obj3.id); + const obj4Error = getResultMock.invalidType(obj4.type, obj4.id); + socCheckConflicts.mockResolvedValue({ errors: [obj2Error, obj3Error, obj4Error] }); + mockUuidv4.mockReturnValueOnce(`new-object-id`); + + const checkConflictsResult = await checkConflicts(objects, options); + expect(checkConflictsResult).toEqual({ + filteredObjects: [obj1, obj3], + errors: [ + { ...obj2Error, title: obj2.attributes.title, error: { type: 'conflict' } }, + { + ...obj4Error, + title: obj4.attributes.title, + error: { ...obj4Error.error, type: 'unknown' }, + }, + ], + importIdMap: new Map([[`${obj3.type}:${obj3.id}`, { id: `new-object-id` }]]), + importIds: new Set([ + `${obj1.type}:${obj1.id}`, + `${obj2.type}:${obj2.id}`, + `${obj3.type}:${obj3.id}`, + `${obj4.type}:${obj4.id}`, + ]), + }); + }); + + it('does not return errors for regular conflicts when ignoreRegularConflicts=true', async () => { + const namespace = 'foo-namespace'; + const objects = [obj1, obj2, obj3, obj4]; + const options = setupOptions({ namespace, ignoreRegularConflicts: true }); + const obj2Error = getResultMock.conflict(obj2.type, obj2.id); + const obj3Error = getResultMock.unresolvableConflict(obj3.type, obj3.id); + const obj4Error = getResultMock.invalidType(obj4.type, obj4.id); + socCheckConflicts.mockResolvedValue({ errors: [obj2Error, obj3Error, obj4Error] }); + mockUuidv4.mockReturnValueOnce(`new-object-id`); + + const checkConflictsResult = await checkConflicts(objects, options); + expect(checkConflictsResult).toEqual({ + filteredObjects: [obj1, obj2, obj3], + errors: [ + { + ...obj4Error, + title: obj4.attributes.title, + error: { ...obj4Error.error, type: 'unknown' }, + }, + ], + importIdMap: new Map([[`${obj3.type}:${obj3.id}`, { id: `new-object-id` }]]), + importIds: new Set([ + `${obj1.type}:${obj1.id}`, + `${obj2.type}:${obj2.id}`, + `${obj3.type}:${obj3.id}`, + `${obj4.type}:${obj4.id}`, + ]), + }); + }); +}); diff --git a/src/core/server/saved_objects/import/check_conflicts.ts b/src/core/server/saved_objects/import/check_conflicts.ts new file mode 100644 index 0000000000000..96bbf9be63ceb --- /dev/null +++ b/src/core/server/saved_objects/import/check_conflicts.ts @@ -0,0 +1,82 @@ +/* + * 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 { v4 as uuidv4 } from 'uuid'; +import { + SavedObject, + SavedObjectsClientContract, + SavedObjectsImportError, + SavedObjectError, +} from '../types'; + +interface CheckConflictsOptions { + savedObjectsClient: SavedObjectsClientContract; + namespace?: string; + ignoreRegularConflicts?: boolean; +} + +const isUnresolvableConflict = (error: SavedObjectError) => + error.statusCode === 409 && error.metadata?.isNotOverwritable; + +export async function checkConflicts( + objects: Array>, + options: CheckConflictsOptions +) { + const filteredObjects: Array> = []; + const errors: SavedObjectsImportError[] = []; + const importIdMap = new Map(); + const importIds = new Set(); + + // exit early if there are no objects to check + if (objects.length === 0) { + return { filteredObjects, errors, importIdMap, importIds }; + } + + const { savedObjectsClient, namespace, ignoreRegularConflicts } = options; + const checkConflictsResult = await savedObjectsClient.checkConflicts(objects, { namespace }); + const errorMap = checkConflictsResult.errors.reduce( + (acc, { type, id, error }) => acc.set(`${type}:${id}`, error), + new Map() + ); + + objects.forEach((object) => { + const { + type, + id, + attributes: { title }, + } = object; + importIds.add(`${type}:${id}`); + const errorObj = errorMap.get(`${type}:${id}`); + if (errorObj && isUnresolvableConflict(errorObj)) { + // Any object create attempt that would result in an unresolvable conflict should have its ID regenerated. This way, when an object + // with a "multi-namespace" type is exported from one namespace and imported to another, it does not result in an error, but instead a + // new object is created. + const destinationId = uuidv4(); + importIdMap.set(`${type}:${id}`, { id: destinationId }); + filteredObjects.push(object); + } else if (errorObj && errorObj.statusCode !== 409) { + errors.push({ type, id, title, error: { ...errorObj, type: 'unknown' } }); + } else if (errorObj?.statusCode === 409 && !ignoreRegularConflicts) { + errors.push({ type, id, title, error: { type: 'conflict' } }); + } else { + filteredObjects.push(object); + } + }); + return { filteredObjects, errors, importIdMap, importIds }; +} diff --git a/src/core/server/saved_objects/import/check_origin_conflicts.test.ts b/src/core/server/saved_objects/import/check_origin_conflicts.test.ts index 396f3becfed6d..1b91e87d183df 100644 --- a/src/core/server/saved_objects/import/check_origin_conflicts.test.ts +++ b/src/core/server/saved_objects/import/check_origin_conflicts.test.ts @@ -64,13 +64,19 @@ describe('#checkOriginConflicts', () => { saved_objects: objects, }); - const setupOptions = (namespace?: string): CheckOriginConflictsOptions => { + const setupOptions = ( + options: { + namespace?: string; + importIds?: Set; + } = {} + ): CheckOriginConflictsOptions => { + const { namespace, importIds = new Set() } = options; savedObjectsClient = savedObjectsClientMock.create(); find = savedObjectsClient.find; find.mockResolvedValue(getResultMock()); // mock zero hits response by default typeRegistry = typeRegistryMock.create(); typeRegistry.isMultiNamespace.mockImplementation((type) => type === MULTI_NS_TYPE); - return { savedObjectsClient, typeRegistry, namespace }; + return { savedObjectsClient, typeRegistry, namespace, importIds }; }; const mockFindResult = (...objects: SavedObjectType[]) => { @@ -111,7 +117,7 @@ describe('#checkOriginConflicts', () => { expectFindArgs(2, multiNsObjWithOriginId, ''); find.mockClear(); - const options2 = setupOptions('some-namespace'); + const options2 = setupOptions({ namespace: 'some-namespace' }); await checkOriginConflicts(objects, options2); expect(find).toHaveBeenCalledTimes(2); expectFindArgs(1, multiNsObj, 'some-namespace:'); @@ -121,7 +127,7 @@ describe('#checkOriginConflicts', () => { test('searches within the current `namespace`', async () => { const objects = [multiNsObj]; const namespace = 'some-namespace'; - const options = setupOptions(namespace); + const options = setupOptions({ namespace }); await checkOriginConflicts(objects, options); expect(find).toHaveBeenCalledTimes(1); @@ -191,64 +197,27 @@ describe('#checkOriginConflicts', () => { expect(checkOriginConflictsResult).toEqual(expectedResult); }); - test('returns object when an exact match is detected (1 hit)', async () => { - // obj1 and obj2 exist in this space - // try to import obj2 and obj2 - const obj1 = createObject(MULTI_NS_TYPE, 'id-1'); - const obj2 = createObject(MULTI_NS_TYPE, 'id-2', 'originId-foo'); - const options = setupOptions(); - mockFindResult(obj1); // find for obj1: the result is an exact match - mockFindResult(obj2); // find for obj2: the result is an exact match - - const checkOriginConflictsResult = await checkOriginConflicts([obj1, obj2], options); - const expectedResult = { - filteredObjects: [obj1, obj2], - importIdMap: new Map(), - errors: [], - }; - expect(checkOriginConflictsResult).toEqual(expectedResult); - }); - - test('returns object when an exact match is detected (2+ hits)', async () => { - // obj1, obj2, objA, and objB exist in this space - // try to import obj1 and obj2 - const obj1 = createObject(MULTI_NS_TYPE, 'id-1'); - const objA = createObject(MULTI_NS_TYPE, 'id-3', obj1.id); - const obj2 = createObject(MULTI_NS_TYPE, 'id-2', 'originId-foo'); - const objB = createObject(MULTI_NS_TYPE, 'id-4', obj2.originId); - const options = setupOptions(); - mockFindResult(obj1, objA); // find for obj1: the first result is an exact match, so the second result is ignored - mockFindResult(objB, obj2); // find for obj2: the second result is an exact match, so the first result is ignored - - const checkOriginConflictsResult = await checkOriginConflicts([obj1, obj2], options); - const expectedResult = { - filteredObjects: [obj1, obj2], - importIdMap: new Map(), - errors: [], - }; - expect(checkOriginConflictsResult).toEqual(expectedResult); - }); - test('returns object when an inexact match is detected (1 hit) with a destination that is exactly matched by another object', async () => { // obj1 and obj3 exist in this space - // try to import obj1, obj2, obj3, and obj4 - // note: this test is only concerned with obj2 and obj4, but obj1 and obj3 must be included to exercise this code path + // try to import obj1, obj2, obj3, and obj4; simulating a scenario where obj1 and obj3 were filtered out during `checkConflicts`, so we only call `checkOriginConflicts` with the remainder const obj1 = createObject(MULTI_NS_TYPE, 'id-1'); const obj2 = createObject(MULTI_NS_TYPE, 'id-2', obj1.id); const obj3 = createObject(MULTI_NS_TYPE, 'id-3', 'originId-foo'); const obj4 = createObject(MULTI_NS_TYPE, 'id-4', obj3.originId); - const options = setupOptions(); - mockFindResult(obj1); // find for obj1: the result is an exact match + const options = setupOptions({ + importIds: new Set([ + `${obj1.type}:${obj1.id}`, + `${obj2.type}:${obj2.id}`, + `${obj3.type}:${obj3.id}`, + `${obj4.type}:${obj4.id}`, + ]), + }); mockFindResult(obj1); // find for obj2: the result is an inexact match with one destination that is exactly matched by obj1 so it is ignored -- accordingly, obj2 has no match - mockFindResult(obj3); // find for obj3: the result is an exact match mockFindResult(obj3); // find for obj4: the result is an inexact match with one destination that is exactly matched by obj3 so it is ignored -- accordingly, obj4 has no match - const checkOriginConflictsResult = await checkOriginConflicts( - [obj1, obj2, obj3, obj4], - options - ); + const checkOriginConflictsResult = await checkOriginConflicts([obj2, obj4], options); const expectedResult = { - filteredObjects: [obj1, obj2, obj3, obj4], + filteredObjects: [obj2, obj4], importIdMap: new Map(), errors: [], }; @@ -257,18 +226,18 @@ describe('#checkOriginConflicts', () => { test('returns object when an inexact match is detected (2+ hits) with destinations that are all exactly matched by another object', async () => { // obj1 and obj2 exist in this space - // try to import obj1, obj2, and obj3 + // try to import obj1, obj2, and obj3; simulating a scenario where obj1 and obj2 were filtered out during `checkConflicts`, so we only call `checkOriginConflicts` with the remainder const obj1 = createObject(MULTI_NS_TYPE, 'id-1'); const obj2 = createObject(MULTI_NS_TYPE, 'id-2', obj1.id); const obj3 = createObject(MULTI_NS_TYPE, 'id-3', obj1.id); - const options = setupOptions(); - mockFindResult(obj1, obj2); // find for obj1: the first result is an exact match, so the second result is ignored - mockFindResult(obj1, obj2); // find for obj2: the second result is an exact match, so the first result is ignored + const options = setupOptions({ + importIds: new Set([`${obj1.type}:${obj1.id}`, `${obj2.type}:${obj2.id}`]), + }); mockFindResult(obj1, obj2); // find for obj3: the result is an inexact match with two destinations that are exactly matched by obj1 and obj2 so they are ignored -- accordingly, obj3 has no match - const checkOriginConflictsResult = await checkOriginConflicts([obj1, obj2, obj3], options); + const checkOriginConflictsResult = await checkOriginConflicts([obj3], options); const expectedResult = { - filteredObjects: [obj1, obj2, obj3], + filteredObjects: [obj3], importIdMap: new Map(), errors: [], }; @@ -302,26 +271,27 @@ describe('#checkOriginConflicts', () => { test('returns object with a `importIdMap` entry when an inexact match is detected (2+ hits), with n-1 destinations that are exactly matched by another object', async () => { // obj1, obj3, objA, and objB exist in this space - // try to import obj1, obj2, obj3, and obj4 - // note: this test is only concerned with obj2 and obj4, but obj1 and obj3 must be included to exercise this code path + // try to import obj1, obj2, obj3, and obj4; simulating a scenario where obj1 and obj3 were filtered out during `checkConflicts`, so we only call `checkOriginConflicts` with the remainder const obj1 = createObject(MULTI_NS_TYPE, 'id-1'); const obj2 = createObject(MULTI_NS_TYPE, 'id-2', obj1.id); const objA = createObject(MULTI_NS_TYPE, 'id-A', obj1.id); const obj3 = createObject(MULTI_NS_TYPE, 'id-3', 'originId-foo'); const obj4 = createObject(MULTI_NS_TYPE, 'id-4', obj3.originId); const objB = createObject(MULTI_NS_TYPE, 'id-B', obj3.originId); - const options = setupOptions(); - mockFindResult(obj1, objA); // find for obj1: the first result is an exact match, so the second result is ignored + const options = setupOptions({ + importIds: new Set([ + `${obj1.type}:${obj1.id}`, + `${obj2.type}:${obj2.id}`, + `${obj3.type}:${obj3.id}`, + `${obj4.type}:${obj4.id}`, + ]), + }); mockFindResult(obj1, objA); // find for obj2: the result is an inexact match with two destinations, but the first destination is exactly matched by obj1 so it is ignored -- accordingly, obj2 has an inexact match with one destination (objA) - mockFindResult(objB, obj3); // find for obj3: the second result is an exact match, so the first result is ignored mockFindResult(objB, obj3); // find for obj4: the result is an inexact match with two destinations, but the second destination is exactly matched by obj3 so it is ignored -- accordingly, obj4 has an inexact match with one destination (objB) - const checkOriginConflictsResult = await checkOriginConflicts( - [obj1, obj2, obj3, obj4], - options - ); + const checkOriginConflictsResult = await checkOriginConflicts([obj2, obj4], options); const expectedResult = { - filteredObjects: [obj1, obj2, obj3, obj4], + filteredObjects: [obj2, obj4], importIdMap: new Map([ [`${obj2.type}:${obj2.id}`, { id: objA.id }], [`${obj4.type}:${obj4.id}`, { id: objB.id }], @@ -429,8 +399,8 @@ describe('#checkOriginConflicts', () => { }); test('returns mixed results', async () => { - // obj3, objA, obB, and objC exist in this space - // try to import obj1, obj2, obj3, obj4, obj5, obj6, and obj7 + // obj3, objA, objB, objC, objD, and objE exist in this space + // try to import obj1, obj2, obj3, obj4, obj5, obj6, and obj7; simulating a scenario where obj3 was filtered out during `checkConflicts`, so we only call `checkOriginConflicts` with the remainder // note: this test is non-exhaustive for different permutations of import objects and results, but prior tests exercise these more thoroughly const obj1 = createObject(OTHER_TYPE, 'id-1'); const obj2 = createObject(MULTI_NS_TYPE, 'id-2'); @@ -445,20 +415,21 @@ describe('#checkOriginConflicts', () => { const objC = createObject(MULTI_NS_TYPE, 'id-C', obj6.id); const objD = createObject(MULTI_NS_TYPE, 'id-D', obj7.id); const objE = createObject(MULTI_NS_TYPE, 'id-E', obj7.id); - const options = setupOptions(); + const objects = [obj1, obj2, obj4, obj5, obj6, obj7, obj8]; + const importIds = new Set([...objects, obj3].map(({ type, id }) => `${type}:${id}`)); + const options = setupOptions({ importIds }); + // obj1 is a non-multi-namespace type, so it is skipped while searching mockFindResult(); // find for obj2: the result is no match - mockFindResult(obj3); // find for obj3: the result is an exact match mockFindResult(obj3); // find for obj4: the result is an inexact match with one destination that is exactly matched by obj3 so it is ignored -- accordingly, obj4 has no match mockFindResult(objA); // find for obj5: the result is an inexact match with one destination mockFindResult(objB, objC); // find for obj6: the result is an inexact match with two destinations mockFindResult(objD, objE); // find for obj7: the result is an inexact match with two destinations mockFindResult(objD, objE); // find for obj8: the result is an inexact match with two destinations - const objects = [obj1, obj2, obj3, obj4, obj5, obj6, obj7, obj8]; const checkOriginConflictsResult = await checkOriginConflicts(objects, options); const expectedResult = { - filteredObjects: [obj1, obj2, obj3, obj4, obj5, obj7, obj8], + filteredObjects: [obj1, obj2, obj4, obj5, obj7, obj8], importIdMap: new Map([ [`${obj5.type}:${obj5.id}`, { id: objA.id }], [`${obj7.type}:${obj7.id}`, { id: 'uuidv4', omitOriginId: true }], diff --git a/src/core/server/saved_objects/import/check_origin_conflicts.ts b/src/core/server/saved_objects/import/check_origin_conflicts.ts index f9568066f4934..c5249ff7e797f 100644 --- a/src/core/server/saved_objects/import/check_origin_conflicts.ts +++ b/src/core/server/saved_objects/import/check_origin_conflicts.ts @@ -30,6 +30,7 @@ interface CheckOriginConflictsOptions { savedObjectsClient: SavedObjectsClientContract; typeRegistry: ISavedObjectTypeRegistry; namespace?: string; + importIds: Set; } interface GetImportIdMapForRetriesOptions { @@ -74,17 +75,16 @@ const getAmbiguousConflictSourceKey = ({ object }: InexactMatch) => /** * Make a search request for an import object to check if any objects of this type that match this object's `originId` or `id` exist in the * specified namespace: - * - A `Right` result indicates that no conflict destinations were found in this namespace ("no match"), *OR* an exact match for this - * object's ID exists in this namespace ("exact match"). + * - A `Right` result indicates that no conflict destinations were found in this namespace ("no match"). * - A `Left` result indicates that one or more conflict destinations exist in this namespace, none of which exactly match this object's ID - * ("inexact match"). + * ("inexact match"). We can make this assumption because any "exact match" results would have been obtained and filtered out by the + * `checkConflicts` submodule, which is called before this. */ const checkOriginConflict = async ( object: SavedObject<{ title?: string }>, - importIds: Set, options: CheckOriginConflictsOptions ): Promise> => { - const { savedObjectsClient, typeRegistry, namespace } = options; + const { savedObjectsClient, typeRegistry, namespace, importIds } = options; const { type, originId } = object; if (!typeRegistry.isMultiNamespace(type)) { @@ -104,7 +104,7 @@ const checkOriginConflict = async ( }; const findResult = await savedObjectsClient.find<{ title?: string }>(findOptions); const { total, saved_objects: savedObjects } = findResult; - if (total === 0 || savedObjects.some(({ id }) => id === object.id)) { + if (total === 0) { return { tag: 'right', value: object }; } // This is an "inexact match" so far; filter the conflict destination(s) to exclude any that exactly match other objects we are importing. @@ -140,9 +140,8 @@ export async function checkOriginConflicts( options: CheckOriginConflictsOptions ) { // Check each object for possible destination conflicts. - const importIds = new Set(objects.map(({ type, id }) => `${type}:${id}`)); const checkOriginConflictResults = await Promise.all( - objects.map((object) => checkOriginConflict(object, importIds, options)) + objects.map((object) => checkOriginConflict(object, options)) ); // Get a map of all inexact matches that share the same destination(s). diff --git a/src/core/server/saved_objects/import/create_saved_objects.test.ts b/src/core/server/saved_objects/import/create_saved_objects.test.ts index dda183d854bd7..8a60425636fa1 100644 --- a/src/core/server/saved_objects/import/create_saved_objects.test.ts +++ b/src/core/server/saved_objects/import/create_saved_objects.test.ts @@ -17,7 +17,6 @@ * under the License. */ -import { mockUuidv4 } from './__mocks__'; import { savedObjectsClientMock } from '../../mocks'; import { createSavedObjects } from './create_saved_objects'; import { SavedObjectReference } from 'kibana/public'; @@ -160,187 +159,86 @@ describe('#createSavedObjects', () => { expect(createSavedObjectsResult).toEqual({ createdObjects: [], errors: [] }); }); - describe('import without retries', () => { - const objs = [obj1, obj2, obj3, obj4, obj6, obj7, obj8, obj10, obj11, obj12, obj13]; - - const setupMockResults = (options: CreateSavedObjectsOptions) => { - bulkCreate.mockResolvedValue({ - saved_objects: [ - getResultMock.success(obj1, options), - getResultMock.conflict(obj2.type, obj2.id), - getResultMock.conflict(obj3.type, importId3), - getResultMock.conflict(obj4.type, importId4), - // skip obj5, we aren't testing an unresolvable conflict here - getResultMock.success(obj6, options), - getResultMock.conflict(obj7.type, obj7.id), - getResultMock.conflict(obj8.type, importId8), - // skip obj9, we aren't testing an unresolvable conflict here - getResultMock.success(obj10, options), - getResultMock.conflict(obj11.type, obj11.id), - getResultMock.success(obj12, options), - getResultMock.conflict(obj13.type, obj13.id), - ], - }); - }; - - const testBulkCreateObjects = async (namespace?: string) => { - const options = setupOptions({ namespace }); - setupMockResults(options); - - await createSavedObjects(objs, options); - expect(bulkCreate).toHaveBeenCalledTimes(1); - // these three objects are transformed before being created, because they are included in the `importIdMap` - const x3 = { ...obj3, id: importId3, originId: undefined }; // this import object already has an originId, but the entry has omitOriginId=true - const x4 = { ...obj4, id: importId4 }; // this import object already has an originId - const x8 = { ...obj8, id: importId8, originId: obj8.id }; // this import object doesn't have an originId, so it is set before create - const argObjs = [obj1, obj2, x3, x4, obj6, obj7, x8, obj10, obj11, obj12, obj13]; - expectBulkCreateArgs.objects(1, argObjs); - }; - const testBulkCreateOptions = async (namespace?: string) => { - const overwrite = (Symbol() as unknown) as boolean; - const options = setupOptions({ namespace, overwrite }); - setupMockResults(options); - - await createSavedObjects(objs, options); - expect(bulkCreate).toHaveBeenCalledTimes(1); - expectBulkCreateArgs.options(1, options); - }; - const testReturnValue = async (namespace?: string) => { - const options = setupOptions({ namespace }); - setupMockResults(options); + const objs = [obj1, obj2, obj3, obj4, obj5, obj6, obj7, obj8, obj9, obj10, obj11, obj12, obj13]; + + const setupMockResults = (options: CreateSavedObjectsOptions) => { + bulkCreate.mockResolvedValue({ + saved_objects: [ + getResultMock.success(obj1, options), + getResultMock.conflict(obj2.type, obj2.id), + getResultMock.conflict(obj3.type, importId3), + getResultMock.conflict(obj4.type, importId4), + getResultMock.unresolvableConflict(obj5.type, obj5.id), + getResultMock.success(obj6, options), + getResultMock.conflict(obj7.type, obj7.id), + getResultMock.conflict(obj8.type, importId8), + getResultMock.unresolvableConflict(obj9.type, obj9.id), + getResultMock.success(obj10, options), + getResultMock.conflict(obj11.type, obj11.id), + getResultMock.success(obj12, options), + getResultMock.conflict(obj13.type, obj13.id), + ], + }); + }; - const results = await createSavedObjects(objs, options); - const resultSavedObjects = (await bulkCreate.mock.results[0].value).saved_objects; - const [r1, r2, r3, r4, r6, r7, r8, r10, r11, r12, r13] = resultSavedObjects; - // these three results are transformed before being returned, because the bulkCreate attempt used different IDs for them - const [x3, x4, x8] = [r3, r4, r8].map((x: SavedObject) => ({ ...x, destinationId: x.id })); - const transformedResults = [r1, r2, x3, x4, r6, r7, x8, r10, r11, r12, r13]; - const expectedResults = getExpectedResults(transformedResults, objs); - expect(results).toEqual(expectedResults); - }; + const testBulkCreateObjects = async (namespace?: string) => { + const options = setupOptions({ namespace }); + setupMockResults(options); + + await createSavedObjects(objs, options); + expect(bulkCreate).toHaveBeenCalledTimes(1); + // these three objects are transformed before being created, because they are included in the `importIdMap` + const x3 = { ...obj3, id: importId3, originId: undefined }; // this import object already has an originId, but the entry has omitOriginId=true + const x4 = { ...obj4, id: importId4 }; // this import object already has an originId + const x8 = { ...obj8, id: importId8, originId: obj8.id }; // this import object doesn't have an originId, so it is set before create + const argObjs = [obj1, obj2, x3, x4, obj5, obj6, obj7, x8, obj9, obj10, obj11, obj12, obj13]; + expectBulkCreateArgs.objects(1, argObjs); + }; + const testBulkCreateOptions = async (namespace?: string) => { + const overwrite = (Symbol() as unknown) as boolean; + const options = setupOptions({ namespace, overwrite }); + setupMockResults(options); + + await createSavedObjects(objs, options); + expect(bulkCreate).toHaveBeenCalledTimes(1); + expectBulkCreateArgs.options(1, options); + }; + const testReturnValue = async (namespace?: string) => { + const options = setupOptions({ namespace }); + setupMockResults(options); + + const results = await createSavedObjects(objs, options); + const resultSavedObjects = (await bulkCreate.mock.results[0].value).saved_objects; + const [r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13] = resultSavedObjects; + // these three results are transformed before being returned, because the bulkCreate attempt used different IDs for them + const [x3, x4, x8] = [r3, r4, r8].map((x: SavedObject) => ({ ...x, destinationId: x.id })); + const transformedResults = [r1, r2, x3, x4, r5, r6, r7, x8, r9, r10, r11, r12, r13]; + const expectedResults = getExpectedResults(transformedResults, objs); + expect(results).toEqual(expectedResults); + }; - describe('with an undefined namespace', () => { - test('calls bulkCreate once with input objects', async () => { - await testBulkCreateObjects(); - }); - test('calls bulkCreate once with input options', async () => { - await testBulkCreateOptions(); - }); - test('returns bulkCreate results that are remapped to IDs of imported objects', async () => { - await testReturnValue(); - }); + describe('with an undefined namespace', () => { + test('calls bulkCreate once with input objects', async () => { + await testBulkCreateObjects(); }); - - describe('with a defined namespace', () => { - const namespace = 'some-namespace'; - test('calls bulkCreate once with input objects', async () => { - await testBulkCreateObjects(namespace); - }); - test('calls bulkCreate once with input options', async () => { - await testBulkCreateOptions(namespace); - }); - test('returns bulkCreate results that are remapped to IDs of imported objects', async () => { - await testReturnValue(namespace); - }); + test('calls bulkCreate once with input options', async () => { + await testBulkCreateOptions(); + }); + test('returns bulkCreate results that are remapped to IDs of imported objects', async () => { + await testReturnValue(); }); }); - describe('import with retries', () => { - const objs = [obj1, obj2, obj3, obj4, obj5, obj6, obj7, obj8, obj9, obj10, obj11, obj12, obj13]; - - const setupMockResults = (options: CreateSavedObjectsOptions) => { - bulkCreate.mockResolvedValueOnce({ - saved_objects: [ - getResultMock.success(obj1, options), - getResultMock.conflict(obj2.type, obj2.id), - getResultMock.conflict(obj3.type, importId3), - getResultMock.conflict(obj4.type, importId4), - getResultMock.unresolvableConflict(obj5.type, obj5.id), // unresolvable conflict will cause a retry - getResultMock.success(obj6, options), - getResultMock.conflict(obj7.type, obj7.id), - getResultMock.conflict(obj8.type, importId8), - getResultMock.unresolvableConflict(obj9.type, obj9.id), // unresolvable conflict will cause a retry - getResultMock.success(obj10, options), - getResultMock.conflict(obj11.type, obj11.id), - getResultMock.success(obj12, options), - getResultMock.conflict(obj13.type, obj13.id), - ], - }); - mockUuidv4.mockReturnValueOnce(`new-id-for-${obj5.id}`); - mockUuidv4.mockReturnValueOnce(`new-id-for-${obj9.id}`); - bulkCreate.mockResolvedValue({ - saved_objects: [ - getResultMock.success({ ...obj5, id: `new-id-for-${obj5.id}` }, options), // retry is a success - getResultMock.success({ ...obj9, id: `new-id-for-${obj9.id}` }, options), // retry is a success - ], - }); - }; - - const testBulkCreateObjects = async (namespace?: string) => { - const options = setupOptions({ namespace }); - setupMockResults(options); - - await createSavedObjects(objs, options); - expect(bulkCreate).toHaveBeenCalledTimes(2); - // these three objects are transformed before being created, because they are included in the `importIdMap` - const x3 = { ...obj3, id: importId3, originId: undefined }; // this import object already has an originId, but the entry has omitOriginId=true - const x4 = { ...obj4, id: importId4 }; // this import object already has an originId - const x8 = { ...obj8, id: importId8, originId: obj8.id }; // this import object doesn't have an originId, so it is set before create - const argObjs = [obj1, obj2, x3, x4, obj5, obj6, obj7, x8, obj9, obj10, obj11, obj12, obj13]; - expectBulkCreateArgs.objects(1, argObjs); // we expect to first try bulkCreate with all thirteen test cases - expectBulkCreateArgs.objects(2, [obj5, obj9], true); // we only expect to retry bulkCreate with the unresolvable conflicts - }; - const testBulkCreateOptions = async (namespace?: string) => { - const overwrite = (Symbol() as unknown) as boolean; - const options = setupOptions({ namespace, overwrite }); - setupMockResults(options); - - await createSavedObjects(objs, options); - expect(bulkCreate).toHaveBeenCalledTimes(2); - expectBulkCreateArgs.options(1, options); - expectBulkCreateArgs.options(2, options); - }; - const testReturnValue = async (namespace?: string) => { - const options = setupOptions({ namespace }); - setupMockResults(options); - - const createSavedObjectsResult = await createSavedObjects(objs, options); - const resultSavedObjects = (await bulkCreate.mock.results[0].value).saved_objects; - const [r1, r2, r3, r4, , r6, r7, r8, , r10, r11, r12, r13] = resultSavedObjects; - const [r5, r9] = (await bulkCreate.mock.results[1].value).saved_objects; // these two import objects were retried, so the retry results are returned - // these five results are transformed before being returned, because the bulkCreate attempt used different IDs for them - const [x3, x4, x5, x8, x9] = [r3, r4, r5, r8, r9].map((x: SavedObject) => ({ - ...x, - destinationId: x.id, - })); - const transformedResults = [r1, r2, x3, x4, x5, r6, r7, x8, x9, r10, r11, r12, r13]; - const expectedResults = getExpectedResults(transformedResults, objs); - expect(createSavedObjectsResult).toEqual(expectedResults); - }; - - describe('with an undefined namespace', () => { - test('calls bulkCreate once with input objects, and a second time for unresolvable conflicts', async () => { - await testBulkCreateObjects(); - }); - test('calls bulkCreate once with input options, and a second time with input options', async () => { - await testBulkCreateOptions(); - }); - test('returns bulkCreate results that are merged and remapped to IDs of imported objects', async () => { - await testReturnValue(); - }); + describe('with a defined namespace', () => { + const namespace = 'some-namespace'; + test('calls bulkCreate once with input objects', async () => { + await testBulkCreateObjects(namespace); }); - - describe('with a defined namespace', () => { - const namespace = 'some-namespace'; - test('calls bulkCreate once with input objects, and a second time for unresolvable conflicts', async () => { - await testBulkCreateObjects(namespace); - }); - test('calls bulkCreate once with input options, and a second time with input options', async () => { - await testBulkCreateOptions(namespace); - }); - test('returns bulkCreate results that are merged and remapped to IDs of imported objects', async () => { - await testReturnValue(namespace); - }); + test('calls bulkCreate once with input options', async () => { + await testBulkCreateOptions(namespace); + }); + test('returns bulkCreate results that are remapped to IDs of imported objects', async () => { + await testReturnValue(namespace); }); }); }); diff --git a/src/core/server/saved_objects/import/create_saved_objects.ts b/src/core/server/saved_objects/import/create_saved_objects.ts index 840b6daf59d54..92441bbe6b1cb 100644 --- a/src/core/server/saved_objects/import/create_saved_objects.ts +++ b/src/core/server/saved_objects/import/create_saved_objects.ts @@ -17,7 +17,6 @@ * under the License. */ -import { v4 as uuidv4 } from 'uuid'; import { SavedObject, SavedObjectsClientContract, SavedObjectsImportError } from '../types'; import { extractErrors } from './extract_errors'; @@ -32,31 +31,9 @@ interface CreateSavedObjectsResult { errors: SavedObjectsImportError[]; } -interface UnresolvableConflict { - retryIndex: number; - retryObject: SavedObject; -} -interface Left { - tag: 'left'; - value: UnresolvableConflict; -} -interface Right { - tag: 'right'; - value: SavedObject; -} -type Either = Left | Right; -const isLeft = (object: Either): object is Left => object.tag === 'left'; -const isRight = (object: Either): object is Right => object.tag === 'right'; - -const isUnresolvableConflict = (object: SavedObject) => - object.error?.statusCode === 409 && object.error?.metadata?.isNotOverwritable; - /** - * This function abstracts the bulk creation of import objects for two purposes: - * 1. The import ID map that was generated by the `checkOriginConflicts` function should dictate the IDs of the objects we create. - * 2. Any object create attempt that results in an unresolvable conflict should have its ID regenerated and retry create. This way, when an - * object with a "multi-namespace" type is exported from one namespace and imported to another, it does not result in an error, but - * instead a new object is created. + * This function abstracts the bulk creation of import objects. The main reason for this is that the import ID map should dictate the IDs of + * the objects we create, and the create results should be mapped to the original IDs that consumers will be able to understand. */ export const createSavedObjects = async ( objects: Array>, @@ -75,8 +52,8 @@ export const createSavedObjects = async ( new Map>() ); - // use the import ID map from the `checkOriginConflicts` or `getImportIdMapForRetries` function to ensure that each object is being - // created with the correct ID also, ensure that the `originId` is set on the created object if it did not have one + // use the import ID map to ensure that each object is being created with the correct ID also, ensure that the `originId` is set on the + // created object if it did not have one const objectsToCreate = objects.map((object) => { const importIdEntry = importIdMap.get(`${object.type}:${object.id}`); if (importIdEntry) { @@ -91,41 +68,11 @@ export const createSavedObjects = async ( overwrite, }); - // retry bulkCreate for multi-namespace saved objects that had an unresolvable conflict - // note: by definition, only multi-namespace saved objects can have an unresolvable conflict - let retryIndexCounter = 0; - const bulkCreateResults: Array> = bulkCreateResponse.saved_objects.map((result) => { - const object = objectIdMap.get(`${result.type}:${result.id}`)!; - if (isUnresolvableConflict(result)) { - const id = uuidv4(); - const originId = object.originId || object.id; - const retryObject = { ...object, id, originId }; - objectIdMap.set(`${retryObject.type}:${retryObject.id}`, object); - return { tag: 'left', value: { retryIndex: retryIndexCounter++, retryObject } }; - } - return { tag: 'right', value: result }; - }); - - // note: this is unrelated to "retries" that are passed into the `resolveSavedObjectsImportErrors` function - const retries = bulkCreateResults.filter(isLeft).map((x) => x.value.retryObject); - const retryResults = - retries.length > 0 - ? (await savedObjectsClient.bulkCreate(retries, { namespace, overwrite })).saved_objects - : []; - - const results: Array> = []; - bulkCreateResults.forEach((result) => { - if (isLeft(result)) { - const { retryIndex } = result.value; - results.push(retryResults[retryIndex]); - } else if (isRight(result)) { - results.push(result.value); - } - }); - // remap results to reflect the object IDs that were submitted for import // this ensures that consumers understand the results - const remappedResults = results.map & { destinationId?: string }>((result) => { + const remappedResults = bulkCreateResponse.saved_objects.map< + SavedObject & { destinationId?: string } + >((result) => { const { id } = objectIdMap.get(`${result.type}:${result.id}`)!; // also, include a `destinationId` field if the object create attempt was made with a different ID return { ...result, id, ...(id !== result.id && { destinationId: result.id }) }; 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 253d27ebf8f7d..0541b626b7fe2 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 @@ -32,11 +32,13 @@ import { importSavedObjectsFromStream } from './import_saved_objects'; import { collectSavedObjects } from './collect_saved_objects'; import { validateReferences } from './validate_references'; +import { checkConflicts } from './check_conflicts'; import { checkOriginConflicts } from './check_origin_conflicts'; import { createSavedObjects } from './create_saved_objects'; jest.mock('./collect_saved_objects'); jest.mock('./validate_references'); +jest.mock('./check_conflicts'); jest.mock('./check_origin_conflicts'); jest.mock('./create_saved_objects'); @@ -49,6 +51,12 @@ describe('#importSavedObjectsFromStream', () => { // mock empty output of each of these mocked modules so the import doesn't throw an error getMockFn(collectSavedObjects).mockResolvedValue({ errors: [], collectedObjects: [] }); getMockFn(validateReferences).mockResolvedValue({ errors: [], filteredObjects: [] }); + getMockFn(checkConflicts).mockResolvedValue({ + errors: [], + filteredObjects: [], + importIdMap: new Map(), + importIds: new Set(), + }); getMockFn(checkOriginConflicts).mockResolvedValue({ errors: [], filteredObjects: [], @@ -110,13 +118,38 @@ describe('#importSavedObjectsFromStream', () => { ); }); - test('checks origin conflicts', async () => { + test('checks conflicts', async () => { const options = setupOptions(); const filteredObjects = [createObject()]; getMockFn(validateReferences).mockResolvedValue({ errors: [], filteredObjects }); await importSavedObjectsFromStream(options); - const checkOriginConflictsOptions = { savedObjectsClient, typeRegistry, namespace }; + const checkConflictsOptions = { + savedObjectsClient, + namespace, + ignoreRegularConflicts: overwrite, + }; + expect(checkConflicts).toHaveBeenCalledWith(filteredObjects, checkConflictsOptions); + }); + + test('checks origin conflicts', async () => { + const options = setupOptions(); + const filteredObjects = [createObject()]; + const importIds = new Set(); + getMockFn(checkConflicts).mockResolvedValue({ + errors: [], + filteredObjects, + importIdMap: new Map(), + importIds, + }); + + await importSavedObjectsFromStream(options); + const checkOriginConflictsOptions = { + savedObjectsClient, + typeRegistry, + namespace, + importIds, + }; expect(checkOriginConflicts).toHaveBeenCalledWith( filteredObjects, checkOriginConflictsOptions @@ -126,14 +159,20 @@ describe('#importSavedObjectsFromStream', () => { test('creates saved objects', async () => { const options = setupOptions(); const filteredObjects = [createObject()]; - const importIdMap = new Map(); + getMockFn(checkConflicts).mockResolvedValue({ + errors: [], + filteredObjects, + importIdMap: new Map().set(`id1`, { id: `newId1` }), + importIds: new Set(), + }); getMockFn(checkOriginConflicts).mockResolvedValue({ errors: [], filteredObjects, - importIdMap, + importIdMap: new Map().set(`id2`, { id: `newId2` }), }); await importSavedObjectsFromStream(options); + const importIdMap = new Map().set(`id1`, { id: `newId1` }).set(`id2`, { id: `newId2` }); const createSavedObjectsOptions = { savedObjectsClient, importIdMap, overwrite, namespace }; expect(createSavedObjects).toHaveBeenCalledWith(filteredObjects, createSavedObjectsOptions); }); @@ -174,18 +213,24 @@ describe('#importSavedObjectsFromStream', () => { test('accumulates multiple errors', async () => { const options = setupOptions(); - const errors = [createError(), createError(), createError(), createError()]; + const errors = [createError(), createError(), createError(), createError(), createError()]; getMockFn(collectSavedObjects).mockResolvedValue({ errors: [errors[0]], collectedObjects: [], }); getMockFn(validateReferences).mockResolvedValue({ errors: [errors[1]], filteredObjects: [] }); - getMockFn(checkOriginConflicts).mockResolvedValue({ + getMockFn(checkConflicts).mockResolvedValue({ errors: [errors[2]], filteredObjects: [], importIdMap: new Map(), + importIds: new Set(), + }); + getMockFn(checkOriginConflicts).mockResolvedValue({ + errors: [errors[3]], + filteredObjects: [], + importIdMap: new Map(), }); - getMockFn(createSavedObjects).mockResolvedValue({ errors: [errors[3]], createdObjects: [] }); + getMockFn(createSavedObjects).mockResolvedValue({ errors: [errors[4]], createdObjects: [] }); const result = await importSavedObjectsFromStream(options); expect(result).toEqual({ success: false, successCount: 0, errors }); 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 9a1e6b6ae03ef..b07c7d31c276a 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.ts @@ -26,6 +26,7 @@ import { import { validateReferences } from './validate_references'; import { checkOriginConflicts } from './check_origin_conflicts'; import { createSavedObjects } from './create_saved_objects'; +import { checkConflicts } from './check_conflicts'; /** * Import saved objects from given stream. See the {@link SavedObjectsImportOptions | options} for more @@ -42,6 +43,7 @@ export async function importSavedObjectsFromStream({ namespace, }: SavedObjectsImportOptions): Promise { let errorAccumulator: SavedObjectsImportError[] = []; + let importIdMap: Map = new Map(); const supportedTypes = typeRegistry.getImportableAndExportableTypes().map((type) => type.name); // Get the objects to import @@ -59,28 +61,49 @@ export async function importSavedObjectsFromStream({ ); errorAccumulator = [...errorAccumulator, ...validateReferencesResult.errors]; - // Check multi-namespace object types for regular conflicts and ambiguous conflicts - const checkOriginConflictsOptions = { savedObjectsClient, typeRegistry, namespace }; - const { filteredObjects, errors: conflictErrors, importIdMap } = await checkOriginConflicts( + // Check single-namespace objects for conflicts in this namespace, and check multi-namespace objects for conflicts across all namespaces + const checkConflictsOptions = { + savedObjectsClient, + namespace, + ignoreRegularConflicts: overwrite, + }; + const checkConflictsResult = await checkConflicts( validateReferencesResult.filteredObjects, + checkConflictsOptions + ); + errorAccumulator = [...errorAccumulator, ...checkConflictsResult.errors]; + importIdMap = new Map([...importIdMap, ...checkConflictsResult.importIdMap]); + + // Check multi-namespace object types for origin conflicts in this namespace + const checkOriginConflictsOptions = { + savedObjectsClient, + typeRegistry, + namespace, + importIds: checkConflictsResult.importIds, + }; + const checkOriginConflictsResult = await checkOriginConflicts( + checkConflictsResult.filteredObjects, checkOriginConflictsOptions ); - errorAccumulator = [...errorAccumulator, ...conflictErrors]; + errorAccumulator = [...errorAccumulator, ...checkOriginConflictsResult.errors]; + importIdMap = new Map([...importIdMap, ...checkOriginConflictsResult.importIdMap]); // Create objects in bulk const createSavedObjectsOptions = { savedObjectsClient, importIdMap, overwrite, namespace }; - const { createdObjects, errors: bulkCreateErrors } = await createSavedObjects( - filteredObjects, + const createSavedObjectsResult = await createSavedObjects( + checkOriginConflictsResult.filteredObjects, createSavedObjectsOptions ); - errorAccumulator = [...errorAccumulator, ...bulkCreateErrors]; + errorAccumulator = [...errorAccumulator, ...createSavedObjectsResult.errors]; - const successResults = createdObjects.map(({ type, id, destinationId }) => { - return { type, id, ...(destinationId && { destinationId }) }; - }); + const successResults = createSavedObjectsResult.createdObjects.map( + ({ type, id, destinationId }) => { + return { type, id, ...(destinationId && { destinationId }) }; + } + ); return { - successCount: createdObjects.length, + successCount: createSavedObjectsResult.createdObjects.length, success: errorAccumulator.length === 0, ...(successResults.length && { successResults }), ...(errorAccumulator.length && { errors: errorAccumulator }), 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 aeccbb89485c9..1fab9ac6a8bb5 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 @@ -35,6 +35,7 @@ import { resolveSavedObjectsImportErrors } from './resolve_import_errors'; import { validateRetries } from './validate_retries'; import { collectSavedObjects } from './collect_saved_objects'; import { validateReferences } from './validate_references'; +import { checkConflicts } from './check_conflicts'; import { getImportIdMapForRetries } from './check_origin_conflicts'; import { splitOverwrites } from './split_overwrites'; import { createSavedObjects } from './create_saved_objects'; @@ -44,6 +45,7 @@ jest.mock('./validate_retries'); jest.mock('./create_objects_filter'); jest.mock('./collect_saved_objects'); jest.mock('./validate_references'); +jest.mock('./check_conflicts'); jest.mock('./check_origin_conflicts'); jest.mock('./split_overwrites'); jest.mock('./create_saved_objects'); @@ -58,6 +60,12 @@ describe('#importSavedObjectsFromStream', () => { getMockFn(createObjectsFilter).mockReturnValue(() => false); getMockFn(collectSavedObjects).mockResolvedValue({ errors: [], collectedObjects: [] }); getMockFn(validateReferences).mockResolvedValue({ errors: [], filteredObjects: [] }); + getMockFn(checkConflicts).mockResolvedValue({ + errors: [], + filteredObjects: [], + importIdMap: new Map(), + importIds: new Set(), + }); getMockFn(getImportIdMapForRetries).mockReturnValue(new Map()); getMockFn(splitOverwrites).mockReturnValue({ objectsToOverwrite: [], @@ -170,11 +178,30 @@ describe('#importSavedObjectsFromStream', () => { ); }); + test('checks conflicts', async () => { + const options = setupOptions(); + const filteredObjects = [createObject()]; + getMockFn(validateReferences).mockResolvedValue({ errors: [], filteredObjects }); + + await resolveSavedObjectsImportErrors(options); + const checkConflictsOptions = { + savedObjectsClient, + namespace, + ignoreRegularConflicts: true, + }; + expect(checkConflicts).toHaveBeenCalledWith(filteredObjects, checkConflictsOptions); + }); + test('gets import ID map for retries', async () => { const retries = [createRetry()]; const options = setupOptions(retries); const filteredObjects = [createObject()]; - getMockFn(validateReferences).mockResolvedValue({ errors: [], filteredObjects }); + getMockFn(checkConflicts).mockResolvedValue({ + errors: [], + filteredObjects, + importIdMap: new Map(), + importIds: new Set(), + }); await resolveSavedObjectsImportErrors(options); const opts = { typeRegistry, retries }; @@ -185,9 +212,11 @@ describe('#importSavedObjectsFromStream', () => { const retries = [createRetry()]; const options = setupOptions(retries); const filteredObjects = [createObject()]; - getMockFn(validateReferences).mockResolvedValue({ + getMockFn(checkConflicts).mockResolvedValue({ errors: [], filteredObjects, + importIdMap: new Map(), + importIds: new Set(), }); await resolveSavedObjectsImportErrors(options); @@ -196,8 +225,14 @@ describe('#importSavedObjectsFromStream', () => { test('creates saved objects', async () => { const options = setupOptions(); - const importIdMap = new Map(); - getMockFn(getImportIdMapForRetries).mockReturnValue(importIdMap); + getMockFn(checkConflicts).mockResolvedValue({ + errors: [], + filteredObjects: [], + importIdMap: new Map().set(`id1`, { id: `newId1` }), + importIds: new Set(), + }); + getMockFn(getImportIdMapForRetries).mockReturnValue(new Map().set(`id2`, { id: `newId2` })); + const importIdMap = new Map().set(`id1`, { id: `newId1` }).set(`id2`, { id: `newId2` }); const objectsToOverwrite = [createObject()]; const objectsToNotOverwrite = [createObject()]; getMockFn(splitOverwrites).mockReturnValue({ objectsToOverwrite, objectsToNotOverwrite }); 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 140e472349f66..36b2ea53d6207 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.ts @@ -29,6 +29,7 @@ import { validateRetries } from './validate_retries'; import { createSavedObjects } from './create_saved_objects'; import { getImportIdMapForRetries } from './check_origin_conflicts'; import { SavedObject } from '../types'; +import { checkConflicts } from './check_conflicts'; /** * Resolve and return saved object import errors. @@ -49,6 +50,7 @@ export async function resolveSavedObjectsImportErrors({ let successCount = 0; let errorAccumulator: SavedObjectsImportError[] = []; + let importIdMap: Map = new Map(); const supportedTypes = typeRegistry.getImportableAndExportableTypes().map((type) => type.name); const filter = createObjectsFilter(retries); @@ -88,15 +90,28 @@ export async function resolveSavedObjectsImportErrors({ } // Validate references - const { filteredObjects, errors: referenceErrors } = await validateReferences( + const validateReferencesResult = await validateReferences( objectsToResolve, savedObjectsClient, namespace ); - errorAccumulator = [...errorAccumulator, ...referenceErrors]; + errorAccumulator = [...errorAccumulator, ...validateReferencesResult.errors]; + + // Check single-namespace objects for conflicts in this namespace, and check multi-namespace objects for conflicts across all namespaces + const checkConflictsOptions = { savedObjectsClient, namespace, ignoreRegularConflicts: true }; + const checkConflictsResult = await checkConflicts( + validateReferencesResult.filteredObjects, + checkConflictsOptions + ); + errorAccumulator = [...errorAccumulator, ...checkConflictsResult.errors]; + importIdMap = new Map([...importIdMap, ...checkConflictsResult.importIdMap]); // Check multi-namespace object types for regular conflicts and ambiguous conflicts - const importIdMap = getImportIdMapForRetries(filteredObjects, { typeRegistry, retries }); + const importIdMapForRetries = getImportIdMapForRetries(checkConflictsResult.filteredObjects, { + typeRegistry, + retries, + }); + importIdMap = new Map([...importIdMap, ...importIdMapForRetries]); // Bulk create in two batches, overwrites and non-overwrites let successResults: Array<{ type: string; id: string; destinationId?: string }> = []; @@ -114,7 +129,10 @@ export async function resolveSavedObjectsImportErrors({ })), ]; }; - const { objectsToOverwrite, objectsToNotOverwrite } = splitOverwrites(filteredObjects, retries); + const { objectsToOverwrite, objectsToNotOverwrite } = splitOverwrites( + checkConflictsResult.filteredObjects, + retries + ); await bulkCreateObjects(objectsToOverwrite, true); await bulkCreateObjects(objectsToNotOverwrite); diff --git a/src/core/server/saved_objects/routes/integration_tests/import.test.ts b/src/core/server/saved_objects/routes/integration_tests/import.test.ts index ae90f19220b8a..0ddecd4486aa8 100644 --- a/src/core/server/saved_objects/routes/integration_tests/import.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/import.test.ts @@ -54,6 +54,7 @@ describe('POST /internal/saved_objects/_import', () => { savedObjectsClient = handlerContext.savedObjects.client; savedObjectsClient.find.mockResolvedValue(emptyResponse); + savedObjectsClient.checkConflicts.mockResolvedValue({ errors: [] }); const router = httpSetup.createRouter('/internal/saved_objects/'); registerImportRoute(router, config); @@ -184,16 +185,18 @@ describe('POST /internal/saved_objects/_import', () => { it('imports an index pattern and dashboard but has a conflict on the index pattern', async () => { // NOTE: changes to this scenario should be reflected in the docs - savedObjectsClient.bulkCreate.mockResolvedValueOnce({ - saved_objects: [ + savedObjectsClient.checkConflicts.mockResolvedValue({ + errors: [ { type: 'index-pattern', id: 'my-pattern', - attributes: {}, - references: [], error: SavedObjectsErrorHelpers.createConflictError('index-pattern', 'my-pattern').output .payload, }, + ], + }); + savedObjectsClient.bulkCreate.mockResolvedValueOnce({ + saved_objects: [ { type: 'dashboard', id: 'my-dashboard', diff --git a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts index e5bfd101fb796..045abfa83fa82 100644 --- a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts @@ -45,6 +45,7 @@ describe('POST /api/saved_objects/_resolve_import_errors', () => { ); savedObjectsClient = handlerContext.savedObjects.client; + savedObjectsClient.checkConflicts.mockResolvedValue({ errors: [] }); const router = httpSetup.createRouter('/api/saved_objects/'); registerResolveImportErrorsRoute(router, config); 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 24c43d9e2da79..06f955733e4f0 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -1512,7 +1512,9 @@ describe('SavedObjectsRepository', () => { describe('returns', () => { it(`expected results`, async () => { - const objects = [obj1, obj2, obj3, obj4, obj5, obj6, obj7]; + const unknownTypeObj = { type: 'unknownType', id: 'three' }; + const hiddenTypeObj = { type: HIDDEN_TYPE, id: 'three' }; + const objects = [unknownTypeObj, hiddenTypeObj, obj1, obj2, obj3, obj4, obj5, obj6, obj7]; const response = { status: 200, docs: [ @@ -1531,6 +1533,8 @@ describe('SavedObjectsRepository', () => { expectClusterCalls('mget'); expect(result).toEqual({ errors: [ + { ...unknownTypeObj, error: createUnsupportedTypeError(unknownTypeObj.type) }, + { ...hiddenTypeObj, error: createUnsupportedTypeError(hiddenTypeObj.type) }, { ...obj1, error: createConflictError(obj1.type, obj1.id) }, // obj2 was not found so it does not result in a conflict error { ...obj3, error: createConflictError(obj3.type, obj3.id) }, diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 9f4e1b75fd215..60cc9fcf624f5 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -451,28 +451,60 @@ export class SavedObjectsRepository { } const { namespace } = options; - const bulkGetDocs = objects.map(({ type, id }) => ({ + + let bulkGetRequestIndexCounter = 0; + const expectedBulkGetResults: Either[] = objects.map((object) => { + const { type, id } = object; + + if (!this._allowedTypes.includes(type)) { + return { + tag: 'Left' as 'Left', + error: { + id, + type, + error: SavedObjectsErrorHelpers.createUnsupportedTypeError(type).output.payload, + }, + }; + } + + return { + tag: 'Right' as 'Right', + value: { + type, + id, + esRequestIndex: bulkGetRequestIndexCounter++, + }, + }; + }); + + const bulkGetDocs = expectedBulkGetResults.filter(isRight).map(({ value: { type, id } }) => ({ _id: this._serializer.generateRawId(namespace, type, id), _index: this.getIndexForType(type), _source: ['type', 'namespaces'], })); - const bulkGetResponse = await this._callCluster('mget', { - body: { docs: bulkGetDocs }, - ignore: [404], - }); + const bulkGetResponse = bulkGetDocs.length + ? await this._callCluster('mget', { + body: { docs: bulkGetDocs }, + ignore: [404], + }) + : undefined; const errors: SavedObjectsCheckConflictsResponse['errors'] = []; - const indexFound = bulkGetResponse.status !== 404; - objects.forEach(({ type, id }, index) => { - const actualResult = indexFound ? bulkGetResponse.docs[index] : undefined; - const docFound = actualResult?.found === true; - if (docFound) { + expectedBulkGetResults.forEach((expectedResult) => { + if (isLeft(expectedResult)) { + errors.push(expectedResult.error as any); + return; + } + + const { type, id, esRequestIndex } = expectedResult.value; + const doc = bulkGetResponse.docs[esRequestIndex]; + if (doc.found) { errors.push({ id, type, error: { ...SavedObjectsErrorHelpers.createConflictError(type, id).output.payload, - ...(!this.rawDocExistsInNamespace(actualResult, namespace) && { + ...(!this.rawDocExistsInNamespace(doc, namespace) && { metadata: { isNotOverwritable: true }, }), }, diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts index ffa727f27c037..01a9504c62fed 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts @@ -61,7 +61,7 @@ const expectGeneralError = async (fn: Function, args: Record) => { * Requires that function args are passed in as key/value pairs * The argument properties must be in the correct order to be spread properly */ -const expectForbiddenError = async (fn: Function, args: Record) => { +const expectForbiddenError = async (fn: Function, args: Record, action?: string) => { clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( getMockCheckPrivilegesFailure ); @@ -84,7 +84,7 @@ const expectForbiddenError = async (fn: Function, args: Record) => expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( USERNAME, - ACTION, + action ?? ACTION, types, spaceIds, missing, @@ -93,7 +93,7 @@ const expectForbiddenError = async (fn: Function, args: Record) => expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); }; -const expectSuccess = async (fn: Function, args: Record) => { +const expectSuccess = async (fn: Function, args: Record, action?: string) => { const result = await fn.bind(client)(...Object.values(args)); const getCalls = (clientOpts.actions.savedObject.get as jest.MockedFunction< SavedObjectActions['get'] @@ -106,7 +106,7 @@ const expectSuccess = async (fn: Function, args: Record) => { expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(1); expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( USERNAME, - ACTION, + action ?? ACTION, types, spaceIds, args @@ -475,8 +475,8 @@ describe('#bulkUpdate', () => { }); describe('#checkConflicts', () => { - const obj1 = Object.freeze({ type: 'foo', otherThing: 'sup' }); - const obj2 = Object.freeze({ type: 'bar', otherThing: 'everyone' }); + const obj1 = Object.freeze({ type: 'foo', id: 'foo-id' }); + const obj2 = Object.freeze({ type: 'bar', id: 'bar-id' }); const options = Object.freeze({ namespace: 'some-ns' }); test(`throws decorated GeneralError when checkPrivileges.globally rejects promise`, async () => { @@ -486,7 +486,7 @@ describe('#checkConflicts', () => { test(`throws decorated ForbiddenError when unauthorized`, async () => { const objects = [obj1, obj2]; - await expectForbiddenError(client.checkConflicts, { objects, options }); + await expectForbiddenError(client.checkConflicts, { objects, options }, 'checkConflicts'); }); test(`returns result of baseClient.create when authorized`, async () => { @@ -494,7 +494,11 @@ describe('#checkConflicts', () => { clientOpts.baseClient.checkConflicts.mockResolvedValue(apiCallReturnValue as any); const objects = [obj1, obj2]; - const result = await expectSuccess(client.checkConflicts, { objects, options }); + const result = await expectSuccess( + client.checkConflicts, + { objects, options }, + 'checkConflicts' + ); expect(result).toBe(apiCallReturnValue); }); diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts index 8b38199063650..8cebcf6140ca7 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts @@ -82,10 +82,9 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra objects: SavedObjectsCheckConflictsObject[] = [], options: SavedObjectsBaseOptions = {} ) { - await this.ensureAuthorized(this.getUniqueObjectTypes(objects), 'create', options.namespace, { - objects, - options, - }); + const types = this.getUniqueObjectTypes(objects); + const args = { objects, options }; + await this.ensureAuthorized(types, 'bulk_create', options.namespace, args, 'checkConflicts'); const response = await this.baseClient.checkConflicts(objects, options); return response; diff --git a/x-pack/test/saved_object_api_integration/common/suites/import.ts b/x-pack/test/saved_object_api_integration/common/suites/import.ts index f03f78c87dcb5..f17c0ab20ec00 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/import.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/import.ts @@ -21,7 +21,6 @@ export interface ImportTestCase extends TestCase { expectedNewId?: string; successParam?: string; failure?: 400 | 409; // only used for permitted response case - fail403Param?: string; fail409Param?: string; } @@ -75,13 +74,12 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe const expectResponseBody = ( testCases: ImportTestCase | ImportTestCase[], statusCode: 200 | 403, - fail403Param?: string, spaceId = SPACES.DEFAULT.spaceId ): ExpectResponseBody => async (response: Record) => { const testCaseArray = Array.isArray(testCases) ? testCases : [testCases]; if (statusCode === 403) { const types = testCaseArray.map((x) => x.type); - await expectResponses.forbidden(fail403Param!)(types)(response); + await expectResponses.forbidden('bulk_create')(types)(response); } else { // permitted const { success, successCount, successResults, errors } = response.body; @@ -170,7 +168,6 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe spaceId?: string; singleRequest?: boolean; responseBodyOverride?: ExpectResponseBody; - fail403Param?: string; } ): ImportTestDefinition[] => { const cases = Array.isArray(testCases) ? testCases : [testCases]; @@ -184,7 +181,7 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe responseStatusCode, responseBody: options?.responseBodyOverride || - expectResponseBody(x, responseStatusCode, options?.fail403Param, options?.spaceId), + expectResponseBody(x, responseStatusCode, options?.spaceId), overwrite, })); } @@ -196,7 +193,7 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe responseStatusCode, responseBody: options?.responseBodyOverride || - expectResponseBody(cases, responseStatusCode, options?.fail403Param, options?.spaceId), + expectResponseBody(cases, responseStatusCode, options?.spaceId), overwrite, }, ]; diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts index 90bcdd5824786..cf26df17f2c32 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts @@ -92,53 +92,24 @@ export default function ({ getService }: FtrProviderContext) { spaceId ); // use singleRequest to reduce execution time and/or test combined cases - const unauthorizedCommon = [ - createTestDefinitions(group1Importable, true, overwrite, { - spaceId, - fail403Param: 'bulk_create', - }), - createTestDefinitions(group1NonImportable, false, overwrite, { - spaceId, - singleRequest: true, - }), - createTestDefinitions(group1All, true, overwrite, { - spaceId, - singleRequest: true, - responseBodyOverride: expectForbidden('bulk_create')([ - 'dashboard', - 'globaltype', - 'isolatedtype', - ]), - }), - ]; return { - unauthorizedRead: [ - ...unauthorizedCommon, - // multi-namespace types result in a preflight search request before a create attempt; - // because of this, importing those types will result in a 403 "find" error (as opposed to a 403 "bulk_create" error) - createTestDefinitions(group2, true, overwrite, { + unauthorized: [ + createTestDefinitions(group1Importable, true, overwrite, { spaceId }), + createTestDefinitions(group1NonImportable, false, overwrite, { spaceId, singleRequest: true, - fail403Param: 'find', }), - createTestDefinitions(group3, true, overwrite, { + createTestDefinitions(group1All, true, overwrite, { spaceId, singleRequest: true, - fail403Param: 'find', - }), - ].flat(), - unauthorizedWrite: [ - ...unauthorizedCommon, - createTestDefinitions(group2, true, overwrite, { - spaceId, - singleRequest: true, - fail403Param: 'bulk_create', - }), - createTestDefinitions(group3, true, overwrite, { - spaceId, - singleRequest: true, - fail403Param: 'bulk_create', + responseBodyOverride: expectForbidden('bulk_create')([ + 'dashboard', + 'globaltype', + 'isolatedtype', + ]), }), + createTestDefinitions(group2, true, overwrite, { spaceId, singleRequest: true }), + createTestDefinitions(group3, true, overwrite, { spaceId, singleRequest: true }), ].flat(), authorized: [ createTestDefinitions(group1All, false, overwrite, { spaceId, singleRequest: true }), @@ -152,19 +123,20 @@ export default function ({ getService }: FtrProviderContext) { getTestScenarios([false, true]).securityAndSpaces.forEach( ({ spaceId, users, modifier: overwrite }) => { const suffix = ` within the ${spaceId} space${overwrite ? ' with overwrite enabled' : ''}`; - const { unauthorizedRead, unauthorizedWrite, authorized } = createTests( - overwrite!, - spaceId - ); + const { unauthorized, authorized } = createTests(overwrite!, spaceId); const _addTests = (user: TestUser, tests: ImportTestDefinition[]) => { addTests(`${user.description}${suffix}`, { user, spaceId, tests }); }; - [users.noAccess, users.legacyAll, users.allAtOtherSpace].forEach((user) => { - _addTests(user, unauthorizedRead); - }); - [users.dualRead, users.readGlobally, users.readAtSpace].forEach((user) => { - _addTests(user, unauthorizedWrite); + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.readAtSpace, + users.allAtOtherSpace, + ].forEach((user) => { + _addTests(user, unauthorized); }); [users.dualAll, users.allGlobally, users.allAtSpace, users.superuser].forEach((user) => { _addTests(user, authorized); diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/import.ts b/x-pack/test/saved_object_api_integration/security_only/apis/import.ts index bd29367aaeeeb..d04c0c43412eb 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/import.ts @@ -70,42 +70,20 @@ export default function ({ getService }: FtrProviderContext) { overwrite ); // use singleRequest to reduce execution time and/or test combined cases - const unauthorizedCommon = [ - createTestDefinitions(group1Importable, true, overwrite, { fail403Param: 'bulk_create' }), - createTestDefinitions(group1NonImportable, false, overwrite, { singleRequest: true }), - createTestDefinitions(group1All, true, overwrite, { - singleRequest: true, - responseBodyOverride: expectForbidden('bulk_create')([ - 'dashboard', - 'globaltype', - 'isolatedtype', - ]), - }), - ]; return { - unauthorizedRead: [ - ...unauthorizedCommon, - // multi-namespace types result in a preflight search request before a create attempt; - // because of this, importing those types will result in a 403 "find" error (as opposed to a 403 "bulk_create" error) - createTestDefinitions(group2, true, overwrite, { + unauthorized: [ + createTestDefinitions(group1Importable, true, overwrite), + createTestDefinitions(group1NonImportable, false, overwrite, { singleRequest: true }), + createTestDefinitions(group1All, true, overwrite, { singleRequest: true, - fail403Param: 'find', - }), - createTestDefinitions(group3, true, overwrite, { - singleRequest: true, - fail403Param: 'find', - }), - ].flat(), - unauthorizedWrite: [ - ...unauthorizedCommon, - createTestDefinitions(group2, true, overwrite, { - singleRequest: true, - fail403Param: 'bulk_create', - }), - createTestDefinitions(group3, true, overwrite, { - singleRequest: true, - fail403Param: 'bulk_create', + responseBodyOverride: expectForbidden('bulk_create')([ + 'dashboard', + 'globaltype', + 'isolatedtype', + ]), }), + createTestDefinitions(group2, true, overwrite, { singleRequest: true }), + createTestDefinitions(group3, true, overwrite, { singleRequest: true }), ].flat(), authorized: [ createTestDefinitions(group1All, false, overwrite, { singleRequest: true }), @@ -118,7 +96,7 @@ export default function ({ getService }: FtrProviderContext) { describe('_import', () => { getTestScenarios([false, true]).security.forEach(({ users, modifier: overwrite }) => { const suffix = overwrite ? ' with overwrite enabled' : ''; - const { unauthorizedRead, unauthorizedWrite, authorized } = createTests(overwrite!); + const { unauthorized, authorized } = createTests(overwrite!); const _addTests = (user: TestUser, tests: ImportTestDefinition[]) => { addTests(`${user.description}${suffix}`, { user, tests }); }; @@ -126,15 +104,14 @@ export default function ({ getService }: FtrProviderContext) { [ users.noAccess, users.legacyAll, + users.dualRead, + users.readGlobally, users.allAtDefaultSpace, users.readAtDefaultSpace, users.allAtSpace1, users.readAtSpace1, ].forEach((user) => { - _addTests(user, unauthorizedRead); - }); - [users.dualRead, users.readGlobally].forEach((user) => { - _addTests(user, unauthorizedWrite); + _addTests(user, unauthorized); }); [users.dualAll, users.allGlobally, users.superuser].forEach((user) => { _addTests(user, authorized); From 4d5756138b32ed9f4e8aa9e23eb6ad1e180d28e0 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Fri, 26 Jun 2020 18:01:39 -0400 Subject: [PATCH 23/55] Fix mock for `find` results A recent PR added the `score` field to `find` results. Had to add this to pass the type checker. --- .../server/saved_objects/import/check_origin_conflicts.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/server/saved_objects/import/check_origin_conflicts.test.ts b/src/core/server/saved_objects/import/check_origin_conflicts.test.ts index 1b91e87d183df..d872b3ca35549 100644 --- a/src/core/server/saved_objects/import/check_origin_conflicts.test.ts +++ b/src/core/server/saved_objects/import/check_origin_conflicts.test.ts @@ -61,7 +61,7 @@ describe('#checkOriginConflicts', () => { page: 1, per_page: 10, total: objects.length, - saved_objects: objects, + saved_objects: objects.map((object) => ({ ...object, score: 0 })), }); const setupOptions = ( From 26db438f766ccf3903fca25056b251187840eac2 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Fri, 26 Jun 2020 18:05:35 -0400 Subject: [PATCH 24/55] Add "true copy" mode for imports --- ...ore-server.importsavedobjectsfromstream.md | 4 +- .../core/server/kibana-plugin-core-server.md | 2 +- ...n-core-server.savedobjectsimportoptions.md | 3 +- ...ver.savedobjectsimportoptions.overwrite.md | 7 +- ...rver.savedobjectsimportoptions.truecopy.md | 16 ++ .../import/import_saved_objects.test.ts | 155 ++++++++++++------ .../import/import_saved_objects.ts | 61 ++++--- .../import/regenerate_ids.test.ts | 52 ++++++ .../saved_objects/import/regenerate_ids.ts | 33 ++++ src/core/server/saved_objects/import/types.ts | 13 +- .../server/saved_objects/routes/import.ts | 19 ++- src/core/server/server.api.md | 5 +- .../lib/copy_to_spaces/copy_to_spaces.test.ts | 5 + .../lib/copy_to_spaces/copy_to_spaces.ts | 1 + .../spaces/server/lib/copy_to_spaces/types.ts | 1 + .../routes/api/external/copy_to_space.test.ts | 15 ++ .../routes/api/external/copy_to_space.ts | 85 ++++++---- .../common/suites/import.ts | 29 +++- .../security_and_spaces/apis/import.ts | 108 ++++++++---- .../security_only/apis/import.ts | 71 ++++++-- .../spaces_only/apis/import.ts | 37 ++++- 21 files changed, 536 insertions(+), 186 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.truecopy.md create mode 100644 src/core/server/saved_objects/import/regenerate_ids.test.ts create mode 100644 src/core/server/saved_objects/import/regenerate_ids.ts diff --git a/docs/development/core/server/kibana-plugin-core-server.importsavedobjectsfromstream.md b/docs/development/core/server/kibana-plugin-core-server.importsavedobjectsfromstream.md index df79aff35ea78..bea33149927d7 100644 --- a/docs/development/core/server/kibana-plugin-core-server.importsavedobjectsfromstream.md +++ b/docs/development/core/server/kibana-plugin-core-server.importsavedobjectsfromstream.md @@ -9,14 +9,14 @@ Import saved objects from given stream. See the [options](./kibana-plugin-core-s Signature: ```typescript -export declare function importSavedObjectsFromStream({ readStream, objectLimit, overwrite, savedObjectsClient, typeRegistry, namespace, }: SavedObjectsImportOptions): Promise; +export declare function importSavedObjectsFromStream({ readStream, objectLimit, overwrite, trueCopy, savedObjectsClient, typeRegistry, namespace, }: SavedObjectsImportOptions): Promise; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| { readStream, objectLimit, overwrite, savedObjectsClient, typeRegistry, namespace, } | SavedObjectsImportOptions | | +| { readStream, objectLimit, overwrite, trueCopy, savedObjectsClient, typeRegistry, namespace, } | SavedObjectsImportOptions | | Returns: diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 5dbf955993c2b..24534b9de57cb 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -45,7 +45,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [deepFreeze(object)](./kibana-plugin-core-server.deepfreeze.md) | Apply Object.freeze to a value recursively and convert the return type to Readonly variant recursively | | [exportSavedObjectsToStream({ types, objects, search, savedObjectsClient, exportSizeLimit, includeReferencesDeep, excludeExportDetails, namespace, })](./kibana-plugin-core-server.exportsavedobjectstostream.md) | Generates sorted saved object stream to be used for export. See the [options](./kibana-plugin-core-server.savedobjectsexportoptions.md) for more detailed information. | | [getFlattenedObject(rootValue)](./kibana-plugin-core-server.getflattenedobject.md) | Flattens a deeply nested object to a map of dot-separated paths pointing to all primitive values \*\*and arrays\*\* from rootValue.example: getFlattenedObject({ a: { b: 1, c: \[2,3\] } }) // => { 'a.b': 1, 'a.c': \[2,3\] } | -| [importSavedObjectsFromStream({ readStream, objectLimit, overwrite, savedObjectsClient, typeRegistry, namespace, })](./kibana-plugin-core-server.importsavedobjectsfromstream.md) | Import saved objects from given stream. See the [options](./kibana-plugin-core-server.savedobjectsimportoptions.md) for more detailed information. | +| [importSavedObjectsFromStream({ readStream, objectLimit, overwrite, trueCopy, savedObjectsClient, typeRegistry, namespace, })](./kibana-plugin-core-server.importsavedobjectsfromstream.md) | Import saved objects from given stream. See the [options](./kibana-plugin-core-server.savedobjectsimportoptions.md) for more detailed information. | | [isRelativeUrl(candidatePath)](./kibana-plugin-core-server.isrelativeurl.md) | Determine if a url is relative. Any url including a protocol, hostname, or port is not considered relative. This means that absolute \*paths\* are considered to be relative \*urls\* | | [modifyUrl(url, urlModifier)](./kibana-plugin-core-server.modifyurl.md) | Takes a URL and a function that takes the meaningful parts of the URL as a key-value object, modifies some or all of the parts, and returns the modified parts formatted again as a url.Url Parts sent: - protocol - slashes (does the url have the //) - auth - hostname (just the name of the host, no port or auth information) - port - pathname (the path after the hostname, no query or hash, starts with a slash if there was a path) - query (always an object, even when no query on original url) - hashWhy? - The default url library in node produces several conflicting properties on the "parsed" output. Modifying any of these might lead to the modifications being ignored (depending on which property was modified) - It's not always clear whether to use path/pathname, host/hostname, so this tries to add helpful constraints | | [resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, typeRegistry, namespace, })](./kibana-plugin-core-server.resolvesavedobjectsimporterrors.md) | Resolve and return saved object import errors. See the [options](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md) for more detailed informations. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.md index 0a307aa07aa73..ad2930a999490 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.md @@ -18,8 +18,9 @@ export interface SavedObjectsImportOptions | --- | --- | --- | | [namespace](./kibana-plugin-core-server.savedobjectsimportoptions.namespace.md) | string | if specified, will import in given namespace, else will import as global object | | [objectLimit](./kibana-plugin-core-server.savedobjectsimportoptions.objectlimit.md) | number | The maximum number of object to import | -| [overwrite](./kibana-plugin-core-server.savedobjectsimportoptions.overwrite.md) | boolean | if true, will override existing object if present | +| [overwrite](./kibana-plugin-core-server.savedobjectsimportoptions.overwrite.md) | boolean | | | [readStream](./kibana-plugin-core-server.savedobjectsimportoptions.readstream.md) | Readable | The stream of [saved objects](./kibana-plugin-core-server.savedobject.md) to import | | [savedObjectsClient](./kibana-plugin-core-server.savedobjectsimportoptions.savedobjectsclient.md) | SavedObjectsClientContract | [client](./kibana-plugin-core-server.savedobjectsclientcontract.md) to use to perform the import operation | +| [trueCopy](./kibana-plugin-core-server.savedobjectsimportoptions.truecopy.md) | boolean | | | [typeRegistry](./kibana-plugin-core-server.savedobjectsimportoptions.typeregistry.md) | ISavedObjectTypeRegistry | The registry of all known saved object types | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.overwrite.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.overwrite.md index e42d04c5a9180..e38e370d601ff 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.overwrite.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.overwrite.md @@ -4,7 +4,12 @@ ## SavedObjectsImportOptions.overwrite property -if true, will override existing object if present +> Warning: This API is now obsolete. +> +> If true, will override existing object if present. This option will be removed and permanently disabled in a future release. +> +> Note: this has no effect when used with the `trueCopy` option. +> Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.truecopy.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.truecopy.md new file mode 100644 index 0000000000000..89918e1ffc59e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.truecopy.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportOptions](./kibana-plugin-core-server.savedobjectsimportoptions.md) > [trueCopy](./kibana-plugin-core-server.savedobjectsimportoptions.truecopy.md) + +## SavedObjectsImportOptions.trueCopy property + +> Warning: This API is now obsolete. +> +> If true, will create new copies of import objects, each with a random `id` and undefined `originId`. This option will be removed and permanently enabled in a future release. +> + +Signature: + +```typescript +trueCopy: boolean; +``` 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 0541b626b7fe2..226d38e8517e2 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 @@ -31,12 +31,14 @@ import { typeRegistryMock } from '../saved_objects_type_registry.mock'; import { importSavedObjectsFromStream } from './import_saved_objects'; import { collectSavedObjects } from './collect_saved_objects'; +import { regenerateIds } from './regenerate_ids'; import { validateReferences } from './validate_references'; import { checkConflicts } from './check_conflicts'; import { checkOriginConflicts } from './check_origin_conflicts'; import { createSavedObjects } from './create_saved_objects'; jest.mock('./collect_saved_objects'); +jest.mock('./regenerate_ids'); jest.mock('./validate_references'); jest.mock('./check_conflicts'); jest.mock('./check_origin_conflicts'); @@ -50,6 +52,7 @@ describe('#importSavedObjectsFromStream', () => { jest.clearAllMocks(); // mock empty output of each of these mocked modules so the import doesn't throw an error getMockFn(collectSavedObjects).mockResolvedValue({ errors: [], collectedObjects: [] }); + getMockFn(regenerateIds).mockReturnValue({ importIdMap: new Map() }); getMockFn(validateReferences).mockResolvedValue({ errors: [], filteredObjects: [] }); getMockFn(checkConflicts).mockResolvedValue({ errors: [], @@ -72,11 +75,19 @@ describe('#importSavedObjectsFromStream', () => { let typeRegistry: jest.Mocked; const namespace = 'some-namespace'; - const setupOptions = (): SavedObjectsImportOptions => { + const setupOptions = (trueCopy: boolean = false): SavedObjectsImportOptions => { readStream = new Readable(); savedObjectsClient = savedObjectsClientMock.create(); typeRegistry = typeRegistryMock.create(); - return { readStream, objectLimit, overwrite, savedObjectsClient, typeRegistry, namespace }; + return { + readStream, + objectLimit, + overwrite, + savedObjectsClient, + typeRegistry, + namespace, + trueCopy, + }; }; const createObject = () => { return ({ type: 'foo-type', id: uuidv4() } as unknown) as SavedObject<{ title: string }>; @@ -118,63 +129,107 @@ describe('#importSavedObjectsFromStream', () => { ); }); - test('checks conflicts', async () => { - const options = setupOptions(); - const filteredObjects = [createObject()]; - getMockFn(validateReferences).mockResolvedValue({ errors: [], filteredObjects }); + describe('with trueCopy disabled', () => { + test('does not regenerate object IDs', async () => { + const options = setupOptions(); + const collectedObjects = [createObject()]; + getMockFn(collectSavedObjects).mockResolvedValue({ errors: [], collectedObjects }); - await importSavedObjectsFromStream(options); - const checkConflictsOptions = { - savedObjectsClient, - namespace, - ignoreRegularConflicts: overwrite, - }; - expect(checkConflicts).toHaveBeenCalledWith(filteredObjects, checkConflictsOptions); - }); + await importSavedObjectsFromStream(options); + expect(regenerateIds).not.toHaveBeenCalled(); + }); - test('checks origin conflicts', async () => { - const options = setupOptions(); - const filteredObjects = [createObject()]; - const importIds = new Set(); - getMockFn(checkConflicts).mockResolvedValue({ - errors: [], - filteredObjects, - importIdMap: new Map(), - importIds, + test('checks conflicts', async () => { + const options = setupOptions(); + const filteredObjects = [createObject()]; + getMockFn(validateReferences).mockResolvedValue({ errors: [], filteredObjects }); + + await importSavedObjectsFromStream(options); + const checkConflictsOptions = { + savedObjectsClient, + namespace, + ignoreRegularConflicts: overwrite, + }; + expect(checkConflicts).toHaveBeenCalledWith(filteredObjects, checkConflictsOptions); }); - await importSavedObjectsFromStream(options); - const checkOriginConflictsOptions = { - savedObjectsClient, - typeRegistry, - namespace, - importIds, - }; - expect(checkOriginConflicts).toHaveBeenCalledWith( - filteredObjects, - checkOriginConflictsOptions - ); + test('checks origin conflicts', async () => { + const options = setupOptions(); + const filteredObjects = [createObject()]; + const importIds = new Set(); + getMockFn(checkConflicts).mockResolvedValue({ + errors: [], + filteredObjects, + importIdMap: new Map(), + importIds, + }); + + await importSavedObjectsFromStream(options); + const checkOriginConflictsOptions = { + savedObjectsClient, + typeRegistry, + namespace, + importIds, + }; + expect(checkOriginConflicts).toHaveBeenCalledWith( + filteredObjects, + checkOriginConflictsOptions + ); + }); + + test('creates saved objects', async () => { + const options = setupOptions(); + const filteredObjects = [createObject()]; + getMockFn(checkConflicts).mockResolvedValue({ + errors: [], + filteredObjects, + importIdMap: new Map().set(`id1`, { id: `newId1` }), + importIds: new Set(), + }); + getMockFn(checkOriginConflicts).mockResolvedValue({ + errors: [], + filteredObjects, + importIdMap: new Map().set(`id2`, { id: `newId2` }), + }); + + await importSavedObjectsFromStream(options); + const importIdMap = new Map().set(`id1`, { id: `newId1` }).set(`id2`, { id: `newId2` }); + const createSavedObjectsOptions = { savedObjectsClient, importIdMap, overwrite, namespace }; + expect(createSavedObjects).toHaveBeenCalledWith(filteredObjects, createSavedObjectsOptions); + }); }); - test('creates saved objects', async () => { - const options = setupOptions(); - const filteredObjects = [createObject()]; - getMockFn(checkConflicts).mockResolvedValue({ - errors: [], - filteredObjects, - importIdMap: new Map().set(`id1`, { id: `newId1` }), - importIds: new Set(), + describe('with trueCopy enabled', () => { + test('regenerates object IDs', async () => { + const options = setupOptions(true); + const collectedObjects = [createObject()]; + getMockFn(collectSavedObjects).mockResolvedValue({ errors: [], collectedObjects }); + + await importSavedObjectsFromStream(options); + expect(regenerateIds).toHaveBeenCalledWith(collectedObjects); }); - getMockFn(checkOriginConflicts).mockResolvedValue({ - errors: [], - filteredObjects, - importIdMap: new Map().set(`id2`, { id: `newId2` }), + + test('does not check conflicts or check origin conflicts', async () => { + const options = setupOptions(true); + const filteredObjects = [createObject()]; + getMockFn(validateReferences).mockResolvedValue({ errors: [], filteredObjects }); + + await importSavedObjectsFromStream(options); + expect(checkConflicts).not.toHaveBeenCalled(); + expect(checkOriginConflicts).not.toHaveBeenCalled(); }); - await importSavedObjectsFromStream(options); - const importIdMap = new Map().set(`id1`, { id: `newId1` }).set(`id2`, { id: `newId2` }); - const createSavedObjectsOptions = { savedObjectsClient, importIdMap, overwrite, namespace }; - expect(createSavedObjects).toHaveBeenCalledWith(filteredObjects, createSavedObjectsOptions); + test('creates saved objects', async () => { + const options = setupOptions(true); + const filteredObjects = [createObject()]; + getMockFn(validateReferences).mockResolvedValue({ errors: [], filteredObjects }); + const importIdMap = new Map().set(`id1`, { id: `newId1` }); + getMockFn(regenerateIds).mockReturnValue({ importIdMap }); + + await importSavedObjectsFromStream(options); + const createSavedObjectsOptions = { savedObjectsClient, importIdMap, overwrite, namespace }; + expect(createSavedObjects).toHaveBeenCalledWith(filteredObjects, createSavedObjectsOptions); + }); }); }); 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 b07c7d31c276a..81c135acf8940 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.ts @@ -27,6 +27,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 } from './regenerate_ids'; /** * Import saved objects from given stream. See the {@link SavedObjectsImportOptions | options} for more @@ -38,6 +39,7 @@ export async function importSavedObjectsFromStream({ readStream, objectLimit, overwrite, + trueCopy, savedObjectsClient, typeRegistry, namespace, @@ -61,37 +63,44 @@ export async function importSavedObjectsFromStream({ ); errorAccumulator = [...errorAccumulator, ...validateReferencesResult.errors]; - // Check single-namespace objects for conflicts in this namespace, and check multi-namespace objects for conflicts across all namespaces - const checkConflictsOptions = { - savedObjectsClient, - namespace, - ignoreRegularConflicts: overwrite, - }; - const checkConflictsResult = await checkConflicts( - validateReferencesResult.filteredObjects, - checkConflictsOptions - ); - errorAccumulator = [...errorAccumulator, ...checkConflictsResult.errors]; - importIdMap = new Map([...importIdMap, ...checkConflictsResult.importIdMap]); + let objectsToCreate = validateReferencesResult.filteredObjects; + if (trueCopy) { + const regenerateIdsResult = regenerateIds(objectsFromStream); + importIdMap = regenerateIdsResult.importIdMap; + } else { + // Check single-namespace objects for conflicts in this namespace, and check multi-namespace objects for conflicts across all namespaces + const checkConflictsOptions = { + savedObjectsClient, + namespace, + ignoreRegularConflicts: overwrite, + }; + const checkConflictsResult = await checkConflicts( + validateReferencesResult.filteredObjects, + checkConflictsOptions + ); + errorAccumulator = [...errorAccumulator, ...checkConflictsResult.errors]; + importIdMap = new Map([...importIdMap, ...checkConflictsResult.importIdMap]); - // Check multi-namespace object types for origin conflicts in this namespace - const checkOriginConflictsOptions = { - savedObjectsClient, - typeRegistry, - namespace, - importIds: checkConflictsResult.importIds, - }; - const checkOriginConflictsResult = await checkOriginConflicts( - checkConflictsResult.filteredObjects, - checkOriginConflictsOptions - ); - errorAccumulator = [...errorAccumulator, ...checkOriginConflictsResult.errors]; - importIdMap = new Map([...importIdMap, ...checkOriginConflictsResult.importIdMap]); + // Check multi-namespace object types for origin conflicts in this namespace + const checkOriginConflictsOptions = { + savedObjectsClient, + typeRegistry, + namespace, + importIds: checkConflictsResult.importIds, + }; + const checkOriginConflictsResult = await checkOriginConflicts( + checkConflictsResult.filteredObjects, + checkOriginConflictsOptions + ); + errorAccumulator = [...errorAccumulator, ...checkOriginConflictsResult.errors]; + importIdMap = new Map([...importIdMap, ...checkOriginConflictsResult.importIdMap]); + objectsToCreate = checkOriginConflictsResult.filteredObjects; + } // Create objects in bulk const createSavedObjectsOptions = { savedObjectsClient, importIdMap, overwrite, namespace }; const createSavedObjectsResult = await createSavedObjects( - checkOriginConflictsResult.filteredObjects, + objectsToCreate, createSavedObjectsOptions ); errorAccumulator = [...errorAccumulator, ...createSavedObjectsResult.errors]; diff --git a/src/core/server/saved_objects/import/regenerate_ids.test.ts b/src/core/server/saved_objects/import/regenerate_ids.test.ts new file mode 100644 index 0000000000000..4a69f45dd52e5 --- /dev/null +++ b/src/core/server/saved_objects/import/regenerate_ids.test.ts @@ -0,0 +1,52 @@ +/* + * 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 { mockUuidv4 } from './__mocks__'; +import { regenerateIds } from './regenerate_ids'; +import { SavedObject } from '../types'; + +describe('#regenerateIds', () => { + const objects = ([ + { type: 'foo', id: '1' }, + { type: 'bar', id: '2' }, + { type: 'baz', id: '3' }, + ] as any) as Array>; + + test('returns expected values', () => { + expect(regenerateIds(objects)).toMatchInlineSnapshot(` + Object { + "importIdMap": Map { + "foo:1" => Object { + "id": "uuidv4", + "omitOriginId": true, + }, + "bar:2" => Object { + "id": "uuidv4", + "omitOriginId": true, + }, + "baz:3" => Object { + "id": "uuidv4", + "omitOriginId": true, + }, + }, + } + `); + expect(mockUuidv4).toHaveBeenCalledTimes(3); + }); +}); diff --git a/src/core/server/saved_objects/import/regenerate_ids.ts b/src/core/server/saved_objects/import/regenerate_ids.ts new file mode 100644 index 0000000000000..01e305785ef01 --- /dev/null +++ b/src/core/server/saved_objects/import/regenerate_ids.ts @@ -0,0 +1,33 @@ +/* + * 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 { v4 as uuidv4 } from 'uuid'; +import { SavedObject } from '../types'; + +/** + * Takes an array of saved objects and returns an importIdMap of randomly-generated new IDs. + * + * @param objects The saved objects to generate new IDs for. + */ +export const regenerateIds = (objects: Array>) => { + const importIdMap = objects.reduce((acc, object) => { + return acc.set(`${object.type}:${object.id}`, { id: uuidv4(), omitOriginId: true }); + }, new Map()); + return { importIdMap }; +}; diff --git a/src/core/server/saved_objects/import/types.ts b/src/core/server/saved_objects/import/types.ts index f2d63812a4c03..f2aeeaa7f0c76 100644 --- a/src/core/server/saved_objects/import/types.ts +++ b/src/core/server/saved_objects/import/types.ts @@ -141,7 +141,12 @@ export interface SavedObjectsImportOptions { readStream: Readable; /** The maximum number of object to import */ objectLimit: number; - /** if true, will override existing object if present */ + /** + * @deprecated + * If true, will override existing object if present. This option will be removed and permanently disabled in a future release. + * + * Note: this has no effect when used with the `trueCopy` option. + */ overwrite: boolean; /** {@link SavedObjectsClientContract | client} to use to perform the import operation */ savedObjectsClient: SavedObjectsClientContract; @@ -149,6 +154,12 @@ export interface SavedObjectsImportOptions { typeRegistry: ISavedObjectTypeRegistry; /** if specified, will import in given namespace, else will import as global object */ namespace?: string; + /** + * @deprecated + * If true, will create new copies of import objects, each with a random `id` and undefined `originId`. This option will be removed and + * permanently enabled in a future release. + */ + trueCopy: boolean; } /** diff --git a/src/core/server/saved_objects/routes/import.ts b/src/core/server/saved_objects/routes/import.ts index 6f15ca505558e..acdb00e6c12bf 100644 --- a/src/core/server/saved_objects/routes/import.ts +++ b/src/core/server/saved_objects/routes/import.ts @@ -45,16 +45,26 @@ export const registerImportRoute = (router: IRouter, config: SavedObjectConfig) }, }, validate: { - query: schema.object({ - overwrite: schema.boolean({ defaultValue: false }), - }), + query: schema.object( + { + overwrite: schema.boolean({ defaultValue: false }), + trueCopy: schema.boolean({ defaultValue: false }), + }, + { + validate: (object) => { + if (object.overwrite && object.trueCopy) { + return 'cannot use [overwrite] with [trueCopy]'; + } + }, + } + ), body: schema.object({ file: schema.stream(), }), }, }, router.handleLegacyErrors(async (context, req, res) => { - const { overwrite } = req.query; + const { overwrite, trueCopy } = req.query; const file = req.body.file as FileStream; const fileExtension = extname(file.hapi.filename).toLowerCase(); if (fileExtension !== '.ndjson') { @@ -67,6 +77,7 @@ export const registerImportRoute = (router: IRouter, config: SavedObjectConfig) readStream: createSavedObjectsStreamFromNdJson(file), objectLimit: maxImportExportSize, overwrite, + trueCopy, }); return res.ok({ body: result }); diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index dda7e96f28753..f3c89e5d905c9 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1112,7 +1112,7 @@ export interface ImageValidation { } // @public -export function importSavedObjectsFromStream({ readStream, objectLimit, overwrite, savedObjectsClient, typeRegistry, namespace, }: SavedObjectsImportOptions): Promise; +export function importSavedObjectsFromStream({ readStream, objectLimit, overwrite, trueCopy, savedObjectsClient, typeRegistry, namespace, }: SavedObjectsImportOptions): Promise; // @public (undocumented) export interface IndexSettingsDeprecationInfo { @@ -2222,9 +2222,12 @@ export interface SavedObjectsImportMissingReferencesError { export interface SavedObjectsImportOptions { namespace?: string; objectLimit: number; + // @deprecated (undocumented) overwrite: boolean; readStream: Readable; savedObjectsClient: SavedObjectsClientContract; + // @deprecated (undocumented) + trueCopy: boolean; typeRegistry: ISavedObjectTypeRegistry; } diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts index 9c8ee231a08a9..d02e21f9c222d 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts @@ -132,6 +132,7 @@ describe('copySavedObjectsToSpaces', () => { id: 'my-dashboard', }, ], + trueCopy: false, }); expect(result).toMatchInlineSnapshot(` @@ -250,6 +251,7 @@ describe('copySavedObjectsToSpaces', () => { "get": [MockFunction], "update": [MockFunction], }, + "trueCopy": false, "typeRegistry": Object { "getAllTypes": [MockFunction], "getImportableAndExportableTypes": [MockFunction], @@ -324,6 +326,7 @@ describe('copySavedObjectsToSpaces', () => { "get": [MockFunction], "update": [MockFunction], }, + "trueCopy": false, "typeRegistry": Object { "getAllTypes": [MockFunction], "getImportableAndExportableTypes": [MockFunction], @@ -396,6 +399,7 @@ describe('copySavedObjectsToSpaces', () => { id: 'my-dashboard', }, ], + trueCopy: false, } ); @@ -480,6 +484,7 @@ describe('copySavedObjectsToSpaces', () => { id: 'my-dashboard', }, ], + trueCopy: false, } ) ).rejects.toThrowErrorMatchingInlineSnapshot( diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts index 997b2239e73e9..4f2c83ba1d548 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts @@ -55,6 +55,7 @@ export function copySavedObjectsToSpacesFactory( savedObjectsClient, typeRegistry: getTypeRegistry(), readStream: objectsStream, + trueCopy: options.trueCopy, }); return { diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/types.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/types.ts index 41606126516ea..fc25a28819eb7 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/types.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/types.ts @@ -15,6 +15,7 @@ export interface CopyOptions { objects: Array<{ type: string; id: string }>; overwrite: boolean; includeReferences: boolean; + trueCopy: boolean; } export interface ResolveConflictsOptions { diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts index 374019b9a7165..619f40863fd27 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts @@ -191,6 +191,21 @@ describe('copy to space', () => { ); }); + it(`does not allow "overwrite" to be used with "trueCopy"`, async () => { + const payload = { + spaces: ['a-space'], + objects: [{ type: 'foo', id: 'bar' }], + overwrite: true, + trueCopy: true, + }; + + const { copyToSpace } = await setup(); + + expect(() => + (copyToSpace.routeValidation.body as ObjectType).validate(payload) + ).toThrowErrorMatchingInlineSnapshot(`"cannot use [overwrite] with [trueCopy]"`); + }); + it(`requires objects to be unique`, async () => { const payload = { spaces: ['a-space'], diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts index 39d4c29416956..79faaa36505f5 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts @@ -30,39 +30,49 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { tags: ['access:copySavedObjectsToSpaces'], }, validate: { - body: schema.object({ - spaces: schema.arrayOf( - schema.string({ - validate: (value) => { - if (!SPACE_ID_REGEX.test(value)) { - return `lower case, a-z, 0-9, "_", and "-" are allowed`; - } - }, - }), - { - validate: (spaceIds) => { - if (_.uniq(spaceIds).length !== spaceIds.length) { - return 'duplicate space ids are not allowed'; - } - }, - } - ), - objects: schema.arrayOf( - schema.object({ - type: schema.string(), - id: schema.string(), - }), - { - validate: (objects) => { - if (!areObjectsUnique(objects)) { - return 'duplicate objects are not allowed'; - } - }, - } - ), - includeReferences: schema.boolean({ defaultValue: false }), - overwrite: schema.boolean({ defaultValue: false }), - }), + body: schema.object( + { + spaces: schema.arrayOf( + schema.string({ + validate: (value) => { + if (!SPACE_ID_REGEX.test(value)) { + return `lower case, a-z, 0-9, "_", and "-" are allowed`; + } + }, + }), + { + validate: (spaceIds) => { + if (_.uniq(spaceIds).length !== spaceIds.length) { + return 'duplicate space ids are not allowed'; + } + }, + } + ), + objects: schema.arrayOf( + schema.object({ + type: schema.string(), + id: schema.string(), + }), + { + validate: (objects) => { + if (!areObjectsUnique(objects)) { + return 'duplicate objects are not allowed'; + } + }, + } + ), + includeReferences: schema.boolean({ defaultValue: false }), + overwrite: schema.boolean({ defaultValue: false }), + trueCopy: schema.boolean({ defaultValue: false }), + }, + { + validate: (object) => { + if (object.overwrite && object.trueCopy) { + return 'cannot use [overwrite] with [trueCopy]'; + } + }, + } + ), }, }, createLicensedRouteHandler(async (context, request, response) => { @@ -73,12 +83,19 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { getImportExportObjectLimit, request ); - const { spaces: destinationSpaceIds, objects, includeReferences, overwrite } = request.body; + const { + spaces: destinationSpaceIds, + objects, + includeReferences, + overwrite, + trueCopy, + } = request.body; const sourceSpaceId = spacesService.getSpaceId(request); const copyResponse = await copySavedObjectsToSpaces(sourceSpaceId, destinationSpaceIds, { objects, includeReferences, overwrite, + trueCopy, }); return response.ok({ body: copyResponse }); }) diff --git a/x-pack/test/saved_object_api_integration/common/suites/import.ts b/x-pack/test/saved_object_api_integration/common/suites/import.ts index f17c0ab20ec00..4173d64011ccf 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/import.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/import.ts @@ -14,6 +14,7 @@ import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/ export interface ImportTestDefinition extends TestDefinition { request: Array<{ type: string; id: string; originId?: string }>; overwrite: boolean; + trueCopy: boolean; } export type ImportTestSuite = TestSuite; export interface ImportTestCase extends TestCase { @@ -110,6 +111,9 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe // the new ID was randomly generated expect(destinationId).to.match(/^[0-9a-f-]{36}$/); } + } else if (successParam === 'trueCopy') { + // the new ID was randomly generated + expect(destinationId).to.match(/^[0-9a-f-]{36}$/); } else { expect(destinationId).to.be(undefined); } @@ -163,8 +167,9 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe const createTestDefinitions = ( testCases: ImportTestCase | ImportTestCase[], forbidden: boolean, - overwrite: boolean, - options?: { + options: { + overwrite?: boolean; + trueCopy?: boolean; spaceId?: string; singleRequest?: boolean; responseBodyOverride?: ExpectResponseBody; @@ -172,17 +177,23 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe ): ImportTestDefinition[] => { const cases = Array.isArray(testCases) ? testCases : [testCases]; const responseStatusCode = forbidden ? 403 : 200; - if (!options?.singleRequest) { + const { + overwrite = false, + trueCopy = false, + spaceId, + singleRequest, + responseBodyOverride, + } = options; + if (!singleRequest) { // if we are testing cases that should result in a forbidden response, we can do each case individually // this ensures that multiple test cases of a single type will each result in a forbidden error return cases.map((x) => ({ title: getTestTitle(x, responseStatusCode), request: [createRequest(x)], responseStatusCode, - responseBody: - options?.responseBodyOverride || - expectResponseBody(x, responseStatusCode, options?.spaceId), + responseBody: responseBodyOverride || expectResponseBody(x, responseStatusCode, spaceId), overwrite, + trueCopy, })); } // batch into a single request to save time during test execution @@ -192,9 +203,9 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe request: cases.map((x) => createRequest(x)), responseStatusCode, responseBody: - options?.responseBodyOverride || - expectResponseBody(cases, responseStatusCode, options?.spaceId), + responseBodyOverride || expectResponseBody(cases, responseStatusCode, spaceId), overwrite, + trueCopy, }, ]; }; @@ -216,7 +227,7 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe const requestBody = test.request .map((obj) => JSON.stringify({ ...obj, ...attrs })) .join('\n'); - const query = test.overwrite ? '?overwrite=true' : ''; + const query = test.overwrite ? '?overwrite=true' : test.trueCopy ? '?trueCopy=true' : ''; await supertest .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_import${query}`) .auth(user?.username, user?.password) diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts index cf26df17f2c32..06740dc11265c 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts @@ -27,6 +27,16 @@ const ambiguousConflict = (suffix: string) => ({ fail409Param: `ambiguous_conflict_${suffix}`, }); +const createTrueCopyTestCases = () => { + // for each outcome, if failure !== undefined then we expect to receive + // an error; otherwise, we expect to receive a success result + const cases = Object.entries(CASES).filter(([key]) => key !== 'HIDDEN'); + const importable = cases.map(([, val]) => ({ ...val, successParam: 'trueCopy' })); + const nonImportable = [{ ...CASES.HIDDEN, ...fail400() }]; + const all = [...importable, ...nonImportable]; + return { importable, nonImportable, all }; +}; + const createTestCases = (overwrite: boolean, spaceId: string) => { // for each permitted (non-403) outcome, if failure !== undefined then we expect // to receive an error; otherwise, we expect to receive a success result @@ -86,62 +96,88 @@ export default function ({ getService }: FtrProviderContext) { esArchiver, supertest ); - const createTests = (overwrite: boolean, spaceId: string) => { + const createTests = (overwrite: boolean, trueCopy: boolean, spaceId: string) => { + const singleRequest = true; + + if (trueCopy) { + const { importable, nonImportable, all } = createTrueCopyTestCases(); + return { + unauthorized: [ + createTestDefinitions(importable, true, { trueCopy, spaceId }), + createTestDefinitions(nonImportable, false, { trueCopy, spaceId, singleRequest }), + createTestDefinitions(all, true, { + trueCopy, + spaceId, + singleRequest, + responseBodyOverride: expectForbidden('bulk_create')([ + 'dashboard', + 'globaltype', + 'isolatedtype', + 'sharedtype', + ]), + }), + ].flat(), + authorized: createTestDefinitions(all, false, { trueCopy, spaceId, singleRequest }), + }; + } + const { group1Importable, group1NonImportable, group1All, group2, group3 } = createTestCases( overwrite, spaceId ); - // use singleRequest to reduce execution time and/or test combined cases return { unauthorized: [ - createTestDefinitions(group1Importable, true, overwrite, { spaceId }), - createTestDefinitions(group1NonImportable, false, overwrite, { + createTestDefinitions(group1Importable, true, { overwrite, spaceId }), + createTestDefinitions(group1NonImportable, false, { overwrite, spaceId, singleRequest }), + createTestDefinitions(group1All, true, { + overwrite, spaceId, - singleRequest: true, - }), - createTestDefinitions(group1All, true, overwrite, { - spaceId, - singleRequest: true, + singleRequest, responseBodyOverride: expectForbidden('bulk_create')([ 'dashboard', 'globaltype', 'isolatedtype', ]), }), - createTestDefinitions(group2, true, overwrite, { spaceId, singleRequest: true }), - createTestDefinitions(group3, true, overwrite, { spaceId, singleRequest: true }), + createTestDefinitions(group2, true, { overwrite, spaceId, singleRequest }), + createTestDefinitions(group3, true, { overwrite, spaceId, singleRequest }), ].flat(), authorized: [ - createTestDefinitions(group1All, false, overwrite, { spaceId, singleRequest: true }), - createTestDefinitions(group2, false, overwrite, { spaceId, singleRequest: true }), - createTestDefinitions(group3, false, overwrite, { spaceId, singleRequest: true }), + createTestDefinitions(group1All, false, { overwrite, spaceId, singleRequest }), + createTestDefinitions(group2, false, { overwrite, spaceId, singleRequest }), + createTestDefinitions(group3, false, { overwrite, spaceId, singleRequest }), ].flat(), }; }; describe('_import', () => { - getTestScenarios([false, true]).securityAndSpaces.forEach( - ({ spaceId, users, modifier: overwrite }) => { - const suffix = ` within the ${spaceId} space${overwrite ? ' with overwrite enabled' : ''}`; - const { unauthorized, authorized } = createTests(overwrite!, spaceId); - const _addTests = (user: TestUser, tests: ImportTestDefinition[]) => { - addTests(`${user.description}${suffix}`, { user, spaceId, tests }); - }; + getTestScenarios([ + [false, false], + [false, true], + [true, false], + ]).securityAndSpaces.forEach(({ spaceId, users, modifier }) => { + const [overwrite, trueCopy] = modifier!; + const suffix = ` within the ${spaceId} space${ + overwrite ? ' with overwrite enabled' : trueCopy ? ' with trueCopy enabled' : '' + }`; + const { unauthorized, authorized } = createTests(overwrite, trueCopy, spaceId); + const _addTests = (user: TestUser, tests: ImportTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; - [ - users.noAccess, - users.legacyAll, - users.dualRead, - users.readGlobally, - users.readAtSpace, - users.allAtOtherSpace, - ].forEach((user) => { - _addTests(user, unauthorized); - }); - [users.dualAll, users.allGlobally, users.allAtSpace, users.superuser].forEach((user) => { - _addTests(user, authorized); - }); - } - ); + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.readAtSpace, + users.allAtOtherSpace, + ].forEach((user) => { + _addTests(user, unauthorized); + }); + [users.dualAll, users.allGlobally, users.allAtSpace, users.superuser].forEach((user) => { + _addTests(user, authorized); + }); + }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/import.ts b/x-pack/test/saved_object_api_integration/security_only/apis/import.ts index d04c0c43412eb..92bf9ccef9a11 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/import.ts @@ -21,6 +21,16 @@ const ambiguousConflict = (suffix: string) => ({ fail409Param: `ambiguous_conflict_${suffix}`, }); +const createTrueCopyTestCases = () => { + // for each outcome, if failure !== undefined then we expect to receive + // an error; otherwise, we expect to receive a success result + const cases = Object.entries(CASES).filter(([key]) => key !== 'HIDDEN'); + const importable = cases.map(([, val]) => ({ ...val, successParam: 'trueCopy' })); + const nonImportable = [{ ...CASES.HIDDEN, ...fail400() }]; + const all = [...importable, ...nonImportable]; + return { importable, nonImportable, all }; +}; + const createTestCases = (overwrite: boolean) => { // for each permitted (non-403) outcome, if failure !== undefined then we expect // to receive an error; otherwise, we expect to receive a success result @@ -65,38 +75,71 @@ export default function ({ getService }: FtrProviderContext) { esArchiver, supertest ); - const createTests = (overwrite: boolean) => { + const createTests = (overwrite: boolean, trueCopy: boolean) => { + // use singleRequest to reduce execution time and/or test combined cases + const singleRequest = true; + + if (trueCopy) { + const { importable, nonImportable, all } = createTrueCopyTestCases(); + return { + unauthorized: [ + createTestDefinitions(importable, true, { trueCopy }), + createTestDefinitions(nonImportable, false, { trueCopy, singleRequest }), + createTestDefinitions(all, true, { + trueCopy, + singleRequest, + responseBodyOverride: expectForbidden('bulk_create')([ + 'dashboard', + 'globaltype', + 'isolatedtype', + 'sharedtype', + ]), + }), + ].flat(), + authorized: createTestDefinitions(all, false, { trueCopy, singleRequest }), + }; + } + const { group1Importable, group1NonImportable, group1All, group2, group3 } = createTestCases( overwrite ); - // use singleRequest to reduce execution time and/or test combined cases return { unauthorized: [ - createTestDefinitions(group1Importable, true, overwrite), - createTestDefinitions(group1NonImportable, false, overwrite, { singleRequest: true }), - createTestDefinitions(group1All, true, overwrite, { - singleRequest: true, + createTestDefinitions(group1Importable, true, { overwrite }), + createTestDefinitions(group1NonImportable, false, { overwrite, singleRequest }), + createTestDefinitions(group1All, true, { + overwrite, + singleRequest, responseBodyOverride: expectForbidden('bulk_create')([ 'dashboard', 'globaltype', 'isolatedtype', ]), }), - createTestDefinitions(group2, true, overwrite, { singleRequest: true }), - createTestDefinitions(group3, true, overwrite, { singleRequest: true }), + createTestDefinitions(group2, true, { overwrite, singleRequest }), + createTestDefinitions(group3, true, { overwrite, singleRequest }), ].flat(), authorized: [ - createTestDefinitions(group1All, false, overwrite, { singleRequest: true }), - createTestDefinitions(group2, false, overwrite, { singleRequest: true }), - createTestDefinitions(group3, false, overwrite, { singleRequest: true }), + createTestDefinitions(group1All, false, { overwrite, singleRequest }), + createTestDefinitions(group2, false, { overwrite, singleRequest }), + createTestDefinitions(group3, false, { overwrite, singleRequest }), ].flat(), }; }; describe('_import', () => { - getTestScenarios([false, true]).security.forEach(({ users, modifier: overwrite }) => { - const suffix = overwrite ? ' with overwrite enabled' : ''; - const { unauthorized, authorized } = createTests(overwrite!); + getTestScenarios([ + [false, false], + [false, true], + [true, false], + ]).security.forEach(({ users, modifier }) => { + const [overwrite, trueCopy] = modifier!; + const suffix = overwrite + ? ' with overwrite enabled' + : trueCopy + ? ' with trueCopy enabled' + : ''; + const { unauthorized, authorized } = createTests(overwrite, trueCopy); const _addTests = (user: TestUser, tests: ImportTestDefinition[]) => { addTests(`${user.description}${suffix}`, { user, tests }); }; diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts index 91edd1088129a..1b38e68019feb 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts @@ -22,6 +22,16 @@ const ambiguousConflict = (suffix: string) => ({ fail409Param: `ambiguous_conflict_${suffix}`, }); +const createTrueCopyTestCases = () => { + // for each outcome, if failure !== undefined then we expect to receive + // an error; otherwise, we expect to receive a success result + const cases = Object.entries(CASES).filter(([key]) => key !== 'HIDDEN'); + return [ + ...cases.map(([, val]) => ({ ...val, successParam: 'trueCopy' })), + { ...CASES.HIDDEN, ...fail400() }, + ]; +}; + const createTestCases = (overwrite: boolean, spaceId: string) => { // for each outcome, if failure !== undefined then we expect to receive // an error; otherwise, we expect to receive a success result @@ -74,18 +84,33 @@ export default function ({ getService }: FtrProviderContext) { const es = getService('legacyEs'); const { addTests, createTestDefinitions } = importTestSuiteFactory(es, esArchiver, supertest); - const createTests = (overwrite: boolean, spaceId: string) => { + const createTests = (overwrite: boolean, trueCopy: boolean, spaceId: string) => { + const singleRequest = true; + if (trueCopy) { + const cases = createTrueCopyTestCases(); + return createTestDefinitions(cases, false, { trueCopy, spaceId, singleRequest }); + } + const { group1, group2 } = createTestCases(overwrite, spaceId); return [ - createTestDefinitions(group1, false, overwrite, { spaceId, singleRequest: true }), - createTestDefinitions(group2, false, overwrite, { spaceId, singleRequest: true }), + createTestDefinitions(group1, false, { overwrite, spaceId, singleRequest }), + createTestDefinitions(group2, false, { overwrite, spaceId, singleRequest }), ].flat(); }; describe('_import', () => { - getTestScenarios([false, true]).spaces.forEach(({ spaceId, modifier: overwrite }) => { - const suffix = overwrite ? ' with overwrite enabled' : ''; - const tests = createTests(overwrite!, spaceId); + getTestScenarios([ + [false, false], + [false, true], + [true, false], + ]).spaces.forEach(({ spaceId, modifier }) => { + const [overwrite, trueCopy] = modifier!; + const suffix = overwrite + ? ' with overwrite enabled' + : trueCopy + ? ' with trueCopy enabled' + : ''; + const tests = createTests(overwrite, trueCopy, spaceId); addTests(`within the ${spaceId} space${suffix}`, { spaceId, tests }); }); }); From 92896744322e178587e9400506c6ea8a234b8a47 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Sat, 27 Jun 2020 15:05:02 -0400 Subject: [PATCH 25/55] Fix `copyToSpace` integration test A recent commit changed the authZ error that can be encountered. --- .../common/suites/copy_to_space.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts index fd0bb9a967505..c1656fc6c8d3a 100644 --- a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts @@ -352,13 +352,12 @@ export function copyToSpaceTestSuiteFactory( const getResult = (response: TestResponse) => (response.body as CopyResponse).space_2; const expectForbiddenResponse = (response: TestResponse) => { - const action = outcome === 'unauthorizedRead' ? 'find' : 'bulk_create'; expect(response.body).to.eql({ space_2: { success: false, successCount: 0, errors: [ - { statusCode: 403, error: 'Forbidden', message: `Unable to ${action} sharedtype` }, + { statusCode: 403, error: 'Forbidden', message: `Unable to bulk_create sharedtype` }, ], }, }); @@ -458,9 +457,7 @@ export function copyToSpaceTestSuiteFactory( objects: [{ type, id: ambiguousConflictId }], statusCode, response: async (response: TestResponse) => { - if (outcome === 'authorized' || outcome === 'unauthorizedWrite') { - // when an ambiguous conflict is encountered, the import function never actually attempts to create the object -- - // because of that, a consumer who is authorized to read (but not write) will see the same response as a user who is authorized + if (outcome === 'authorized') { const { success, successCount, successResults, errors } = getResult(response); const updatedAt = '2017-09-21T18:59:16.270Z'; const destinations = [ @@ -482,7 +479,7 @@ export function copyToSpaceTestSuiteFactory( } else if (outcome === 'noAccess') { expectNotFoundResponse(response); } else { - // unauthorized read + // unauthorized read/write expectForbiddenResponse(response); } }, From 6496c256367d5202a6a94ec2a87523ea7c8ba573 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Sun, 28 Jun 2020 12:30:07 -0400 Subject: [PATCH 26/55] Add "true copy" mode for resolving import errors Initially I didn't think this was necessary, but we need this flag so we know whether or not to omit the originId on newly imported objects. --- .../core/server/kibana-plugin-core-server.md | 2 +- ...-server.resolvesavedobjectsimporterrors.md | 4 +- ....savedobjectsresolveimporterrorsoptions.md | 1 + ...ectsresolveimporterrorsoptions.truecopy.md | 16 +++ .../import/check_conflicts.test.ts | 106 +++++++++--------- .../saved_objects/import/check_conflicts.ts | 5 +- .../import/check_origin_conflicts.test.ts | 21 +++- .../import/check_origin_conflicts.ts | 7 +- .../import/resolve_import_errors.test.ts | 15 ++- .../import/resolve_import_errors.ts | 9 +- src/core/server/saved_objects/import/types.ts | 6 + .../routes/resolve_import_errors.ts | 4 + src/core/server/server.api.md | 4 +- .../resolve_copy_conflicts.test.ts | 5 + .../copy_to_spaces/resolve_copy_conflicts.ts | 7 +- .../spaces/server/lib/copy_to_spaces/types.ts | 1 + .../routes/api/external/copy_to_space.ts | 4 +- 17 files changed, 148 insertions(+), 69 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.truecopy.md diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 24534b9de57cb..61cbc224e1c97 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -48,7 +48,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [importSavedObjectsFromStream({ readStream, objectLimit, overwrite, trueCopy, savedObjectsClient, typeRegistry, namespace, })](./kibana-plugin-core-server.importsavedobjectsfromstream.md) | Import saved objects from given stream. See the [options](./kibana-plugin-core-server.savedobjectsimportoptions.md) for more detailed information. | | [isRelativeUrl(candidatePath)](./kibana-plugin-core-server.isrelativeurl.md) | Determine if a url is relative. Any url including a protocol, hostname, or port is not considered relative. This means that absolute \*paths\* are considered to be relative \*urls\* | | [modifyUrl(url, urlModifier)](./kibana-plugin-core-server.modifyurl.md) | Takes a URL and a function that takes the meaningful parts of the URL as a key-value object, modifies some or all of the parts, and returns the modified parts formatted again as a url.Url Parts sent: - protocol - slashes (does the url have the //) - auth - hostname (just the name of the host, no port or auth information) - port - pathname (the path after the hostname, no query or hash, starts with a slash if there was a path) - query (always an object, even when no query on original url) - hashWhy? - The default url library in node produces several conflicting properties on the "parsed" output. Modifying any of these might lead to the modifications being ignored (depending on which property was modified) - It's not always clear whether to use path/pathname, host/hostname, so this tries to add helpful constraints | -| [resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, typeRegistry, namespace, })](./kibana-plugin-core-server.resolvesavedobjectsimporterrors.md) | Resolve and return saved object import errors. See the [options](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md) for more detailed informations. | +| [resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, typeRegistry, namespace, trueCopy, })](./kibana-plugin-core-server.resolvesavedobjectsimporterrors.md) | Resolve and return saved object import errors. See the [options](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md) for more detailed informations. | ## Interfaces diff --git a/docs/development/core/server/kibana-plugin-core-server.resolvesavedobjectsimporterrors.md b/docs/development/core/server/kibana-plugin-core-server.resolvesavedobjectsimporterrors.md index 6ee273fff7059..9be4db86a328d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.resolvesavedobjectsimporterrors.md +++ b/docs/development/core/server/kibana-plugin-core-server.resolvesavedobjectsimporterrors.md @@ -9,14 +9,14 @@ Resolve and return saved object import errors. See the [options](./kibana-plugin Signature: ```typescript -export declare function resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, typeRegistry, namespace, }: SavedObjectsResolveImportErrorsOptions): Promise; +export declare function resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, typeRegistry, namespace, trueCopy, }: SavedObjectsResolveImportErrorsOptions): Promise; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| { readStream, objectLimit, retries, savedObjectsClient, typeRegistry, namespace, } | SavedObjectsResolveImportErrorsOptions | | +| { readStream, objectLimit, retries, savedObjectsClient, typeRegistry, namespace, trueCopy, } | SavedObjectsResolveImportErrorsOptions | | Returns: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md index 4a01dd3161b37..8c3f2d446ffff 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md @@ -21,5 +21,6 @@ export interface SavedObjectsResolveImportErrorsOptions | [readStream](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.readstream.md) | Readable | The stream of [saved objects](./kibana-plugin-core-server.savedobject.md) to resolve errors from | | [retries](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.retries.md) | SavedObjectsImportRetry[] | saved object import references to retry | | [savedObjectsClient](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.savedobjectsclient.md) | SavedObjectsClientContract | client to use to perform the import operation | +| [trueCopy](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.truecopy.md) | boolean | | | [typeRegistry](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.typeregistry.md) | ISavedObjectTypeRegistry | The registry of all known saved object types | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.truecopy.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.truecopy.md new file mode 100644 index 0000000000000..394e332b208e6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.truecopy.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsResolveImportErrorsOptions](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md) > [trueCopy](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.truecopy.md) + +## SavedObjectsResolveImportErrorsOptions.trueCopy property + +> Warning: This API is now obsolete. +> +> If true, will create new copies of import objects, each with a random `id` and undefined `originId`. This option will be removed and permanently enabled in a future release. +> + +Signature: + +```typescript +trueCopy: boolean; +``` diff --git a/src/core/server/saved_objects/import/check_conflicts.test.ts b/src/core/server/saved_objects/import/check_conflicts.test.ts index 9fdd50900fcd4..d2daab87aa0df 100644 --- a/src/core/server/saved_objects/import/check_conflicts.test.ts +++ b/src/core/server/saved_objects/import/check_conflicts.test.ts @@ -37,6 +37,22 @@ const createObject = (type: string, id: string): SavedObjectType => ({ references: (Symbol() as unknown) as SavedObjectReference[], }); +const getResultMock = { + conflict: (type: string, id: string) => { + const error = SavedObjectsErrorHelpers.createConflictError(type, id).output.payload; + return { type, id, error }; + }, + unresolvableConflict: (type: string, id: string) => { + const conflictMock = getResultMock.conflict(type, id); + const metadata = { isNotOverwritable: true }; + return { ...conflictMock, error: { ...conflictMock.error, metadata } }; + }, + invalidType: (type: string, id: string) => { + const error = SavedObjectsErrorHelpers.createUnsupportedTypeError(type).output.payload; + return { type, id, error }; + }, +}; + /** * Create a variety of different objects to exercise different import / result scenarios */ @@ -44,6 +60,10 @@ const obj1 = createObject('type-1', 'id-1'); // -> success const obj2 = createObject('type-2', 'id-2'); // -> conflict const obj3 = createObject('type-3', 'id-3'); // -> unresolvable conflict const obj4 = createObject('type-4', 'id-4'); // -> invalid type +const objects = [obj1, obj2, obj3, obj4]; +const obj2Error = getResultMock.conflict(obj2.type, obj2.id); +const obj3Error = getResultMock.unresolvableConflict(obj3.type, obj3.id); +const obj4Error = getResultMock.invalidType(obj4.type, obj4.id); describe('#checkConflicts', () => { let savedObjectsClient: jest.Mocked; @@ -57,30 +77,20 @@ describe('#checkConflicts', () => { options: { namespace?: string; ignoreRegularConflicts?: boolean; + trueCopy?: boolean; } = {} ): CheckConflictsOptions => { - const { namespace, ignoreRegularConflicts } = options; + const { namespace, ignoreRegularConflicts, trueCopy } = options; savedObjectsClient = savedObjectsClientMock.create(); socCheckConflicts = savedObjectsClient.checkConflicts; socCheckConflicts.mockResolvedValue({ errors: [] }); // by default, mock to empty results - return { savedObjectsClient, namespace, ignoreRegularConflicts }; + return { savedObjectsClient, namespace, ignoreRegularConflicts, trueCopy }; }; - const getResultMock = { - conflict: (type: string, id: string) => { - const error = SavedObjectsErrorHelpers.createConflictError(type, id).output.payload; - return { type, id, error }; - }, - unresolvableConflict: (type: string, id: string) => { - const conflictMock = getResultMock.conflict(type, id); - const metadata = { isNotOverwritable: true }; - return { ...conflictMock, error: { ...conflictMock.error, metadata } }; - }, - invalidType: (type: string, id: string) => { - const error = SavedObjectsErrorHelpers.createUnsupportedTypeError(type).output.payload; - return { type, id, error }; - }, - }; + beforeEach(() => { + mockUuidv4.mockReset(); + mockUuidv4.mockReturnValueOnce(`new-object-id`); + }); it('exits early if there are no objects to check', async () => { const namespace = 'foo-namespace'; @@ -98,7 +108,6 @@ describe('#checkConflicts', () => { it('calls checkConflicts with expected inputs', async () => { const namespace = 'foo-namespace'; - const objects = [obj1, obj2, obj3, obj4]; const options = setupOptions({ namespace }); await checkConflicts(objects, options); @@ -108,13 +117,8 @@ describe('#checkConflicts', () => { it('returns expected result', async () => { const namespace = 'foo-namespace'; - const objects = [obj1, obj2, obj3, obj4]; const options = setupOptions({ namespace }); - const obj2Error = getResultMock.conflict(obj2.type, obj2.id); - const obj3Error = getResultMock.unresolvableConflict(obj3.type, obj3.id); - const obj4Error = getResultMock.invalidType(obj4.type, obj4.id); socCheckConflicts.mockResolvedValue({ errors: [obj2Error, obj3Error, obj4Error] }); - mockUuidv4.mockReturnValueOnce(`new-object-id`); const checkConflictsResult = await checkConflicts(objects, options); expect(checkConflictsResult).toEqual({ @@ -128,42 +132,42 @@ describe('#checkConflicts', () => { }, ], importIdMap: new Map([[`${obj3.type}:${obj3.id}`, { id: `new-object-id` }]]), - importIds: new Set([ - `${obj1.type}:${obj1.id}`, - `${obj2.type}:${obj2.id}`, - `${obj3.type}:${obj3.id}`, - `${obj4.type}:${obj4.id}`, - ]), + importIds: new Set(objects.map(({ type, id }) => `${type}:${id}`)), }); }); it('does not return errors for regular conflicts when ignoreRegularConflicts=true', async () => { const namespace = 'foo-namespace'; - const objects = [obj1, obj2, obj3, obj4]; const options = setupOptions({ namespace, ignoreRegularConflicts: true }); - const obj2Error = getResultMock.conflict(obj2.type, obj2.id); - const obj3Error = getResultMock.unresolvableConflict(obj3.type, obj3.id); - const obj4Error = getResultMock.invalidType(obj4.type, obj4.id); socCheckConflicts.mockResolvedValue({ errors: [obj2Error, obj3Error, obj4Error] }); - mockUuidv4.mockReturnValueOnce(`new-object-id`); const checkConflictsResult = await checkConflicts(objects, options); - expect(checkConflictsResult).toEqual({ - filteredObjects: [obj1, obj2, obj3], - errors: [ - { - ...obj4Error, - title: obj4.attributes.title, - error: { ...obj4Error.error, type: 'unknown' }, - }, - ], - importIdMap: new Map([[`${obj3.type}:${obj3.id}`, { id: `new-object-id` }]]), - importIds: new Set([ - `${obj1.type}:${obj1.id}`, - `${obj2.type}:${obj2.id}`, - `${obj3.type}:${obj3.id}`, - `${obj4.type}:${obj4.id}`, - ]), - }); + expect(checkConflictsResult).toEqual( + expect.objectContaining({ + filteredObjects: [obj1, obj2, obj3], + errors: [ + { + ...obj4Error, + title: obj4.attributes.title, + error: { ...obj4Error.error, type: 'unknown' }, + }, + ], + }) + ); + }); + + it('adds `omitOriginId` field to `importIdMap` entries when trueCopy=true', async () => { + const namespace = 'foo-namespace'; + const options = setupOptions({ namespace, trueCopy: true }); + socCheckConflicts.mockResolvedValue({ errors: [obj2Error, obj3Error, obj4Error] }); + + const checkConflictsResult = await checkConflicts(objects, options); + expect(checkConflictsResult).toEqual( + expect.objectContaining({ + importIdMap: new Map([ + [`${obj3.type}:${obj3.id}`, { id: `new-object-id`, omitOriginId: true }], + ]), + }) + ); }); }); diff --git a/src/core/server/saved_objects/import/check_conflicts.ts b/src/core/server/saved_objects/import/check_conflicts.ts index 96bbf9be63ceb..907b8dcc6d77c 100644 --- a/src/core/server/saved_objects/import/check_conflicts.ts +++ b/src/core/server/saved_objects/import/check_conflicts.ts @@ -29,6 +29,7 @@ interface CheckConflictsOptions { savedObjectsClient: SavedObjectsClientContract; namespace?: string; ignoreRegularConflicts?: boolean; + trueCopy?: boolean; } const isUnresolvableConflict = (error: SavedObjectError) => @@ -48,7 +49,7 @@ export async function checkConflicts( return { filteredObjects, errors, importIdMap, importIds }; } - const { savedObjectsClient, namespace, ignoreRegularConflicts } = options; + const { savedObjectsClient, namespace, ignoreRegularConflicts, trueCopy } = options; const checkConflictsResult = await savedObjectsClient.checkConflicts(objects, { namespace }); const errorMap = checkConflictsResult.errors.reduce( (acc, { type, id, error }) => acc.set(`${type}:${id}`, error), @@ -68,7 +69,7 @@ export async function checkConflicts( // with a "multi-namespace" type is exported from one namespace and imported to another, it does not result in an error, but instead a // new object is created. const destinationId = uuidv4(); - importIdMap.set(`${type}:${id}`, { id: destinationId }); + importIdMap.set(`${type}:${id}`, { id: destinationId, omitOriginId: trueCopy }); filteredObjects.push(object); } else if (errorObj && errorObj.statusCode !== 409) { errors.push({ type, id, title, error: { ...errorObj, type: 'unknown' } }); diff --git a/src/core/server/saved_objects/import/check_origin_conflicts.test.ts b/src/core/server/saved_objects/import/check_origin_conflicts.test.ts index d872b3ca35549..620ddd552f603 100644 --- a/src/core/server/saved_objects/import/check_origin_conflicts.test.ts +++ b/src/core/server/saved_objects/import/check_origin_conflicts.test.ts @@ -446,10 +446,13 @@ describe('#checkOriginConflicts', () => { describe('#getImportIdMapForRetries', () => { let typeRegistry: jest.Mocked; - const setupOptions = (retries: SavedObjectsImportRetry[]): GetImportIdMapForRetriesOptions => { + const setupOptions = ( + retries: SavedObjectsImportRetry[], + trueCopy: boolean = false + ): GetImportIdMapForRetriesOptions => { typeRegistry = typeRegistryMock.create(); typeRegistry.isMultiNamespace.mockImplementation((type) => type === MULTI_NS_TYPE); - return { typeRegistry, retries }; + return { typeRegistry, retries, trueCopy }; }; const createOverwriteRetry = ( @@ -492,7 +495,19 @@ describe('#getImportIdMapForRetries', () => { const checkOriginConflictsResult = await getImportIdMapForRetries(objects, options); expect(checkOriginConflictsResult).toEqual( - new Map([[`${obj6.type}:${obj6.id}`, { id: 'id-Y' }]]) + new Map([[`${obj6.type}:${obj6.id}`, { id: 'id-Y', omitOriginId: false }]]) + ); + }); + + test('omits origin ID in `importIdMap` entries when trueCopy=true', async () => { + const obj6 = createObject(MULTI_NS_TYPE, 'id-6'); + const objects = [obj6]; + const retries = [createOverwriteRetry(obj6, 'id-Y')]; + const options = setupOptions(retries, true); + + const checkOriginConflictsResult = await getImportIdMapForRetries(objects, options); + expect(checkOriginConflictsResult).toEqual( + new Map([[`${obj6.type}:${obj6.id}`, { id: 'id-Y', omitOriginId: true }]]) ); }); }); diff --git a/src/core/server/saved_objects/import/check_origin_conflicts.ts b/src/core/server/saved_objects/import/check_origin_conflicts.ts index c5249ff7e797f..2c0e549db0867 100644 --- a/src/core/server/saved_objects/import/check_origin_conflicts.ts +++ b/src/core/server/saved_objects/import/check_origin_conflicts.ts @@ -36,6 +36,7 @@ interface CheckOriginConflictsOptions { interface GetImportIdMapForRetriesOptions { typeRegistry: ISavedObjectTypeRegistry; retries: SavedObjectsImportRetry[]; + trueCopy: boolean; } interface InexactMatch { @@ -209,13 +210,13 @@ export function getImportIdMapForRetries( objects: Array>, options: GetImportIdMapForRetriesOptions ) { - const { typeRegistry, retries } = options; + const { typeRegistry, retries, trueCopy } = options; const retryMap = retries.reduce( (acc, cur) => acc.set(`${cur.type}:${cur.id}`, cur), new Map() ); - const importIdMap = new Map(); + const importIdMap = new Map(); objects.forEach(({ type, id }) => { const retry = retryMap.get(`${type}:${id}`); @@ -224,7 +225,7 @@ export function getImportIdMapForRetries( } const { overwrite, idToOverwrite } = retry; if (overwrite && idToOverwrite && idToOverwrite !== id && typeRegistry.isMultiNamespace(type)) { - importIdMap.set(`${type}:${id}`, { id: idToOverwrite }); + importIdMap.set(`${type}:${id}`, { id: idToOverwrite, omitOriginId: trueCopy }); } }); 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 1fab9ac6a8bb5..e77c733885840 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 @@ -79,6 +79,7 @@ describe('#importSavedObjectsFromStream', () => { let savedObjectsClient: jest.Mocked; let typeRegistry: jest.Mocked; const namespace = 'some-namespace'; + const trueCopy = false; const setupOptions = ( retries: SavedObjectsImportRetry[] = [] @@ -86,7 +87,16 @@ describe('#importSavedObjectsFromStream', () => { readStream = new Readable(); savedObjectsClient = savedObjectsClientMock.create(); typeRegistry = typeRegistryMock.create(); - return { readStream, objectLimit, retries, savedObjectsClient, typeRegistry, namespace }; + return { + readStream, + objectLimit, + retries, + savedObjectsClient, + typeRegistry, + // namespace and trueCopy don't matter, as they don't change the logic in this module, they just get passed to sub-module methods + namespace, + trueCopy, + }; }; const createRetry = (options?: { @@ -188,6 +198,7 @@ describe('#importSavedObjectsFromStream', () => { savedObjectsClient, namespace, ignoreRegularConflicts: true, + trueCopy, }; expect(checkConflicts).toHaveBeenCalledWith(filteredObjects, checkConflictsOptions); }); @@ -204,7 +215,7 @@ describe('#importSavedObjectsFromStream', () => { }); await resolveSavedObjectsImportErrors(options); - const opts = { typeRegistry, retries }; + const opts = { typeRegistry, retries, trueCopy }; expect(getImportIdMapForRetries).toHaveBeenCalledWith(filteredObjects, opts); }); 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 36b2ea53d6207..b3c693a42af5d 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.ts @@ -44,6 +44,7 @@ export async function resolveSavedObjectsImportErrors({ savedObjectsClient, typeRegistry, namespace, + trueCopy, }: SavedObjectsResolveImportErrorsOptions): Promise { // throw a BadRequest error if we see invalid retries validateRetries(retries); @@ -98,7 +99,12 @@ export async function resolveSavedObjectsImportErrors({ errorAccumulator = [...errorAccumulator, ...validateReferencesResult.errors]; // Check single-namespace objects for conflicts in this namespace, and check multi-namespace objects for conflicts across all namespaces - const checkConflictsOptions = { savedObjectsClient, namespace, ignoreRegularConflicts: true }; + const checkConflictsOptions = { + savedObjectsClient, + namespace, + ignoreRegularConflicts: true, + trueCopy, + }; const checkConflictsResult = await checkConflicts( validateReferencesResult.filteredObjects, checkConflictsOptions @@ -110,6 +116,7 @@ export async function resolveSavedObjectsImportErrors({ const importIdMapForRetries = getImportIdMapForRetries(checkConflictsResult.filteredObjects, { typeRegistry, retries, + trueCopy, }); importIdMap = new Map([...importIdMap, ...importIdMapForRetries]); diff --git a/src/core/server/saved_objects/import/types.ts b/src/core/server/saved_objects/import/types.ts index f2aeeaa7f0c76..2762f8333e437 100644 --- a/src/core/server/saved_objects/import/types.ts +++ b/src/core/server/saved_objects/import/types.ts @@ -179,4 +179,10 @@ export interface SavedObjectsResolveImportErrorsOptions { retries: SavedObjectsImportRetry[]; /** if specified, will import in given namespace */ namespace?: string; + /** + * @deprecated + * If true, will create new copies of import objects, each with a random `id` and undefined `originId`. This option will be removed and + * permanently enabled in a future release. + */ + trueCopy: boolean; } diff --git a/src/core/server/saved_objects/routes/resolve_import_errors.ts b/src/core/server/saved_objects/routes/resolve_import_errors.ts index 6e505cb862689..74932866e39e9 100644 --- a/src/core/server/saved_objects/routes/resolve_import_errors.ts +++ b/src/core/server/saved_objects/routes/resolve_import_errors.ts @@ -45,6 +45,9 @@ export const registerResolveImportErrorsRoute = (router: IRouter, config: SavedO }, }, validate: { + query: schema.object({ + trueCopy: schema.boolean({ defaultValue: false }), + }), body: schema.object({ file: schema.stream(), retries: schema.arrayOf( @@ -88,6 +91,7 @@ export const registerResolveImportErrorsRoute = (router: IRouter, config: SavedO readStream: createSavedObjectsStreamFromNdJson(file), retries: req.body.retries, objectLimit: maxImportExportSize, + trueCopy: req.query.trueCopy, }); return res.ok({ body: result }); diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index f3c89e5d905c9..51e02741214ad 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1698,7 +1698,7 @@ export type RequestHandlerContextProvider(handler: RequestHandler) => RequestHandler; // @public -export function resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, typeRegistry, namespace, }: SavedObjectsResolveImportErrorsOptions): Promise; +export function resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, typeRegistry, namespace, trueCopy, }: SavedObjectsResolveImportErrorsOptions): Promise; // @public export type ResponseError = string | Error | { @@ -2399,6 +2399,8 @@ export interface SavedObjectsResolveImportErrorsOptions { readStream: Readable; retries: SavedObjectsImportRetry[]; savedObjectsClient: SavedObjectsClientContract; + // @deprecated (undocumented) + trueCopy: boolean; typeRegistry: ISavedObjectTypeRegistry; } diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts index 1b9d42a3af4e8..0778c379c53b9 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts @@ -148,6 +148,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { }, ], }, + trueCopy: false, }); expect(result).toMatchInlineSnapshot(` @@ -273,6 +274,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { "get": [MockFunction], "update": [MockFunction], }, + "trueCopy": false, "typeRegistry": Object { "getAllTypes": [MockFunction], "getImportableAndExportableTypes": [MockFunction], @@ -354,6 +356,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { "get": [MockFunction], "update": [MockFunction], }, + "trueCopy": false, "typeRegistry": Object { "getAllTypes": [MockFunction], "getImportableAndExportableTypes": [MockFunction], @@ -445,6 +448,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { }, ], }, + trueCopy: false, }); expect(result).toMatchInlineSnapshot(` @@ -504,6 +508,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { includeReferences: true, objects: [], retries: {}, + trueCopy: false, }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Something went wrong while reading this stream"` diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts index 8b78b07e08b78..d8bbb3e2c2644 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts @@ -44,7 +44,8 @@ export function resolveCopySavedObjectsToSpacesConflictsFactory( const resolveConflictsForSpace = async ( spaceId: string, objectsStream: Readable, - retries: SavedObjectsImportRetry[] + retries: SavedObjectsImportRetry[], + trueCopy: boolean ) => { try { const importResponse = await resolveSavedObjectsImportErrors({ @@ -54,6 +55,7 @@ export function resolveCopySavedObjectsToSpacesConflictsFactory( typeRegistry: getTypeRegistry(), readStream: objectsStream, retries, + trueCopy, }); return { @@ -86,7 +88,8 @@ export function resolveCopySavedObjectsToSpacesConflictsFactory( response[spaceId] = await resolveConflictsForSpace( spaceId, createReadableStreamFromArray(exportedSavedObjects), - retries + retries, + options.trueCopy ); } diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/types.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/types.ts index fc25a28819eb7..4301d3790ce60 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/types.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/types.ts @@ -24,6 +24,7 @@ export interface ResolveConflictsOptions { retries: { [spaceId: string]: Array>; }; + trueCopy: boolean; } export interface CopyResponse { diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts index 79faaa36505f5..d94bccd6c4529 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts @@ -149,6 +149,7 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { } ), includeReferences: schema.boolean({ defaultValue: false }), + trueCopy: schema.boolean({ defaultValue: false }), }), }, }, @@ -160,7 +161,7 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { getImportExportObjectLimit, request ); - const { objects, includeReferences, retries } = request.body; + const { objects, includeReferences, retries, trueCopy } = request.body; const sourceSpaceId = spacesService.getSpaceId(request); const resolveConflictsResponse = await resolveCopySavedObjectsToSpacesConflicts( sourceSpaceId, @@ -168,6 +169,7 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { objects, includeReferences, retries, + trueCopy, } ); return response.ok({ body: resolveConflictsResponse }); From 0bae0c9002980864e57222d924f5208d3de81a09 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Sun, 28 Jun 2020 13:52:06 -0400 Subject: [PATCH 27/55] Change `idToOverwrite` field on retry object to `destinationId` This commit also modifies retries so that `destinationId` can be used without `overwrite`. This will allow resolving import errors where any object ID will be changed. --- ...c.savedobjectsimportretry.destinationid.md | 13 ++ ...c.savedobjectsimportretry.idtooverwrite.md | 13 -- ...gin-core-public.savedobjectsimportretry.md | 2 +- ...r.savedobjectsimportretry.destinationid.md | 13 ++ ...r.savedobjectsimportretry.idtooverwrite.md | 13 -- ...gin-core-server.savedobjectsimportretry.md | 2 +- src/core/public/public.api.md | 2 +- .../import/check_origin_conflicts.test.ts | 47 +++----- .../import/check_origin_conflicts.ts | 9 +- .../import/resolve_import_errors.test.ts | 2 +- .../import/resolve_import_errors.ts | 1 - src/core/server/saved_objects/import/types.ts | 4 +- .../import/validate_retries.test.ts | 19 ++- .../saved_objects/import/validate_retries.ts | 12 +- .../routes/resolve_import_errors.ts | 37 +++--- src/core/server/server.api.md | 2 +- .../routes/api/external/copy_to_space.test.ts | 24 ---- .../routes/api/external/copy_to_space.ts | 21 +--- .../common/suites/resolve_import_errors.ts | 60 ++++++---- .../apis/resolve_import_errors.ts | 111 ++++++++++++------ .../apis/resolve_import_errors.ts | 79 ++++++++++--- .../spaces_only/apis/resolve_import_errors.ts | 51 ++++++-- .../suites/resolve_copy_to_space_conflicts.ts | 4 +- 23 files changed, 300 insertions(+), 241 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.destinationid.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.idtooverwrite.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.destinationid.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.idtooverwrite.md diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.destinationid.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.destinationid.md new file mode 100644 index 0000000000000..5131d1d01ff02 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.destinationid.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportRetry](./kibana-plugin-core-public.savedobjectsimportretry.md) > [destinationId](./kibana-plugin-core-public.savedobjectsimportretry.destinationid.md) + +## SavedObjectsImportRetry.destinationId property + +The object ID that will be created or overwritten. If not specified, the `id` field will be used. + +Signature: + +```typescript +destinationId?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.idtooverwrite.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.idtooverwrite.md deleted file mode 100644 index 6c240852908a1..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.idtooverwrite.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportRetry](./kibana-plugin-core-public.savedobjectsimportretry.md) > [idToOverwrite](./kibana-plugin-core-public.savedobjectsimportretry.idtooverwrite.md) - -## SavedObjectsImportRetry.idToOverwrite property - -The object ID that will be overwritten. Only used if `overwrite` == true. This is required to resolve ambiguous conflicts. - -Signature: - -```typescript -idToOverwrite?: string; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.md index 9ca858c681508..c72dd22a68739 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.md @@ -16,8 +16,8 @@ export interface SavedObjectsImportRetry | Property | Type | Description | | --- | --- | --- | +| [destinationId](./kibana-plugin-core-public.savedobjectsimportretry.destinationid.md) | string | The object ID that will be created or overwritten. If not specified, the id field will be used. | | [id](./kibana-plugin-core-public.savedobjectsimportretry.id.md) | string | | -| [idToOverwrite](./kibana-plugin-core-public.savedobjectsimportretry.idtooverwrite.md) | string | The object ID that will be overwritten. Only used if overwrite == true. This is required to resolve ambiguous conflicts. | | [overwrite](./kibana-plugin-core-public.savedobjectsimportretry.overwrite.md) | boolean | | | [replaceReferences](./kibana-plugin-core-public.savedobjectsimportretry.replacereferences.md) | Array<{
type: string;
from: string;
to: string;
}> | | | [type](./kibana-plugin-core-public.savedobjectsimportretry.type.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.destinationid.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.destinationid.md new file mode 100644 index 0000000000000..9a3ccf4442db7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.destinationid.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportRetry](./kibana-plugin-core-server.savedobjectsimportretry.md) > [destinationId](./kibana-plugin-core-server.savedobjectsimportretry.destinationid.md) + +## SavedObjectsImportRetry.destinationId property + +The object ID that will be created or overwritten. If not specified, the `id` field will be used. + +Signature: + +```typescript +destinationId?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.idtooverwrite.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.idtooverwrite.md deleted file mode 100644 index c060fe0bfe121..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.idtooverwrite.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportRetry](./kibana-plugin-core-server.savedobjectsimportretry.md) > [idToOverwrite](./kibana-plugin-core-server.savedobjectsimportretry.idtooverwrite.md) - -## SavedObjectsImportRetry.idToOverwrite property - -The object ID that will be overwritten. Only used if `overwrite` == true. This is required to resolve ambiguous conflicts. - -Signature: - -```typescript -idToOverwrite?: string; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.md index 130bd98a8ea79..73c416696b51f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.md @@ -16,8 +16,8 @@ export interface SavedObjectsImportRetry | Property | Type | Description | | --- | --- | --- | +| [destinationId](./kibana-plugin-core-server.savedobjectsimportretry.destinationid.md) | string | The object ID that will be created or overwritten. If not specified, the id field will be used. | | [id](./kibana-plugin-core-server.savedobjectsimportretry.id.md) | string | | -| [idToOverwrite](./kibana-plugin-core-server.savedobjectsimportretry.idtooverwrite.md) | string | The object ID that will be overwritten. Only used if overwrite == true. This is required to resolve ambiguous conflicts. | | [overwrite](./kibana-plugin-core-server.savedobjectsimportretry.overwrite.md) | boolean | | | [replaceReferences](./kibana-plugin-core-server.savedobjectsimportretry.replacereferences.md) | Array<{
type: string;
from: string;
to: string;
}> | | | [type](./kibana-plugin-core-server.savedobjectsimportretry.type.md) | string | | diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index be4406f103e24..31a4f37502bb2 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -1376,9 +1376,9 @@ export interface SavedObjectsImportResponse { // @public export interface SavedObjectsImportRetry { + destinationId?: string; // (undocumented) id: string; - idToOverwrite?: string; // (undocumented) overwrite: boolean; // (undocumented) diff --git a/src/core/server/saved_objects/import/check_origin_conflicts.test.ts b/src/core/server/saved_objects/import/check_origin_conflicts.test.ts index 620ddd552f603..b8d0e024d2b85 100644 --- a/src/core/server/saved_objects/import/check_origin_conflicts.test.ts +++ b/src/core/server/saved_objects/import/check_origin_conflicts.test.ts @@ -444,28 +444,25 @@ describe('#checkOriginConflicts', () => { }); describe('#getImportIdMapForRetries', () => { - let typeRegistry: jest.Mocked; - const setupOptions = ( retries: SavedObjectsImportRetry[], trueCopy: boolean = false ): GetImportIdMapForRetriesOptions => { - typeRegistry = typeRegistryMock.create(); - typeRegistry.isMultiNamespace.mockImplementation((type) => type === MULTI_NS_TYPE); - return { typeRegistry, retries, trueCopy }; + return { retries, trueCopy }; }; - const createOverwriteRetry = ( + const createRetry = ( { type, id }: { type: string; id: string }, - idToOverwrite?: string + options: { destinationId?: string } = {} ): SavedObjectsImportRetry => { - return { type, id, overwrite: true, idToOverwrite, replaceReferences: [] }; + const { destinationId } = options; + return { type, id, overwrite: false, destinationId, replaceReferences: [] }; }; test('throws an error if retry is not found for an object', async () => { const obj1 = createObject(MULTI_NS_TYPE, 'id-1'); const obj2 = createObject(MULTI_NS_TYPE, 'id-2'); - const retries = [createOverwriteRetry(obj1)]; + const retries = [createRetry(obj1)]; const options = setupOptions(retries); expect(() => @@ -474,40 +471,32 @@ describe('#getImportIdMapForRetries', () => { }); test('returns expected results', async () => { - const obj1 = createObject(OTHER_TYPE, 'id-1'); - const obj2 = createObject(OTHER_TYPE, 'id-2'); - const obj3 = createObject(OTHER_TYPE, 'id-3'); - const obj4 = createObject(MULTI_NS_TYPE, 'id-4'); - const obj5 = createObject(MULTI_NS_TYPE, 'id-5'); - const obj6 = createObject(MULTI_NS_TYPE, 'id-6'); - const objects = [obj1, obj2, obj3, obj4, obj5, obj6]; + const obj1 = createObject('type-1', 'id-1'); + const obj2 = createObject('type-2', 'id-2'); + const obj3 = createObject('type-3', 'id-3'); + const objects = [obj1, obj2, obj3]; const retries = [ - // all three overwrite retries for non-multi-namespace types are ignored; - // retries for non-multi-namespace these should not have `idToOverwrite` specified, but we test it here for posterity - createOverwriteRetry(obj1), - createOverwriteRetry(obj2, obj2.id), - createOverwriteRetry(obj3, 'id-X'), - createOverwriteRetry(obj4), // retries that do not have `idToOverwrite` specified are ignored - createOverwriteRetry(obj5, obj5.id), // retries that have `id` that matches `idToOverwrite` are ignored - createOverwriteRetry(obj6, 'id-Y'), // this retry will get added to the `importIdMap`! + createRetry(obj1), // retries that do not have `destinationId` specified are ignored + createRetry(obj2, { destinationId: obj2.id }), // retries that have `id` that matches `destinationId` are ignored + createRetry(obj3, { destinationId: 'id-X' }), // this retry will get added to the `importIdMap`! ]; const options = setupOptions(retries); const checkOriginConflictsResult = await getImportIdMapForRetries(objects, options); expect(checkOriginConflictsResult).toEqual( - new Map([[`${obj6.type}:${obj6.id}`, { id: 'id-Y', omitOriginId: false }]]) + new Map([[`${obj3.type}:${obj3.id}`, { id: 'id-X', omitOriginId: false }]]) ); }); test('omits origin ID in `importIdMap` entries when trueCopy=true', async () => { - const obj6 = createObject(MULTI_NS_TYPE, 'id-6'); - const objects = [obj6]; - const retries = [createOverwriteRetry(obj6, 'id-Y')]; + const obj = createObject('type-1', 'id-1'); + const objects = [obj]; + const retries = [createRetry(obj, { destinationId: 'id-X' })]; const options = setupOptions(retries, true); const checkOriginConflictsResult = await getImportIdMapForRetries(objects, options); expect(checkOriginConflictsResult).toEqual( - new Map([[`${obj6.type}:${obj6.id}`, { id: 'id-Y', omitOriginId: true }]]) + new Map([[`${obj.type}:${obj.id}`, { id: 'id-X', omitOriginId: true }]]) ); }); }); diff --git a/src/core/server/saved_objects/import/check_origin_conflicts.ts b/src/core/server/saved_objects/import/check_origin_conflicts.ts index 2c0e549db0867..8244734f42070 100644 --- a/src/core/server/saved_objects/import/check_origin_conflicts.ts +++ b/src/core/server/saved_objects/import/check_origin_conflicts.ts @@ -34,7 +34,6 @@ interface CheckOriginConflictsOptions { } interface GetImportIdMapForRetriesOptions { - typeRegistry: ISavedObjectTypeRegistry; retries: SavedObjectsImportRetry[]; trueCopy: boolean; } @@ -210,7 +209,7 @@ export function getImportIdMapForRetries( objects: Array>, options: GetImportIdMapForRetriesOptions ) { - const { typeRegistry, retries, trueCopy } = options; + const { retries, trueCopy } = options; const retryMap = retries.reduce( (acc, cur) => acc.set(`${cur.type}:${cur.id}`, cur), @@ -223,9 +222,9 @@ export function getImportIdMapForRetries( if (!retry) { throw new Error(`Retry was expected for "${type}:${id}" but not found`); } - const { overwrite, idToOverwrite } = retry; - if (overwrite && idToOverwrite && idToOverwrite !== id && typeRegistry.isMultiNamespace(type)) { - importIdMap.set(`${type}:${id}`, { id: idToOverwrite, omitOriginId: trueCopy }); + const { destinationId } = retry; + if (destinationId && destinationId !== id) { + importIdMap.set(`${type}:${id}`, { id: destinationId, omitOriginId: trueCopy }); } }); 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 e77c733885840..ac31bb2008bb8 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 @@ -215,7 +215,7 @@ describe('#importSavedObjectsFromStream', () => { }); await resolveSavedObjectsImportErrors(options); - const opts = { typeRegistry, retries, trueCopy }; + const opts = { retries, trueCopy }; expect(getImportIdMapForRetries).toHaveBeenCalledWith(filteredObjects, opts); }); 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 b3c693a42af5d..4b1f112bb4f5c 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.ts @@ -114,7 +114,6 @@ export async function resolveSavedObjectsImportErrors({ // Check multi-namespace object types for regular conflicts and ambiguous conflicts const importIdMapForRetries = getImportIdMapForRetries(checkConflictsResult.filteredObjects, { - typeRegistry, retries, trueCopy, }); diff --git a/src/core/server/saved_objects/import/types.ts b/src/core/server/saved_objects/import/types.ts index 2762f8333e437..fb32e7943e2bd 100644 --- a/src/core/server/saved_objects/import/types.ts +++ b/src/core/server/saved_objects/import/types.ts @@ -30,9 +30,9 @@ export interface SavedObjectsImportRetry { id: string; overwrite: boolean; /** - * The object ID that will be overwritten. Only used if `overwrite` == true. This is required to resolve ambiguous conflicts. + * The object ID that will be created or overwritten. If not specified, the `id` field will be used. */ - idToOverwrite?: string; + destinationId?: string; replaceReferences: Array<{ type: string; from: string; diff --git a/src/core/server/saved_objects/import/validate_retries.test.ts b/src/core/server/saved_objects/import/validate_retries.test.ts index 09a10efea13cc..7861e60a323c5 100644 --- a/src/core/server/saved_objects/import/validate_retries.test.ts +++ b/src/core/server/saved_objects/import/validate_retries.test.ts @@ -45,18 +45,17 @@ describe('#validateRetries', () => { test('non-empty retries', () => { const retry1 = createRetry({ type: 'foo', id: '1' }); const retry2 = createRetry({ type: 'foo', id: '2', overwrite: true }); - const retry3 = createRetry({ type: 'foo', id: '3', idToOverwrite: 'a' }); // this is not a valid retry but we test it for posterity - const retry4 = createRetry({ type: 'foo', id: '4', overwrite: true, idToOverwrite: 'b' }); - const retry5 = createRetry({ type: 'foo', id: '5', overwrite: true, idToOverwrite: 'c' }); - const retries = [retry1, retry2, retry3, retry4, retry5]; + const retry3 = createRetry({ type: 'foo', id: '3', destinationId: 'a' }); + const retry4 = createRetry({ type: 'foo', id: '4', overwrite: true, destinationId: 'b' }); + const retries = [retry1, retry2, retry3, retry4]; validateRetries(retries); expect(getNonUniqueEntries).toHaveBeenCalledTimes(2); // check all retry objects for non-unique entries expect(getNonUniqueEntries).toHaveBeenNthCalledWith(1, retries); - // check only retry overwrites with `overwrite` === true and `idToOverwrite` !== undefined for non-unique entries + // check only retry objects with `destinationId` !== undefined for non-unique entries const retryOverwriteEntries = [ - { type: retry4.type, id: retry4.idToOverwrite }, - { type: retry5.type, id: retry5.idToOverwrite }, + { type: retry3.type, id: retry3.destinationId }, + { type: retry4.type, id: retry4.destinationId }, ]; expect(getNonUniqueEntries).toHaveBeenNthCalledWith(2, retryOverwriteEntries); }); @@ -76,7 +75,7 @@ describe('#validateRetries', () => { } }); - test('throws Boom error if any retry overwrites are not unique', () => { + test('throws Boom error if any retry destinations are not unique', () => { mockGetNonUniqueEntries.mockReturnValueOnce([]); mockGetNonUniqueEntries.mockReturnValue(['type1:id1', 'type2:id2']); expect.assertions(2); @@ -85,12 +84,12 @@ describe('#validateRetries', () => { } catch ({ isBoom, message }) { expect(isBoom).toBe(true); expect(message).toMatchInlineSnapshot( - `"Non-unique retry overwrites: [type1:id1,type2:id2]: Bad Request"` + `"Non-unique retry destinations: [type1:id1,type2:id2]: Bad Request"` ); } }); - test('does not throw error if retry objects and retry overwrites are unique', () => { + test('does not throw error if retry objects and retry destinations are unique', () => { // no need to mock return value, the mock `getNonUniqueEntries` function returns an empty array by default expect(() => validateRetries([])).not.toThrowError(); }); diff --git a/src/core/server/saved_objects/import/validate_retries.ts b/src/core/server/saved_objects/import/validate_retries.ts index 275f2ca6b746a..f32c5bf82f989 100644 --- a/src/core/server/saved_objects/import/validate_retries.ts +++ b/src/core/server/saved_objects/import/validate_retries.ts @@ -29,13 +29,13 @@ export const validateRetries = (retries: SavedObjectsImportRetry[]) => { ); } - const overwriteEntries = retries - .filter((retry) => retry.overwrite && retry.idToOverwrite !== undefined) - .map(({ type, idToOverwrite }) => ({ type, id: idToOverwrite! })); - const nonUniqueRetryOverwrites = getNonUniqueEntries(overwriteEntries); - if (nonUniqueRetryOverwrites.length > 0) { + const destinationEntries = retries + .filter((retry) => retry.destinationId !== undefined) + .map(({ type, destinationId }) => ({ type, id: destinationId! })); + const nonUniqueRetryDestinations = getNonUniqueEntries(destinationEntries); + if (nonUniqueRetryDestinations.length > 0) { throw SavedObjectsErrorHelpers.createBadRequestError( - `Non-unique retry overwrites: [${nonUniqueRetryOverwrites.join()}]` + `Non-unique retry destinations: [${nonUniqueRetryDestinations.join()}]` ); } }; diff --git a/src/core/server/saved_objects/routes/resolve_import_errors.ts b/src/core/server/saved_objects/routes/resolve_import_errors.ts index 74932866e39e9..9f0a674bdd25e 100644 --- a/src/core/server/saved_objects/routes/resolve_import_errors.ts +++ b/src/core/server/saved_objects/routes/resolve_import_errors.ts @@ -51,29 +51,20 @@ export const registerResolveImportErrorsRoute = (router: IRouter, config: SavedO body: schema.object({ file: schema.stream(), retries: schema.arrayOf( - schema.object( - { - type: schema.string(), - id: schema.string(), - overwrite: schema.boolean({ defaultValue: false }), - idToOverwrite: schema.maybe(schema.string()), - replaceReferences: schema.arrayOf( - schema.object({ - type: schema.string(), - from: schema.string(), - to: schema.string(), - }), - { defaultValue: [] } - ), - }, - { - validate: (object) => { - if (object.idToOverwrite && !object.overwrite) { - return 'cannot use [idToOverwrite] without [overwrite]'; - } - }, - } - ) + schema.object({ + type: schema.string(), + id: schema.string(), + overwrite: schema.boolean({ defaultValue: false }), + destinationId: schema.maybe(schema.string()), + replaceReferences: schema.arrayOf( + schema.object({ + type: schema.string(), + from: schema.string(), + to: schema.string(), + }), + { defaultValue: [] } + ), + }) ), }), }, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 51e02741214ad..f16ebca1d7d77 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2245,9 +2245,9 @@ export interface SavedObjectsImportResponse { // @public export interface SavedObjectsImportRetry { + destinationId?: string; // (undocumented) id: string; - idToOverwrite?: string; // (undocumented) overwrite: boolean; // (undocumented) diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts index 619f40863fd27..77f1751f8a104 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts @@ -323,30 +323,6 @@ describe('copy to space', () => { ).toThrowErrorMatchingInlineSnapshot(`"[objects]: duplicate objects are not allowed"`); }); - it(`does not allow "idToOverwrite" to be used without "overwrite"`, async () => { - const payload = { - retries: { - ['a-space']: [ - { - type: 'foo', - id: 'bar', - overwrite: false, - idToOverwrite: 'baz', - }, - ], - }, - objects: [{ type: 'foo', id: 'bar' }], - }; - - const { resolveConflicts } = await setup(); - - expect(() => - (resolveConflicts.routeValidation.body as ObjectType).validate(payload) - ).toThrowErrorMatchingInlineSnapshot( - `"[retries.a-space.0]: cannot use [idToOverwrite] without [overwrite]"` - ); - }); - it(`requires well-formed space ids`, async () => { const payload = { retries: { diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts index d94bccd6c4529..88d71ed9cb0e4 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts @@ -118,21 +118,12 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { }, }), schema.arrayOf( - schema.object( - { - type: schema.string(), - id: schema.string(), - overwrite: schema.boolean({ defaultValue: false }), - idToOverwrite: schema.maybe(schema.string()), - }, - { - validate: (object) => { - if (object.idToOverwrite && !object.overwrite) { - return 'cannot use [idToOverwrite] without [overwrite]'; - } - }, - } - ) + schema.object({ + type: schema.string(), + id: schema.string(), + overwrite: schema.boolean({ defaultValue: false }), + destinationId: schema.maybe(schema.string()), + }) ) ), objects: schema.arrayOf( diff --git a/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts index cc7ab600b9b67..f0e4dfe8e3ccc 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts @@ -14,16 +14,15 @@ import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/ export interface ResolveImportErrorsTestDefinition extends TestDefinition { request: { objects: Array<{ type: string; id: string; originId?: string }>; - retries: Array< - { type: string; id: string } & ({ overwrite: true; idToOverwrite?: string } | {}) - >; + retries: Array<{ type: string; id: string; overwrite: boolean; destinationId?: string }>; }; overwrite: boolean; + trueCopy: boolean; } export type ResolveImportErrorsTestSuite = TestSuite; export interface ResolveImportErrorsTestCase extends TestCase { originId?: string; - idToOverwrite?: string; // only used for overwrite retries for multi-namespace object types + expectedNewId?: string; successParam?: string; failure?: 400 | 409; // only used for permitted response case } @@ -44,18 +43,18 @@ export const TEST_CASES = Object.freeze({ type: 'sharedtype', id: `conflict_2c`, originId: `conflict_2`, - idToOverwrite: `conflict_2a`, + expectedNewId: `conflict_2a`, }), CONFLICT_3A_OBJ: Object.freeze({ type: 'sharedtype', id: `conflict_3a`, originId: `conflict_3`, - idToOverwrite: `conflict_3`, + expectedNewId: `conflict_3`, }), CONFLICT_4_OBJ: Object.freeze({ type: 'sharedtype', id: `conflict_4`, - idToOverwrite: `conflict_4a`, + expectedNewId: `conflict_4a`, }), }); @@ -63,13 +62,11 @@ export const TEST_CASES = Object.freeze({ * Test cases have additional properties that we don't want to send in HTTP Requests */ const createRequest = ( - { type, id, originId, idToOverwrite }: ResolveImportErrorsTestCase, + { type, id, originId, expectedNewId }: ResolveImportErrorsTestCase, overwrite: boolean ): ResolveImportErrorsTestDefinition['request'] => ({ objects: [{ type, id, ...(originId && { originId }) }], - retries: overwrite - ? [{ type, id, overwrite, ...(idToOverwrite && { idToOverwrite }) }] - : [{ type, id }], + retries: [{ type, id, overwrite, ...(expectedNewId && { destinationId: expectedNewId }) }], }); export function resolveImportErrorsTestSuiteFactory( @@ -100,7 +97,7 @@ export function resolveImportErrorsTestSuiteFactory( expect(response.body).not.to.have.property('errors'); } for (let i = 0; i < expectedSuccesses.length; i++) { - const { type, id, successParam, idToOverwrite } = expectedSuccesses[i]; + const { type, id, successParam, expectedNewId } = expectedSuccesses[i]; // we don't know the order of the returned successResults; search for each one const object = (successResults as Array>).find( (x) => x.type === type && x.id === id @@ -111,12 +108,14 @@ export function resolveImportErrorsTestSuiteFactory( // Kibana created the object with a different ID than what was specified in the import // This can happen due to an unresolvable conflict (so the new ID will be random), or due to an inexact match (so the new ID will // be equal to the ID or originID of the existing object that it inexactly matched) - if (idToOverwrite) { - expect(destinationId).to.be(idToOverwrite); + if (expectedNewId) { + expect(destinationId).to.be(expectedNewId); } else { // the new ID was randomly generated expect(destinationId).to.match(/^[0-9a-f-]{36}$/); } + } else if (successParam === 'trueCopy') { + expect(destinationId).to.be(expectedNewId!); } else { expect(destinationId).to.be(undefined); } @@ -129,7 +128,7 @@ export function resolveImportErrorsTestSuiteFactory( expect(_source[type][NEW_ATTRIBUTE_KEY]).to.eql(NEW_ATTRIBUTE_VAL); } for (let i = 0; i < expectedFailures.length; i++) { - const { type, id, failure } = expectedFailures[i]; + const { type, id, failure, expectedNewId } = expectedFailures[i]; // we don't know the order of the returned errors; search for each one const object = (errors as Array>).find( (x) => x.type === type && x.id === id @@ -139,7 +138,10 @@ export function resolveImportErrorsTestSuiteFactory( expect(object!.error).to.eql({ type: 'unsupported_type' }); } else { // 409 - expect(object!.error).to.eql({ type: 'conflict' }); + expect(object!.error).to.eql({ + type: 'conflict', + ...(expectedNewId && { destinationId: expectedNewId }), + }); } } } @@ -147,8 +149,9 @@ export function resolveImportErrorsTestSuiteFactory( const createTestDefinitions = ( testCases: ResolveImportErrorsTestCase | ResolveImportErrorsTestCase[], forbidden: boolean, - overwrite: boolean, - options?: { + options: { + overwrite?: boolean; + trueCopy?: boolean; spaceId?: string; singleRequest?: boolean; responseBodyOverride?: ExpectResponseBody; @@ -156,17 +159,23 @@ export function resolveImportErrorsTestSuiteFactory( ): ResolveImportErrorsTestDefinition[] => { const cases = Array.isArray(testCases) ? testCases : [testCases]; const responseStatusCode = forbidden ? 403 : 200; - if (!options?.singleRequest) { + const { + overwrite = false, + trueCopy = false, + spaceId, + singleRequest, + responseBodyOverride, + } = options; + if (!singleRequest) { // if we are testing cases that should result in a forbidden response, we can do each case individually // this ensures that multiple test cases of a single type will each result in a forbidden error return cases.map((x) => ({ title: getTestTitle(x, responseStatusCode), request: createRequest(x, overwrite), responseStatusCode, - responseBody: - options?.responseBodyOverride || - expectResponseBody(x, responseStatusCode, options?.spaceId), + responseBody: responseBodyOverride || expectResponseBody(x, responseStatusCode, spaceId), overwrite, + trueCopy, })); } // batch into a single request to save time during test execution @@ -181,9 +190,9 @@ export function resolveImportErrorsTestSuiteFactory( })), responseStatusCode, responseBody: - options?.responseBodyOverride || - expectResponseBody(cases, responseStatusCode, options?.spaceId), + responseBodyOverride || expectResponseBody(cases, responseStatusCode, spaceId), overwrite, + trueCopy, }, ]; }; @@ -205,8 +214,9 @@ export function resolveImportErrorsTestSuiteFactory( const requestBody = test.request.objects .map((obj) => JSON.stringify({ ...obj, ...attrs })) .join('\n'); + const query = test.trueCopy ? '?trueCopy=true' : ''; await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_resolve_import_errors`) + .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_resolve_import_errors${query}`) .auth(user?.username, user?.password) .field('retries', JSON.stringify(test.request.retries)) .attach('file', Buffer.from(requestBody, 'utf8'), 'export.ndjson') diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts index a61d7cd7ffb7b..0a66b3479482f 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { v4 as uuidv4 } from 'uuid'; import { SPACES } from '../../common/lib/spaces'; import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { TestUser } from '../../common/lib/types'; @@ -23,6 +24,20 @@ const { fail400, fail409 } = testCaseFailures; const destinationId = (condition?: boolean) => condition !== false ? { successParam: 'destinationId' } : {}; +const createTrueCopyTestCases = () => { + // for each outcome, if failure !== undefined then we expect to receive + // an error; otherwise, we expect to receive a success result + const cases = Object.entries(CASES).filter(([key]) => key !== 'HIDDEN'); + const importable = cases.map(([, val]) => ({ + ...val, + successParam: 'trueCopy', + expectedNewId: uuidv4(), + })); + const nonImportable = [{ ...CASES.HIDDEN, ...fail400() }]; + const all = [...importable, ...nonImportable]; + return { importable, nonImportable, all }; +}; + const createTestCases = (overwrite: boolean, spaceId: string) => { // for each permitted (non-403) outcome, if failure !== undefined then we expect // to receive an error; otherwise, we expect to receive a success result @@ -55,11 +70,11 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { ...destinationId(spaceId !== SPACE_2_ID), }, // all of the cases below represent imports that had an inexact match conflict or an ambiguous conflict - // if we call _resolve_import_errors and don't specify overwrite, each of these will not result in a conflict because they will skip the - // preflight search results; so the objects will be created instead. - { ...CASES.CONFLICT_2C_OBJ, ...destinationId(overwrite) }, // "ambiguous destination" conflict; if overwrite=true, will overwrite 'conflict_2a' - { ...CASES.CONFLICT_3A_OBJ, ...destinationId(overwrite) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3' - { ...CASES.CONFLICT_4_OBJ, ...destinationId(overwrite) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a' + // if we call _resolve_import_errors and don't specify overwrite, each of these will result in a conflict because an object with that + // `expectedDestinationId` already exists + { ...CASES.CONFLICT_2C_OBJ, ...fail409(!overwrite), ...destinationId() }, // "ambiguous destination" conflict; if overwrite=true, will overwrite 'conflict_2a' + { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3' + { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a' ]; return { group1Importable, group1NonImportable, group1All, group2 }; }; @@ -74,54 +89,78 @@ export default function ({ getService }: FtrProviderContext) { esArchiver, supertest ); - const createTests = (overwrite: boolean, spaceId: string) => { + const createTests = (overwrite: boolean, trueCopy: boolean, spaceId: string) => { + // use singleRequest to reduce execution time and/or test combined cases + const singleRequest = true; + + if (trueCopy) { + const { importable, nonImportable, all } = createTrueCopyTestCases(); + return { + unauthorized: [ + createTestDefinitions(importable, true, { trueCopy, spaceId }), + createTestDefinitions(nonImportable, false, { trueCopy, spaceId, singleRequest }), + createTestDefinitions(all, true, { + trueCopy, + spaceId, + singleRequest, + responseBodyOverride: expectForbidden(['globaltype', 'isolatedtype', 'sharedtype']), + }), + ].flat(), + authorized: createTestDefinitions(all, false, { trueCopy, spaceId, singleRequest }), + }; + } + const { group1Importable, group1NonImportable, group1All, group2 } = createTestCases( overwrite, spaceId ); - const singleRequest = true; - // use singleRequest to reduce execution time and/or test combined cases return { unauthorized: [ - createTestDefinitions(group1Importable, true, overwrite, { spaceId }), - createTestDefinitions(group1NonImportable, false, overwrite, { spaceId, singleRequest }), - createTestDefinitions(group1All, true, overwrite, { + createTestDefinitions(group1Importable, true, { overwrite, spaceId }), + createTestDefinitions(group1NonImportable, false, { overwrite, spaceId, singleRequest }), + createTestDefinitions(group1All, true, { + overwrite, spaceId, singleRequest, responseBodyOverride: expectForbidden(['globaltype', 'isolatedtype']), }), - createTestDefinitions(group2, true, overwrite, { spaceId, singleRequest }), + createTestDefinitions(group2, true, { overwrite, spaceId, singleRequest }), ].flat(), authorized: [ - createTestDefinitions(group1All, false, overwrite, { spaceId, singleRequest }), - createTestDefinitions(group2, false, overwrite, { spaceId, singleRequest }), + createTestDefinitions(group1All, false, { overwrite, spaceId, singleRequest }), + createTestDefinitions(group2, false, { overwrite, spaceId, singleRequest }), ].flat(), }; }; describe('_resolve_import_errors', () => { - getTestScenarios([false, true]).securityAndSpaces.forEach( - ({ spaceId, users, modifier: overwrite }) => { - const suffix = ` within the ${spaceId} space` + overwrite ? ' with overwrite enabled' : ''; - const { unauthorized, authorized } = createTests(overwrite!, spaceId); - const _addTests = (user: TestUser, tests: ResolveImportErrorsTestDefinition[]) => { - addTests(`${user.description}${suffix}`, { user, spaceId, tests }); - }; + getTestScenarios([ + [false, false], + [false, true], + [true, false], + ]).securityAndSpaces.forEach(({ spaceId, users, modifier }) => { + const [overwrite, trueCopy] = modifier!; + const suffix = ` within the ${spaceId} space${ + overwrite ? ' with overwrite enabled' : trueCopy ? ' with trueCopy enabled' : '' + }`; + const { unauthorized, authorized } = createTests(overwrite, trueCopy, spaceId); + const _addTests = (user: TestUser, tests: ResolveImportErrorsTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; - [ - users.noAccess, - users.legacyAll, - users.dualRead, - users.readGlobally, - users.readAtSpace, - users.allAtOtherSpace, - ].forEach((user) => { - _addTests(user, unauthorized); - }); - [users.dualAll, users.allGlobally, users.allAtSpace, users.superuser].forEach((user) => { - _addTests(user, authorized); - }); - } - ); + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.readAtSpace, + users.allAtOtherSpace, + ].forEach((user) => { + _addTests(user, unauthorized); + }); + [users.dualAll, users.allGlobally, users.allAtSpace, users.superuser].forEach((user) => { + _addTests(user, authorized); + }); + }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts index 0f4fd66939ca6..1cf732e45a39f 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { v4 as uuidv4 } from 'uuid'; import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -17,6 +18,20 @@ const { fail400, fail409 } = testCaseFailures; const destinationId = (condition?: boolean) => condition !== false ? { successParam: 'destinationId' } : {}; +const createTrueCopyTestCases = () => { + // for each outcome, if failure !== undefined then we expect to receive + // an error; otherwise, we expect to receive a success result + const cases = Object.entries(CASES).filter(([key]) => key !== 'HIDDEN'); + const importable = cases.map(([, val]) => ({ + ...val, + successParam: 'trueCopy', + expectedNewId: uuidv4(), + })); + const nonImportable = [{ ...CASES.HIDDEN, ...fail400() }]; + const all = [...importable, ...nonImportable]; + return { importable, nonImportable, all }; +}; + const createTestCases = (overwrite: boolean) => { // for each permitted (non-403) outcome, if failure !== undefined then we expect // to receive an error; otherwise, we expect to receive a success result @@ -29,11 +44,11 @@ const createTestCases = (overwrite: boolean) => { const group2 = [ { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite) }, // all of the cases below represent imports that had an inexact match conflict or an ambiguous conflict - // if we call _resolve_import_errors and don't specify overwrite, each of these will not result in a conflict because they will skip the - // preflight search results; so the objects will be created instead. - { ...CASES.CONFLICT_2C_OBJ, ...destinationId(overwrite) }, // "ambiguous destination" conflict; if overwrite=true, will overwrite 'conflict_2a' - { ...CASES.CONFLICT_3A_OBJ, ...destinationId(overwrite) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3' - { ...CASES.CONFLICT_4_OBJ, ...destinationId(overwrite) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a' + // if we call _resolve_import_errors and don't specify overwrite, each of these will result in a conflict because an object with that + // `expectedDestinationId` already exists + { ...CASES.CONFLICT_2C_OBJ, ...fail409(!overwrite), ...destinationId() }, // "ambiguous destination" conflict; if overwrite=true, will overwrite 'conflict_2a' + { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3' + { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a' ]; return { group1Importable, group1NonImportable, group1All, group2 }; }; @@ -48,32 +63,58 @@ export default function ({ getService }: FtrProviderContext) { esArchiver, supertest ); - const createTests = (overwrite: boolean) => { - const { group1Importable, group1NonImportable, group1All, group2 } = createTestCases(overwrite); + const createTests = (overwrite: boolean, trueCopy: boolean) => { // use singleRequest to reduce execution time and/or test combined cases + const singleRequest = true; + + if (trueCopy) { + const { importable, nonImportable, all } = createTrueCopyTestCases(); + return { + unauthorized: [ + createTestDefinitions(importable, true, { trueCopy }), + createTestDefinitions(nonImportable, false, { trueCopy, singleRequest }), + createTestDefinitions(all, true, { + trueCopy, + singleRequest, + responseBodyOverride: expectForbidden(['globaltype', 'isolatedtype', 'sharedtype']), + }), + ].flat(), + authorized: createTestDefinitions(all, false, { trueCopy, singleRequest }), + }; + } + + const { group1Importable, group1NonImportable, group1All, group2 } = createTestCases(overwrite); return { unauthorized: [ - createTestDefinitions(group1Importable, true, overwrite), - createTestDefinitions(group1NonImportable, false, overwrite, { - singleRequest: true, - }), - createTestDefinitions(group1All, true, overwrite, { - singleRequest: true, + createTestDefinitions(group1Importable, true, { overwrite }), + createTestDefinitions(group1NonImportable, false, { overwrite, singleRequest }), + createTestDefinitions(group1All, true, { + overwrite, + singleRequest, responseBodyOverride: expectForbidden(['globaltype', 'isolatedtype']), }), - createTestDefinitions(group2, true, overwrite, { singleRequest: true }), + createTestDefinitions(group2, true, { overwrite, singleRequest }), ].flat(), authorized: [ - createTestDefinitions(group1All, false, overwrite, { singleRequest: true }), - createTestDefinitions(group2, false, overwrite, { singleRequest: true }), + createTestDefinitions(group1All, false, { overwrite, singleRequest }), + createTestDefinitions(group2, false, { overwrite, singleRequest }), ].flat(), }; }; describe('_resolve_import_errors', () => { - getTestScenarios([false, true]).security.forEach(({ users, modifier: overwrite }) => { - const suffix = overwrite ? ' with overwrite enabled' : ''; - const { unauthorized, authorized } = createTests(overwrite!); + getTestScenarios([ + [false, false], + [false, true], + [true, false], + ]).security.forEach(({ users, modifier }) => { + const [overwrite, trueCopy] = modifier!; + const suffix = overwrite + ? ' with overwrite enabled' + : trueCopy + ? ' with trueCopy enabled' + : ''; + const { unauthorized, authorized } = createTests(overwrite, trueCopy); const _addTests = (user: TestUser, tests: ResolveImportErrorsTestDefinition[]) => { addTests(`${user.description}${suffix}`, { user, tests }); }; diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts index b8034c053113d..a45abeb644845 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { v4 as uuidv4 } from 'uuid'; import { SPACES } from '../../common/lib/spaces'; import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -21,6 +22,16 @@ const { fail400, fail409 } = testCaseFailures; const destinationId = (condition?: boolean) => condition !== false ? { successParam: 'destinationId' } : {}; +const createTrueCopyTestCases = () => { + // for each outcome, if failure !== undefined then we expect to receive + // an error; otherwise, we expect to receive a success result + const cases = Object.entries(CASES).filter(([key]) => key !== 'HIDDEN'); + return [ + ...cases.map(([, val]) => ({ ...val, successParam: 'trueCopy', expectedNewId: uuidv4() })), + { ...CASES.HIDDEN, ...fail400() }, + ]; +}; + const createTestCases = (overwrite: boolean, spaceId: string) => { // for each outcome, if failure !== undefined then we expect to receive // an error; otherwise, we expect to receive a success result @@ -50,11 +61,11 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, { ...CASES.HIDDEN, ...fail400() }, // all of the cases below represent imports that had an inexact match conflict or an ambiguous conflict - // if we call _resolve_import_errors and don't specify overwrite, each of these will not result in a conflict because they will skip the - // preflight search results; so the objects will be created instead. - { ...CASES.CONFLICT_2C_OBJ, ...destinationId(overwrite) }, // "ambiguous destination" conflict; if overwrite=true, will overwrite 'conflict_2a' - { ...CASES.CONFLICT_3A_OBJ, ...destinationId(overwrite) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3' - { ...CASES.CONFLICT_4_OBJ, ...destinationId(overwrite) }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a' + // if we call _resolve_import_errors and don't specify overwrite, each of these will result in a conflict because an object with that + // `expectedDestinationId` already exists + { ...CASES.CONFLICT_2C_OBJ, ...fail409(!overwrite), ...destinationId() }, // "ambiguous destination" conflict; if overwrite=true, will overwrite 'conflict_2a' + { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3' + { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a' ]; }; @@ -68,18 +79,32 @@ export default function ({ getService }: FtrProviderContext) { esArchiver, supertest ); - const createTests = (overwrite: boolean, spaceId: string) => { + const createTests = (overwrite: boolean, trueCopy: boolean, spaceId: string) => { + const singleRequest = true; + if (trueCopy) { + const cases = createTrueCopyTestCases(); + // The resolveImportErrors API doesn't actually have a flag for "trueCopy" mode; rather, we create test cases as if we are resolving + // errors from a call to the import API that had trueCopy mode enabled. + return createTestDefinitions(cases, false, { trueCopy, spaceId, singleRequest }); + } + const testCases = createTestCases(overwrite, spaceId); - return createTestDefinitions(testCases, false, overwrite, { - spaceId, - singleRequest: true, - }); + return createTestDefinitions(testCases, false, { overwrite, spaceId, singleRequest }); }; describe('_resolve_import_errors', () => { - getTestScenarios([false, true]).spaces.forEach(({ spaceId, modifier: overwrite }) => { - const suffix = overwrite ? ' with overwrite enabled' : ''; - const tests = createTests(overwrite!, spaceId); + getTestScenarios([ + [false, false], + [false, true], + [true, false], + ]).spaces.forEach(({ spaceId, modifier }) => { + const [overwrite, trueCopy] = modifier!; + const suffix = overwrite + ? ' with overwrite enabled' + : trueCopy + ? ' with trueCopy enabled' + : ''; + const tests = createTests(overwrite, trueCopy, spaceId); addTests(`within the ${spaceId} space${suffix}`, { spaceId, tests }); }); }); diff --git a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts index 300e8b4b0bab0..3dab8a4e7f244 100644 --- a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts +++ b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts @@ -331,7 +331,7 @@ export function resolveCopyToSpaceConflictsSuite( type, id: inexactMatchId, overwrite: true, - idToOverwrite: 'conflict_1_space_2', + destinationId: 'conflict_1_space_2', }), statusCode, response: async (response: TestResponse) => { @@ -352,7 +352,7 @@ export function resolveCopyToSpaceConflictsSuite( type, id: ambiguousConflictId, overwrite: true, - idToOverwrite: 'conflict_2_space_2', + destinationId: 'conflict_2_space_2', }), statusCode, response: async (response: TestResponse) => { From 2cbdb754a77fcab9d85b7a634e8c525ab54ee35e Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 29 Jun 2020 12:02:08 -0400 Subject: [PATCH 28/55] Clean up Jest integration tests Tests assertions are unchanged, jsut more concise now. --- .../routes/integration_tests/import.test.ts | 181 ++++-------- .../resolve_import_errors.test.ts | 270 +++++------------- 2 files changed, 114 insertions(+), 337 deletions(-) diff --git a/src/core/server/saved_objects/routes/integration_tests/import.test.ts b/src/core/server/saved_objects/routes/integration_tests/import.test.ts index 0ddecd4486aa8..dd967503cb152 100644 --- a/src/core/server/saved_objects/routes/integration_tests/import.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/import.test.ts @@ -28,22 +28,27 @@ import { SavedObjectsErrorHelpers } from '../..'; type setupServerReturn = UnwrapPromise>; const allowedTypes = ['index-pattern', 'visualization', 'dashboard']; -const config = { - maxImportPayloadBytes: 10485760, - maxImportExportSize: 10000, -} as SavedObjectConfig; +const config = { maxImportPayloadBytes: 10485760, maxImportExportSize: 10000 } as SavedObjectConfig; +const URL = '/internal/saved_objects/_import'; -describe('POST /internal/saved_objects/_import', () => { +describe(`POST ${URL}`, () => { let server: setupServerReturn['server']; let httpSetup: setupServerReturn['httpSetup']; let handlerContext: setupServerReturn['handlerContext']; let savedObjectsClient: ReturnType; - const emptyResponse = { - saved_objects: [], - total: 0, - per_page: 0, - page: 0, + const emptyResponse = { saved_objects: [], total: 0, per_page: 0, page: 0 }; + const mockIndexPattern = { + type: 'index-pattern', + id: 'my-pattern', + attributes: { title: 'my-pattern-*' }, + references: [], + }; + const mockDashboard = { + type: 'dashboard', + id: 'my-dashboard', + attributes: { title: 'Look at my dashboard' }, + references: [], }; beforeEach(async () => { @@ -68,7 +73,7 @@ describe('POST /internal/saved_objects/_import', () => { it('formats successful response', async () => { const result = await supertest(httpSetup.server.listener) - .post('/internal/saved_objects/_import') + .post(URL) .set('content-Type', 'multipart/form-data; boundary=BOUNDARY') .send( [ @@ -82,29 +87,15 @@ describe('POST /internal/saved_objects/_import', () => { ) .expect(200); - expect(result.body).toEqual({ - success: true, - successCount: 0, - }); + expect(result.body).toEqual({ success: true, successCount: 0 }); expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(0); }); it('defaults migrationVersion to empty object', async () => { - savedObjectsClient.bulkCreate.mockResolvedValueOnce({ - saved_objects: [ - { - type: 'index-pattern', - id: 'my-pattern', - attributes: { - title: 'my-pattern-*', - }, - references: [], - }, - ], - }); + savedObjectsClient.bulkCreate.mockResolvedValueOnce({ saved_objects: [mockIndexPattern] }); const result = await supertest(httpSetup.server.listener) - .post('/internal/saved_objects/_import') + .post(URL) .set('content-Type', 'multipart/form-data; boundary=EXAMPLE') .send( [ @@ -124,37 +115,21 @@ describe('POST /internal/saved_objects/_import', () => { successResults: [{ type: 'index-pattern', id: 'my-pattern' }], }); expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); - const firstBulkCreateCallArray = savedObjectsClient.bulkCreate.mock.calls[0][0]; - expect(firstBulkCreateCallArray).toHaveLength(1); - expect(firstBulkCreateCallArray[0].migrationVersion).toEqual({}); + expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( + [expect.objectContaining({ migrationVersion: {} })], + expect.anything() // options + ); }); it('imports an index pattern and dashboard, ignoring empty lines in the file', async () => { // NOTE: changes to this scenario should be reflected in the docs savedObjectsClient.bulkCreate.mockResolvedValueOnce({ - saved_objects: [ - { - type: 'index-pattern', - id: 'my-pattern', - attributes: { - title: 'my-pattern-*', - }, - references: [], - }, - { - type: 'dashboard', - id: 'my-dashboard', - attributes: { - title: 'Look at my dashboard', - }, - references: [], - }, - ], + saved_objects: [mockIndexPattern, mockDashboard], }); const result = await supertest(httpSetup.server.listener) - .post('/internal/saved_objects/_import') + .post(URL) .set('content-Type', 'multipart/form-data; boundary=EXAMPLE') .send( [ @@ -176,8 +151,8 @@ describe('POST /internal/saved_objects/_import', () => { success: true, successCount: 2, successResults: [ - { type: 'index-pattern', id: 'my-pattern' }, - { type: 'dashboard', id: 'my-dashboard' }, + { type: mockIndexPattern.type, id: mockIndexPattern.id }, + { type: mockDashboard.type, id: mockDashboard.id }, ], }); }); @@ -185,31 +160,15 @@ describe('POST /internal/saved_objects/_import', () => { it('imports an index pattern and dashboard but has a conflict on the index pattern', async () => { // NOTE: changes to this scenario should be reflected in the docs + const error = SavedObjectsErrorHelpers.createConflictError('index-pattern', 'my-pattern').output + .payload; savedObjectsClient.checkConflicts.mockResolvedValue({ - errors: [ - { - type: 'index-pattern', - id: 'my-pattern', - error: SavedObjectsErrorHelpers.createConflictError('index-pattern', 'my-pattern').output - .payload, - }, - ], - }); - savedObjectsClient.bulkCreate.mockResolvedValueOnce({ - saved_objects: [ - { - type: 'dashboard', - id: 'my-dashboard', - attributes: { - title: 'Look at my dashboard', - }, - references: [], - }, - ], + errors: [{ type: mockIndexPattern.type, id: mockIndexPattern.id, error }], }); + savedObjectsClient.bulkCreate.mockResolvedValueOnce({ saved_objects: [mockDashboard] }); const result = await supertest(httpSetup.server.listener) - .post('/internal/saved_objects/_import') + .post(URL) .set('content-Type', 'multipart/form-data; boundary=EXAMPLE') .send( [ @@ -227,15 +186,13 @@ describe('POST /internal/saved_objects/_import', () => { expect(result.body).toEqual({ success: false, successCount: 1, - successResults: [{ type: 'dashboard', id: 'my-dashboard' }], + successResults: [{ type: mockDashboard.type, id: mockDashboard.id }], errors: [ { - id: 'my-pattern', - type: 'index-pattern', - title: 'my-pattern-*', - error: { - type: 'conflict', - }, + id: mockIndexPattern.id, + type: mockIndexPattern.type, + title: mockIndexPattern.attributes.title, + error: { type: 'conflict' }, }, ], }); @@ -244,23 +201,16 @@ describe('POST /internal/saved_objects/_import', () => { it('imports a visualization with missing references', async () => { // NOTE: changes to this scenario should be reflected in the docs + const error = SavedObjectsErrorHelpers.createGenericNotFoundError( + 'index-pattern', + 'my-pattern-*' + ).output.payload; savedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: 'my-pattern-*', - type: 'index-pattern', - error: SavedObjectsErrorHelpers.createGenericNotFoundError( - 'index-pattern', - 'my-pattern-*' - ).output.payload, - references: [], - attributes: {}, - }, - ], + saved_objects: [{ ...mockIndexPattern, error }], }); const result = await supertest(httpSetup.server.listener) - .post('/internal/saved_objects/_import') + .post(URL) .set('content-Type', 'multipart/form-data; boundary=EXAMPLE') .send( [ @@ -285,47 +235,16 @@ describe('POST /internal/saved_objects/_import', () => { title: 'my-vis', error: { type: 'missing_references', - references: [ - { - type: 'index-pattern', - id: 'my-pattern-*', - }, - ], - blocking: [ - { - type: 'dashboard', - id: 'my-dashboard', - }, - ], + references: [{ type: 'index-pattern', id: 'my-pattern-*' }], + blocking: [{ type: 'dashboard', id: 'my-dashboard' }], }, }, ], }); - expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` -[MockFunction] { - "calls": Array [ - Array [ - Array [ - Object { - "fields": Array [ - "id", - ], - "id": "my-pattern-*", - "type": "index-pattern", - }, - ], - Object { - "namespace": undefined, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], -} -`); + expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.bulkGet).toHaveBeenCalledWith( + [{ fields: ['id'], id: 'my-pattern-*', type: 'index-pattern' }], + expect.anything() // options + ); }); }); diff --git a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts index 045abfa83fa82..ebf3e7f470f59 100644 --- a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts @@ -27,17 +27,34 @@ import { SavedObjectConfig } from '../../saved_objects_config'; type setupServerReturn = UnwrapPromise>; const allowedTypes = ['index-pattern', 'visualization', 'dashboard']; -const config = { - maxImportPayloadBytes: 10485760, - maxImportExportSize: 10000, -} as SavedObjectConfig; +const config = { maxImportPayloadBytes: 10485760, maxImportExportSize: 10000 } as SavedObjectConfig; +const URL = '/api/saved_objects/_resolve_import_errors'; -describe('POST /api/saved_objects/_resolve_import_errors', () => { +describe(`POST ${URL}`, () => { let server: setupServerReturn['server']; let httpSetup: setupServerReturn['httpSetup']; let handlerContext: setupServerReturn['handlerContext']; let savedObjectsClient: ReturnType; + const mockDashboard = { + type: 'dashboard', + id: 'my-dashboard', + attributes: { title: 'Look at my dashboard' }, + references: [], + }; + const mockVisualization = { + type: 'visualization', + id: 'my-vis', + attributes: { title: 'Look at my visualization' }, + references: [{ name: 'ref_0', type: 'index-pattern', id: 'existing' }], + }; + const mockIndexPattern = { + type: 'index-pattern', + id: 'existing', + attributes: {}, + references: [], + }; + beforeEach(async () => { ({ server, httpSetup, handlerContext } = await setupServer()); handlerContext.savedObjects.typeRegistry.getImportableAndExportableTypes.mockReturnValue( @@ -59,7 +76,7 @@ describe('POST /api/saved_objects/_resolve_import_errors', () => { it('formats successful response', async () => { const result = await supertest(httpSetup.server.listener) - .post('/api/saved_objects/_resolve_import_errors') + .post(URL) .set('content-Type', 'multipart/form-data; boundary=BOUNDARY') .send( [ @@ -82,21 +99,10 @@ describe('POST /api/saved_objects/_resolve_import_errors', () => { }); it('defaults migrationVersion to empty object', async () => { - savedObjectsClient.bulkCreate.mockResolvedValueOnce({ - saved_objects: [ - { - type: 'dashboard', - id: 'my-dashboard', - attributes: { - title: 'Look at my dashboard', - }, - references: [], - }, - ], - }); + savedObjectsClient.bulkCreate.mockResolvedValueOnce({ saved_objects: [mockDashboard] }); const result = await supertest(httpSetup.server.listener) - .post('/api/saved_objects/_resolve_import_errors') + .post(URL) .set('content-Type', 'multipart/form-data; boundary=EXAMPLE') .send( [ @@ -114,34 +120,21 @@ describe('POST /api/saved_objects/_resolve_import_errors', () => { ) .expect(200); - expect(result.body).toEqual({ - success: true, - successCount: 1, - successResults: [{ type: 'dashboard', id: 'my-dashboard' }], - }); - expect(savedObjectsClient.bulkCreate.mock.calls).toHaveLength(1); - const firstBulkCreateCallArray = savedObjectsClient.bulkCreate.mock.calls[0][0]; - expect(firstBulkCreateCallArray).toHaveLength(1); - expect(firstBulkCreateCallArray[0].migrationVersion).toEqual({}); + const { type, id } = mockDashboard; + expect(result.body).toEqual({ success: true, successCount: 1, successResults: [{ type, id }] }); + expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( + [expect.objectContaining({ migrationVersion: {} })], + expect.anything() // options + ); }); it('retries importing a dashboard', async () => { // NOTE: changes to this scenario should be reflected in the docs - savedObjectsClient.bulkCreate.mockResolvedValueOnce({ - saved_objects: [ - { - type: 'dashboard', - id: 'my-dashboard', - attributes: { - title: 'Look at my dashboard', - }, - references: [], - }, - ], - }); + savedObjectsClient.bulkCreate.mockResolvedValueOnce({ saved_objects: [mockDashboard] }); const result = await supertest(httpSetup.server.listener) - .post('/api/saved_objects/_resolve_import_errors') + .post(URL) .set('content-Type', 'multipart/form-data; boundary=EXAMPLE') .send( [ @@ -159,58 +152,21 @@ describe('POST /api/saved_objects/_resolve_import_errors', () => { ) .expect(200); - expect(result.body).toEqual({ - success: true, - successCount: 1, - successResults: [{ type: 'dashboard', id: 'my-dashboard' }], - }); - expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(` - [MockFunction] { - "calls": Array [ - Array [ - Array [ - Object { - "attributes": Object { - "title": "Look at my dashboard", - }, - "id": "my-dashboard", - "migrationVersion": Object {}, - "type": "dashboard", - }, - ], - Object { - "namespace": undefined, - "overwrite": undefined, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], - } - `); + const { type, id, attributes } = mockDashboard; + expect(result.body).toEqual({ success: true, successCount: 1, successResults: [{ type, id }] }); + expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( + [{ type, id, attributes, migrationVersion: {} }], + expect.objectContaining({ overwrite: undefined }) + ); }); it('resolves conflicts for dashboard', async () => { // NOTE: changes to this scenario should be reflected in the docs - savedObjectsClient.bulkCreate.mockResolvedValueOnce({ - saved_objects: [ - { - type: 'dashboard', - id: 'my-dashboard', - attributes: { - title: 'Look at my dashboard', - }, - references: [], - }, - ], - }); + savedObjectsClient.bulkCreate.mockResolvedValueOnce({ saved_objects: [mockDashboard] }); const result = await supertest(httpSetup.server.listener) - .post('/api/saved_objects/_resolve_import_errors') + .post(URL) .set('content-Type', 'multipart/form-data; boundary=EXAMPLE') .send( [ @@ -229,74 +185,26 @@ describe('POST /api/saved_objects/_resolve_import_errors', () => { ) .expect(200); + const { type, id, attributes } = mockDashboard; expect(result.body).toEqual({ success: true, successCount: 1, - successResults: [{ type: 'dashboard', id: 'my-dashboard' }], + successResults: [{ type, id }], }); - expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(` - [MockFunction] { - "calls": Array [ - Array [ - Array [ - Object { - "attributes": Object { - "title": "Look at my dashboard", - }, - "id": "my-dashboard", - "migrationVersion": Object {}, - "type": "dashboard", - }, - ], - Object { - "namespace": undefined, - "overwrite": true, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], - } - `); + expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( + [{ type, id, attributes, migrationVersion: {} }], + expect.objectContaining({ overwrite: true }) + ); }); it('resolves conflicts by replacing the visualization references', async () => { // NOTE: changes to this scenario should be reflected in the docs - savedObjectsClient.bulkCreate.mockResolvedValueOnce({ - saved_objects: [ - { - type: 'visualization', - id: 'my-vis', - attributes: { - title: 'Look at my visualization', - }, - references: [ - { - name: 'ref_0', - type: 'index-pattern', - id: 'existing', - }, - ], - }, - ], - }); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: 'existing', - type: 'index-pattern', - attributes: {}, - references: [], - }, - ], - }); + savedObjectsClient.bulkCreate.mockResolvedValueOnce({ saved_objects: [mockVisualization] }); + savedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [mockIndexPattern] }); const result = await supertest(httpSetup.server.listener) - .post('/api/saved_objects/_resolve_import_errors') + .post(URL) .set('content-Type', 'multipart/form-data; boundary=EXAMPLE') .send( [ @@ -314,71 +222,21 @@ describe('POST /api/saved_objects/_resolve_import_errors', () => { ) .expect(200); + const { type, id, attributes, references } = mockVisualization; expect(result.body).toEqual({ success: true, successCount: 1, successResults: [{ type: 'visualization', id: 'my-vis' }], }); - expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(` - [MockFunction] { - "calls": Array [ - Array [ - Array [ - Object { - "attributes": Object { - "title": "Look at my visualization", - }, - "id": "my-vis", - "migrationVersion": Object {}, - "references": Array [ - Object { - "id": "existing", - "name": "ref_0", - "type": "index-pattern", - }, - ], - "type": "visualization", - }, - ], - Object { - "namespace": undefined, - "overwrite": undefined, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], - } - `); - expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` - [MockFunction] { - "calls": Array [ - Array [ - Array [ - Object { - "fields": Array [ - "id", - ], - "id": "existing", - "type": "index-pattern", - }, - ], - Object { - "namespace": undefined, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], - } - `); + expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( + [{ type, id, attributes, references, migrationVersion: {} }], + expect.objectContaining({ overwrite: undefined }) + ); + expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.bulkGet).toHaveBeenCalledWith( + [{ fields: ['id'], id: 'existing', type: 'index-pattern' }], + expect.anything() // options + ); }); }); From f5c56cf5a1a3e7862201289798cd0bfe04c5d165 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 29 Jun 2020 13:41:34 -0400 Subject: [PATCH 29/55] Change outbound reference IDs when import IDs are regenerated Also updated Jest integration tests and added reference-specific test cases. Note that the X-Pack functional tests do not cover references. --- .../import/create_saved_objects.test.ts | 13 +++- .../import/create_saved_objects.ts | 35 ++++++---- .../routes/integration_tests/import.test.ts | 66 ++++++++++++++++++- .../resolve_import_errors.test.ts | 60 +++++++++++++++++ 4 files changed, 157 insertions(+), 17 deletions(-) diff --git a/src/core/server/saved_objects/import/create_saved_objects.test.ts b/src/core/server/saved_objects/import/create_saved_objects.test.ts index 8a60425636fa1..9c8a2d070b8bf 100644 --- a/src/core/server/saved_objects/import/create_saved_objects.test.ts +++ b/src/core/server/saved_objects/import/create_saved_objects.test.ts @@ -19,7 +19,6 @@ import { savedObjectsClientMock } from '../../mocks'; import { createSavedObjects } from './create_saved_objects'; -import { SavedObjectReference } from 'kibana/public'; import { SavedObjectsClientContract, SavedObject } from '../types'; import { SavedObjectsErrorHelpers } from '..'; import { extractErrors } from './extract_errors'; @@ -33,7 +32,11 @@ const createObject = (type: string, id: string, originId?: string): SavedObject type, id, attributes: {}, - references: (Symbol() as unknown) as SavedObjectReference[], + references: [ + { name: 'name-1', type: 'other-type', id: 'other-id' }, // object that is not present + { name: 'name-2', type: MULTI_NS_TYPE, id: 'id-1' }, // object that is present, but does not have an importIdMap entry + { name: 'name-3', type: MULTI_NS_TYPE, id: 'id-3' }, // object that is present and has an importIdMap entry + ], ...(originId && { originId }), }); @@ -91,7 +94,11 @@ describe('#createSavedObjects', () => { type, id: retry ? `new-id-for-${id}` : id, // if this was a retry, we regenerated the id -- this is mocked below attributes, - references, + references: [ + { name: 'name-1', type: 'other-type', id: 'other-id' }, // object that is not present + { name: 'name-2', type: MULTI_NS_TYPE, id: 'id-1' }, // object that is present, but does not have an importIdMap entry + { name: 'name-3', type: MULTI_NS_TYPE, id: 'id-foo' }, // object that is present and has an importIdMap entry + ], // if the import object had an originId, and/or if we regenerated the id, expect an originId to be included in the create args ...((originId || retry) && { originId: originId || id }), })); diff --git a/src/core/server/saved_objects/import/create_saved_objects.ts b/src/core/server/saved_objects/import/create_saved_objects.ts index 92441bbe6b1cb..e53f25baddb9b 100644 --- a/src/core/server/saved_objects/import/create_saved_objects.ts +++ b/src/core/server/saved_objects/import/create_saved_objects.ts @@ -52,17 +52,30 @@ export const createSavedObjects = async ( new Map>() ); - // use the import ID map to ensure that each object is being created with the correct ID also, ensure that the `originId` is set on the - // created object if it did not have one - const objectsToCreate = objects.map((object) => { - const importIdEntry = importIdMap.get(`${object.type}:${object.id}`); - if (importIdEntry) { - objectIdMap.set(`${object.type}:${importIdEntry.id}`, object); - const originId = importIdEntry.omitOriginId ? undefined : object.originId ?? object.id; - return { ...object, id: importIdEntry.id, originId }; - } - return object; - }); + const objectsToCreate = objects + .map((object) => { + // use the import ID map to ensure that each object is being created with the correct ID, also ensure that the `originId` is set on + // the created object if it did not have one (or is omitted if specified) + const importIdEntry = importIdMap.get(`${object.type}:${object.id}`); + if (importIdEntry) { + objectIdMap.set(`${object.type}:${importIdEntry.id}`, object); + const originId = importIdEntry.omitOriginId ? undefined : object.originId ?? object.id; + return { ...object, id: importIdEntry.id, originId }; + } + return object; + }) + .map((object) => { + // use the import ID map to ensure that each reference is being created with the correct ID + const references = object.references?.map((reference) => { + const { type, id } = reference; + const importIdEntry = importIdMap.get(`${type}:${id}`); + if (importIdEntry) { + return { ...reference, id: importIdEntry.id }; + } + return reference; + }); + return { ...object, ...(references && { references }) }; + }); const bulkCreateResponse = await savedObjectsClient.bulkCreate(objectsToCreate, { namespace, overwrite, diff --git a/src/core/server/saved_objects/routes/integration_tests/import.test.ts b/src/core/server/saved_objects/routes/integration_tests/import.test.ts index dd967503cb152..676d09d985d01 100644 --- a/src/core/server/saved_objects/routes/integration_tests/import.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/import.test.ts @@ -17,6 +17,7 @@ * under the License. */ +import { mockUuidv4 } from '../../import/__mocks__'; import supertest from 'supertest'; import { UnwrapPromise } from '@kbn/utility-types'; import { registerImportRoute } from '../import'; @@ -24,6 +25,7 @@ import { savedObjectsClientMock } from '../../../../../core/server/mocks'; import { SavedObjectConfig } from '../../saved_objects_config'; import { setupServer, createExportableType } from '../test_utils'; import { SavedObjectsErrorHelpers } from '../..'; +import { SavedObject } from '../../types'; type setupServerReturn = UnwrapPromise>; @@ -52,6 +54,8 @@ describe(`POST ${URL}`, () => { }; beforeEach(async () => { + mockUuidv4.mockReset(); + mockUuidv4.mockImplementation(() => jest.requireActual('uuidv4')); ({ server, httpSetup, handlerContext } = await setupServer()); handlerContext.savedObjects.typeRegistry.getImportableAndExportableTypes.mockReturnValue( allowedTypes.map(createExportableType) @@ -218,7 +222,7 @@ describe(`POST ${URL}`, () => { 'Content-Disposition: form-data; name="file"; filename="export.ndjson"', 'Content-Type: application/ndjson', '', - '{"type":"visualization","id":"my-vis","attributes":{"title":"my-vis"},"references":[{"name":"ref_0","type":"index-pattern","id":"my-pattern-*"}]}', + '{"type":"visualization","id":"my-vis","attributes":{"title":"my-vis"},"references":[{"name":"ref_0","type":"index-pattern","id":"my-pattern"}]}', '{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"},"references":[{"name":"ref_0","type":"visualization","id":"my-vis"}]}', '--EXAMPLE--', ].join('\r\n') @@ -235,7 +239,7 @@ describe(`POST ${URL}`, () => { title: 'my-vis', error: { type: 'missing_references', - references: [{ type: 'index-pattern', id: 'my-pattern-*' }], + references: [{ type: 'index-pattern', id: 'my-pattern' }], blocking: [{ type: 'dashboard', id: 'my-dashboard' }], }, }, @@ -243,8 +247,64 @@ describe(`POST ${URL}`, () => { }); expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1); expect(savedObjectsClient.bulkGet).toHaveBeenCalledWith( - [{ fields: ['id'], id: 'my-pattern-*', type: 'index-pattern' }], + [{ fields: ['id'], id: 'my-pattern', type: 'index-pattern' }], expect.anything() // options ); }); + + describe('trueCopy enabled', () => { + it('imports objects, regenerating all IDs/reference IDs present, and resetting all origin IDs', async () => { + mockUuidv4.mockReturnValueOnce('new-id-1').mockReturnValueOnce('new-id-2'); + savedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [mockIndexPattern] }); + savedObjectsClient.bulkCreate.mockResolvedValueOnce({ + saved_objects: [ + { type: 'visualization', id: 'new-id-1' } as SavedObject, + { type: 'dashboard', id: 'new-id-2' } as SavedObject, + ], + }); + + const result = await supertest(httpSetup.server.listener) + .post(`${URL}?trueCopy=true`) + .set('content-Type', 'multipart/form-data; boundary=EXAMPLE') + .send( + [ + '--EXAMPLE', + 'Content-Disposition: form-data; name="file"; filename="export.ndjson"', + 'Content-Type: application/ndjson', + '', + '{"type":"visualization","id":"my-vis","attributes":{"title":"my-vis"},"references":[{"name":"ref_0","type":"index-pattern","id":"my-pattern"}]}', + '{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"},"references":[{"name":"ref_0","type":"visualization","id":"my-vis"}]}', + '--EXAMPLE--', + ].join('\r\n') + ) + .expect(200); + + expect(result.body).toEqual({ + success: true, + successCount: 2, + successResults: [ + { type: 'visualization', id: 'my-vis', destinationId: 'new-id-1' }, + { type: 'dashboard', id: 'my-dashboard', destinationId: 'new-id-2' }, + ], + }); + expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( + [ + expect.objectContaining({ + type: 'visualization', + id: 'new-id-1', + references: [{ name: 'ref_0', type: 'index-pattern', id: 'my-pattern' }], + originId: undefined, + }), + expect.objectContaining({ + type: 'dashboard', + id: 'new-id-2', + references: [{ name: 'ref_0', type: 'visualization', id: 'new-id-1' }], + originId: undefined, + }), + ], + expect.anything() // options + ); + }); + }); }); diff --git a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts index ebf3e7f470f59..627a854daadd2 100644 --- a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts @@ -23,6 +23,7 @@ import { registerResolveImportErrorsRoute } from '../resolve_import_errors'; import { savedObjectsClientMock } from '../../../../../core/server/mocks'; import { setupServer, createExportableType } from '../test_utils'; import { SavedObjectConfig } from '../../saved_objects_config'; +import { SavedObject } from '../../types'; type setupServerReturn = UnwrapPromise>; @@ -239,4 +240,63 @@ describe(`POST ${URL}`, () => { expect.anything() // options ); }); + + describe('trueCopy enabled', () => { + it('imports objects, regenerating all IDs/reference IDs present, and resetting all origin IDs', async () => { + savedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [mockIndexPattern] }); + savedObjectsClient.bulkCreate.mockResolvedValueOnce({ + saved_objects: [ + { type: 'visualization', id: 'new-id-1' } as SavedObject, + { type: 'dashboard', id: 'new-id-2' } as SavedObject, + ], + }); + + const result = await supertest(httpSetup.server.listener) + .post(`${URL}?trueCopy=true`) + .set('content-Type', 'multipart/form-data; boundary=EXAMPLE') + .send( + [ + '--EXAMPLE', + 'Content-Disposition: form-data; name="file"; filename="export.ndjson"', + 'Content-Type: application/ndjson', + '', + '{"type":"visualization","id":"my-vis","attributes":{"title":"my-vis"},"references":[{"name":"ref_0","type":"index-pattern","id":"existing"}]}', + '{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"},"references":[{"name":"ref_0","type":"visualization","id":"my-vis"}]}', + '--EXAMPLE', + 'Content-Disposition: form-data; name="retries"', + '', + '[{"type":"visualization","id":"my-vis","destinationId":"new-id-1"},{"type":"dashboard","id":"my-dashboard","destinationId":"new-id-2"}]', + '--EXAMPLE--', + ].join('\r\n') + ) + .expect(200); + + expect(result.body).toEqual({ + success: true, + successCount: 2, + successResults: [ + { type: 'visualization', id: 'my-vis', destinationId: 'new-id-1' }, + { type: 'dashboard', id: 'my-dashboard', destinationId: 'new-id-2' }, + ], + }); + expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( + [ + expect.objectContaining({ + type: 'visualization', + id: 'new-id-1', + references: [{ name: 'ref_0', type: 'index-pattern', id: 'existing' }], + originId: undefined, + }), + expect.objectContaining({ + type: 'dashboard', + id: 'new-id-2', + references: [{ name: 'ref_0', type: 'visualization', id: 'new-id-1' }], + originId: undefined, + }), + ], + expect.anything() // options + ); + }); + }); }); From d30bdfef4875099ff278f59fbd2dcfde91ed4286 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 29 Jun 2020 13:51:20 -0400 Subject: [PATCH 30/55] Make Jest integration test more realistic --- .../routes/integration_tests/resolve_import_errors.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts index 627a854daadd2..994f302e340a2 100644 --- a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts @@ -260,12 +260,12 @@ describe(`POST ${URL}`, () => { 'Content-Disposition: form-data; name="file"; filename="export.ndjson"', 'Content-Type: application/ndjson', '', - '{"type":"visualization","id":"my-vis","attributes":{"title":"my-vis"},"references":[{"name":"ref_0","type":"index-pattern","id":"existing"}]}', + '{"type":"visualization","id":"my-vis","attributes":{"title":"my-vis"},"references":[{"name":"ref_0","type":"index-pattern","id":"my-pattern"}]}', '{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"},"references":[{"name":"ref_0","type":"visualization","id":"my-vis"}]}', '--EXAMPLE', 'Content-Disposition: form-data; name="retries"', '', - '[{"type":"visualization","id":"my-vis","destinationId":"new-id-1"},{"type":"dashboard","id":"my-dashboard","destinationId":"new-id-2"}]', + '[{"type":"visualization","id":"my-vis","destinationId":"new-id-1","replaceReferences":[{"type":"index-pattern","from":"my-pattern","to":"existing"}]},{"type":"dashboard","id":"my-dashboard","destinationId":"new-id-2"}]', '--EXAMPLE--', ].join('\r\n') ) From 7badeacf0e14a69465a5b1b007b9b331c4b9f1bd Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 29 Jun 2020 16:36:28 -0400 Subject: [PATCH 31/55] Change import APIs to not create any objects until errors resolved Old behavior allowed an import object to be created even if other objects errored. This can be problematic when inbound references are considered if the imported object's ID changed. Instead, we now only create objects if/when all resolvable errors have been addressed (conflict, ambiguous conflict, and missing references). --- .../import/create_saved_objects.test.ts | 50 ++++++++++++++++--- .../import/create_saved_objects.ts | 29 +++++++---- .../import/import_saved_objects.test.ts | 32 ++++++++++-- .../import/import_saved_objects.ts | 1 + .../import/resolve_import_errors.test.ts | 18 ++++++- .../import/resolve_import_errors.ts | 7 ++- .../routes/integration_tests/import.test.ts | 10 ++-- .../resolve_import_errors.test.ts | 12 ++--- 8 files changed, 124 insertions(+), 35 deletions(-) diff --git a/src/core/server/saved_objects/import/create_saved_objects.test.ts b/src/core/server/saved_objects/import/create_saved_objects.test.ts index 9c8a2d070b8bf..2996bd4c3e9fc 100644 --- a/src/core/server/saved_objects/import/create_saved_objects.test.ts +++ b/src/core/server/saved_objects/import/create_saved_objects.test.ts @@ -19,11 +19,11 @@ import { savedObjectsClientMock } from '../../mocks'; import { createSavedObjects } from './create_saved_objects'; -import { SavedObjectsClientContract, SavedObject } from '../types'; +import { SavedObjectsClientContract, SavedObject, SavedObjectsImportError } from '../types'; import { SavedObjectsErrorHelpers } from '..'; import { extractErrors } from './extract_errors'; -type CreateSavedObjectsOptions = Parameters[1]; +type CreateSavedObjectsOptions = Parameters[2]; /** * Function to create a realistic-looking import object given a type, ID, and optional originId @@ -161,7 +161,7 @@ describe('#createSavedObjects', () => { test('exits early if there are no objects to create', async () => { const options = setupOptions(); - const createSavedObjectsResult = await createSavedObjects([], options); + const createSavedObjectsResult = await createSavedObjects([], [], options); expect(bulkCreate).not.toHaveBeenCalled(); expect(createSavedObjectsResult).toEqual({ createdObjects: [], errors: [] }); }); @@ -188,11 +188,49 @@ describe('#createSavedObjects', () => { }); }; + describe('handles accumulated errors as expected', () => { + const resolvableErrors: SavedObjectsImportError[] = [ + { type: 'foo', id: 'foo-id', error: { type: 'conflict' } }, + { type: 'bar', id: 'bar-id', error: { type: 'ambiguous_conflict', destinations: [] } }, + { + type: 'baz', + id: 'baz-id', + error: { type: 'missing_references', references: [], blocking: [] }, + }, + ]; + const unresolvableErrors: SavedObjectsImportError[] = [ + { type: 'qux', id: 'qux-id', error: { type: 'unsupported_type' } }, + { type: 'quux', id: 'quux-id', error: { type: 'unknown', message: '', statusCode: 400 } }, + ]; + + test('does not call bulkCreate when resolvable errors are present', async () => { + for (const error of resolvableErrors) { + const options = setupOptions(); + await createSavedObjects(objs, [error], options); + expect(bulkCreate).not.toHaveBeenCalled(); + } + }); + + test('calls bulkCreate when unresolvable errors or no errors are present', async () => { + for (const error of unresolvableErrors) { + const options = setupOptions(); + setupMockResults(options); + await createSavedObjects(objs, [error], options); + expect(bulkCreate).toHaveBeenCalledTimes(1); + bulkCreate.mockClear(); + } + const options = setupOptions(); + setupMockResults(options); + await createSavedObjects(objs, [], options); + expect(bulkCreate).toHaveBeenCalledTimes(1); + }); + }); + const testBulkCreateObjects = async (namespace?: string) => { const options = setupOptions({ namespace }); setupMockResults(options); - await createSavedObjects(objs, options); + await createSavedObjects(objs, [], options); expect(bulkCreate).toHaveBeenCalledTimes(1); // these three objects are transformed before being created, because they are included in the `importIdMap` const x3 = { ...obj3, id: importId3, originId: undefined }; // this import object already has an originId, but the entry has omitOriginId=true @@ -206,7 +244,7 @@ describe('#createSavedObjects', () => { const options = setupOptions({ namespace, overwrite }); setupMockResults(options); - await createSavedObjects(objs, options); + await createSavedObjects(objs, [], options); expect(bulkCreate).toHaveBeenCalledTimes(1); expectBulkCreateArgs.options(1, options); }; @@ -214,7 +252,7 @@ describe('#createSavedObjects', () => { const options = setupOptions({ namespace }); setupMockResults(options); - const results = await createSavedObjects(objs, options); + const results = await createSavedObjects(objs, [], options); const resultSavedObjects = (await bulkCreate.mock.results[0].value).saved_objects; const [r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13] = resultSavedObjects; // these three results are transformed before being returned, because the bulkCreate attempt used different IDs for them diff --git a/src/core/server/saved_objects/import/create_saved_objects.ts b/src/core/server/saved_objects/import/create_saved_objects.ts index e53f25baddb9b..53965ddef7857 100644 --- a/src/core/server/saved_objects/import/create_saved_objects.ts +++ b/src/core/server/saved_objects/import/create_saved_objects.ts @@ -37,6 +37,7 @@ interface CreateSavedObjectsResult { */ export const createSavedObjects = async ( objects: Array>, + accumulatedErrors: SavedObjectsImportError[], options: CreateSavedObjectsOptions ): Promise> => { // exit early if there are no objects to create @@ -76,20 +77,26 @@ export const createSavedObjects = async ( }); return { ...object, ...(references && { references }) }; }); - const bulkCreateResponse = await savedObjectsClient.bulkCreate(objectsToCreate, { - namespace, - overwrite, - }); + + const resolvableErrors = ['conflict', 'ambiguous_conflict', 'missing_references']; + let expectedResults = objectsToCreate; + if (!accumulatedErrors.some(({ error: { type } }) => resolvableErrors.includes(type))) { + const bulkCreateResponse = await savedObjectsClient.bulkCreate(objectsToCreate, { + namespace, + overwrite, + }); + expectedResults = bulkCreateResponse.saved_objects; + } // remap results to reflect the object IDs that were submitted for import // this ensures that consumers understand the results - const remappedResults = bulkCreateResponse.saved_objects.map< - SavedObject & { destinationId?: string } - >((result) => { - const { id } = objectIdMap.get(`${result.type}:${result.id}`)!; - // also, include a `destinationId` field if the object create attempt was made with a different ID - return { ...result, id, ...(id !== result.id && { destinationId: result.id }) }; - }); + const remappedResults = expectedResults.map & { destinationId?: string }>( + (result) => { + const { id } = objectIdMap.get(`${result.type}:${result.id}`)!; + // also, include a `destinationId` field if the object create attempt was made with a different ID + return { ...result, id, ...(id !== result.id && { destinationId: result.id }) }; + } + ); return { createdObjects: remappedResults.filter((obj) => !obj.error), 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 226d38e8517e2..9367297e5e370 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 @@ -180,14 +180,23 @@ describe('#importSavedObjectsFromStream', () => { test('creates saved objects', async () => { const options = setupOptions(); const filteredObjects = [createObject()]; + const errors = [createError(), createError(), createError(), createError()]; + getMockFn(collectSavedObjects).mockResolvedValue({ + errors: [errors[0]], + collectedObjects: [], // doesn't matter + }); + getMockFn(validateReferences).mockResolvedValue({ + errors: [errors[1]], + filteredObjects: [], // doesn't matter + }); getMockFn(checkConflicts).mockResolvedValue({ - errors: [], + errors: [errors[2]], filteredObjects, importIdMap: new Map().set(`id1`, { id: `newId1` }), importIds: new Set(), }); getMockFn(checkOriginConflicts).mockResolvedValue({ - errors: [], + errors: [errors[3]], filteredObjects, importIdMap: new Map().set(`id2`, { id: `newId2` }), }); @@ -195,7 +204,11 @@ describe('#importSavedObjectsFromStream', () => { await importSavedObjectsFromStream(options); const importIdMap = new Map().set(`id1`, { id: `newId1` }).set(`id2`, { id: `newId2` }); const createSavedObjectsOptions = { savedObjectsClient, importIdMap, overwrite, namespace }; - expect(createSavedObjects).toHaveBeenCalledWith(filteredObjects, createSavedObjectsOptions); + expect(createSavedObjects).toHaveBeenCalledWith( + filteredObjects, + errors, + createSavedObjectsOptions + ); }); }); @@ -222,13 +235,22 @@ describe('#importSavedObjectsFromStream', () => { test('creates saved objects', async () => { const options = setupOptions(true); const filteredObjects = [createObject()]; - getMockFn(validateReferences).mockResolvedValue({ errors: [], filteredObjects }); + const errors = [createError(), createError()]; + getMockFn(collectSavedObjects).mockResolvedValue({ + errors: [errors[0]], + collectedObjects: [], // doesn't matter + }); + getMockFn(validateReferences).mockResolvedValue({ errors: [errors[1]], filteredObjects }); const importIdMap = new Map().set(`id1`, { id: `newId1` }); getMockFn(regenerateIds).mockReturnValue({ importIdMap }); await importSavedObjectsFromStream(options); const createSavedObjectsOptions = { savedObjectsClient, importIdMap, overwrite, namespace }; - expect(createSavedObjects).toHaveBeenCalledWith(filteredObjects, createSavedObjectsOptions); + expect(createSavedObjects).toHaveBeenCalledWith( + filteredObjects, + errors, + createSavedObjectsOptions + ); }); }); }); 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 81c135acf8940..5283f08dc56a1 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.ts @@ -101,6 +101,7 @@ export async function importSavedObjectsFromStream({ const createSavedObjectsOptions = { savedObjectsClient, importIdMap, overwrite, namespace }; const createSavedObjectsResult = await createSavedObjects( objectsToCreate, + errorAccumulator, createSavedObjectsOptions ); errorAccumulator = [...errorAccumulator, ...createSavedObjectsResult.errors]; 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 ac31bb2008bb8..7356c770909c6 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 @@ -236,8 +236,17 @@ describe('#importSavedObjectsFromStream', () => { test('creates saved objects', async () => { const options = setupOptions(); + const errors = [createError(), createError(), createError()]; + getMockFn(collectSavedObjects).mockResolvedValue({ + errors: [errors[0]], + collectedObjects: [], // doesn't matter + }); + getMockFn(validateReferences).mockResolvedValue({ + errors: [errors[1]], + filteredObjects: [], // doesn't matter + }); getMockFn(checkConflicts).mockResolvedValue({ - errors: [], + errors: [errors[2]], filteredObjects: [], importIdMap: new Map().set(`id1`, { id: `newId1` }), importIds: new Set(), @@ -247,16 +256,21 @@ describe('#importSavedObjectsFromStream', () => { const objectsToOverwrite = [createObject()]; const objectsToNotOverwrite = [createObject()]; getMockFn(splitOverwrites).mockReturnValue({ objectsToOverwrite, objectsToNotOverwrite }); + getMockFn(createSavedObjects).mockResolvedValueOnce({ + errors: [createError()], // this error will NOT be passed to the second `createSavedObjects` call + createdObjects: [], + }); await resolveSavedObjectsImportErrors(options); const createSavedObjectsOptions = { savedObjectsClient, importIdMap, namespace }; - expect(createSavedObjects).toHaveBeenNthCalledWith(1, objectsToOverwrite, { + expect(createSavedObjects).toHaveBeenNthCalledWith(1, objectsToOverwrite, errors, { ...createSavedObjectsOptions, overwrite: true, }); expect(createSavedObjects).toHaveBeenNthCalledWith( 2, objectsToNotOverwrite, + errors, createSavedObjectsOptions ); }); 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 4b1f112bb4f5c..2d7cddb73fd37 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.ts @@ -121,9 +121,14 @@ export async function resolveSavedObjectsImportErrors({ // Bulk create in two batches, overwrites and non-overwrites let successResults: Array<{ type: string; id: string; destinationId?: string }> = []; + const accumulatedErrors = [...errorAccumulator]; const bulkCreateObjects = async (objects: Array>, overwrite?: boolean) => { const options = { savedObjectsClient, importIdMap, namespace, overwrite }; - const { createdObjects, errors: bulkCreateErrors } = await createSavedObjects(objects, options); + const { createdObjects, errors: bulkCreateErrors } = await createSavedObjects( + objects, + accumulatedErrors, + options + ); errorAccumulator = [...errorAccumulator, ...bulkCreateErrors]; successCount += createdObjects.length; successResults = [ diff --git a/src/core/server/saved_objects/routes/integration_tests/import.test.ts b/src/core/server/saved_objects/routes/integration_tests/import.test.ts index 676d09d985d01..a9722c13c6563 100644 --- a/src/core/server/saved_objects/routes/integration_tests/import.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/import.test.ts @@ -92,7 +92,7 @@ describe(`POST ${URL}`, () => { .expect(200); expect(result.body).toEqual({ success: true, successCount: 0 }); - expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(0); + expect(savedObjectsClient.bulkCreate).not.toHaveBeenCalled(); // no objects were created }); it('defaults migrationVersion to empty object', async () => { @@ -118,7 +118,7 @@ describe(`POST ${URL}`, () => { successCount: 1, successResults: [{ type: 'index-pattern', id: 'my-pattern' }], }); - expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( [expect.objectContaining({ migrationVersion: {} })], expect.anything() // options @@ -159,6 +159,7 @@ describe(`POST ${URL}`, () => { { type: mockDashboard.type, id: mockDashboard.id }, ], }); + expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present }); it('imports an index pattern and dashboard but has a conflict on the index pattern', async () => { @@ -169,7 +170,6 @@ describe(`POST ${URL}`, () => { savedObjectsClient.checkConflicts.mockResolvedValue({ errors: [{ type: mockIndexPattern.type, id: mockIndexPattern.id, error }], }); - savedObjectsClient.bulkCreate.mockResolvedValueOnce({ saved_objects: [mockDashboard] }); const result = await supertest(httpSetup.server.listener) .post(URL) @@ -200,6 +200,7 @@ describe(`POST ${URL}`, () => { }, ], }); + expect(savedObjectsClient.bulkCreate).not.toHaveBeenCalled(); // successResults objects were not created because resolvable errors are present }); it('imports a visualization with missing references', async () => { @@ -250,6 +251,7 @@ describe(`POST ${URL}`, () => { [{ fields: ['id'], id: 'my-pattern', type: 'index-pattern' }], expect.anything() // options ); + expect(savedObjectsClient.bulkCreate).not.toHaveBeenCalled(); // no objects were created }); describe('trueCopy enabled', () => { @@ -287,7 +289,7 @@ describe(`POST ${URL}`, () => { { type: 'dashboard', id: 'my-dashboard', destinationId: 'new-id-2' }, ], }); - expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( [ expect.objectContaining({ diff --git a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts index 994f302e340a2..536ea874cfd9f 100644 --- a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts @@ -96,7 +96,7 @@ describe(`POST ${URL}`, () => { .expect(200); expect(result.body).toEqual({ success: true, successCount: 0 }); - expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(0); + expect(savedObjectsClient.bulkCreate).not.toHaveBeenCalled(); // no objects were created }); it('defaults migrationVersion to empty object', async () => { @@ -123,7 +123,7 @@ describe(`POST ${URL}`, () => { const { type, id } = mockDashboard; expect(result.body).toEqual({ success: true, successCount: 1, successResults: [{ type, id }] }); - expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( [expect.objectContaining({ migrationVersion: {} })], expect.anything() // options @@ -155,7 +155,7 @@ describe(`POST ${URL}`, () => { const { type, id, attributes } = mockDashboard; expect(result.body).toEqual({ success: true, successCount: 1, successResults: [{ type, id }] }); - expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( [{ type, id, attributes, migrationVersion: {} }], expect.objectContaining({ overwrite: undefined }) @@ -192,7 +192,7 @@ describe(`POST ${URL}`, () => { successCount: 1, successResults: [{ type, id }], }); - expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( [{ type, id, attributes, migrationVersion: {} }], expect.objectContaining({ overwrite: true }) @@ -229,7 +229,7 @@ describe(`POST ${URL}`, () => { successCount: 1, successResults: [{ type: 'visualization', id: 'my-vis' }], }); - expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( [{ type, id, attributes, references, migrationVersion: {} }], expect.objectContaining({ overwrite: undefined }) @@ -279,7 +279,7 @@ describe(`POST ${URL}`, () => { { type: 'dashboard', id: 'my-dashboard', destinationId: 'new-id-2' }, ], }); - expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( [ expect.objectContaining({ From 5e70f38d2450d0d85cfb410d64c2e4edcaae9b6d Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 30 Jun 2020 01:04:47 -0400 Subject: [PATCH 32/55] Fix return conflict errors when importing with unresolved errors The previous commit, which prevented any import objects from being created until all resolvable errors are addressed, introduced a bug that obfuscated regular conflict errors (when overwrite=false). Instead of being returned as errors, these cases were being returned as success results. This commit fixes the issue. It also updates integration tests to correctly identify which objects were created or not (based on whether or not the import operation had any resolvable errors). --- .../import/check_origin_conflicts.test.ts | 188 ++++++++++++------ .../import/check_origin_conflicts.ts | 17 +- .../import/import_saved_objects.test.ts | 1 + .../import/import_saved_objects.ts | 1 + .../common/suites/import.ts | 27 ++- .../security_and_spaces/apis/import.ts | 25 ++- .../security_only/apis/import.ts | 24 ++- .../spaces_only/apis/import.ts | 13 +- .../common/suites/copy_to_space.ts | 8 +- 9 files changed, 216 insertions(+), 88 deletions(-) diff --git a/src/core/server/saved_objects/import/check_origin_conflicts.test.ts b/src/core/server/saved_objects/import/check_origin_conflicts.test.ts index b8d0e024d2b85..51c9839fbb166 100644 --- a/src/core/server/saved_objects/import/check_origin_conflicts.test.ts +++ b/src/core/server/saved_objects/import/check_origin_conflicts.test.ts @@ -68,15 +68,16 @@ describe('#checkOriginConflicts', () => { options: { namespace?: string; importIds?: Set; + ignoreRegularConflicts?: boolean; } = {} ): CheckOriginConflictsOptions => { - const { namespace, importIds = new Set() } = options; + const { namespace, importIds = new Set(), ignoreRegularConflicts } = options; savedObjectsClient = savedObjectsClientMock.create(); find = savedObjectsClient.find; find.mockResolvedValue(getResultMock()); // mock zero hits response by default typeRegistry = typeRegistryMock.create(); typeRegistry.isMultiNamespace.mockImplementation((type) => type === MULTI_NS_TYPE); - return { savedObjectsClient, typeRegistry, namespace, importIds }; + return { savedObjectsClient, typeRegistry, namespace, ignoreRegularConflicts, importIds }; }; const mockFindResult = (...objects: SavedObjectType[]) => { @@ -172,6 +173,18 @@ describe('#checkOriginConflicts', () => { destinations: getAmbiguousConflicts(destinations), }, }); + const createConflictError = ( + object: SavedObjectType, + destinationId?: string + ): SavedObjectsImportError => ({ + type: object.type, + id: object.id, + title: object.attributes?.title, + error: { + type: 'conflict', + ...(destinationId && { destinationId }), + }, + }); describe('object result without a `importIdMap` entry (no match or exact match)', () => { test('returns object when no match is detected (0 hits)', async () => { @@ -246,30 +259,48 @@ describe('#checkOriginConflicts', () => { }); describe('object result with a `importIdMap` entry (partial match with a single destination)', () => { - test('returns object with a `importIdMap` entry when an inexact match is detected (1 hit)', async () => { + describe('when an inexact match is detected (1 hit)', () => { // objA and objB exist in this space // try to import obj1 and obj2 const obj1 = createObject(MULTI_NS_TYPE, 'id-1'); const objA = createObject(MULTI_NS_TYPE, 'id-A', obj1.id); const obj2 = createObject(MULTI_NS_TYPE, 'id-2', 'originId-foo'); const objB = createObject(MULTI_NS_TYPE, 'id-B', obj2.originId); - const options = setupOptions(); - mockFindResult(objA); // find for obj1: the result is an inexact match with one destination - mockFindResult(objB); // find for obj2: the result is an inexact match with one destination - const checkOriginConflictsResult = await checkOriginConflicts([obj1, obj2], options); - const expectedResult = { - filteredObjects: [obj1, obj2], - importIdMap: new Map([ - [`${obj1.type}:${obj1.id}`, { id: objA.id }], - [`${obj2.type}:${obj2.id}`, { id: objB.id }], - ]), - errors: [], + const setup = (ignoreRegularConflicts: boolean) => { + const options = setupOptions({ ignoreRegularConflicts }); + mockFindResult(objA); // find for obj1: the result is an inexact match with one destination + mockFindResult(objB); // find for obj2: the result is an inexact match with one destination + return options; }; - expect(checkOriginConflictsResult).toEqual(expectedResult); + + test('returns conflict error when ignoreRegularConflicts=false', async () => { + const options = setup(false); + const checkOriginConflictsResult = await checkOriginConflicts([obj1, obj2], options); + const expectedResult = { + filteredObjects: [], + importIdMap: new Map(), + errors: [createConflictError(obj1, objA.id), createConflictError(obj2, objB.id)], + }; + expect(checkOriginConflictsResult).toEqual(expectedResult); + }); + + test('returns object with a `importIdMap` entry when ignoreRegularConflicts=true', async () => { + const options = setup(true); + const checkOriginConflictsResult = await checkOriginConflicts([obj1, obj2], options); + const expectedResult = { + filteredObjects: [obj1, obj2], + importIdMap: new Map([ + [`${obj1.type}:${obj1.id}`, { id: objA.id }], + [`${obj2.type}:${obj2.id}`, { id: objB.id }], + ]), + errors: [], + }; + expect(checkOriginConflictsResult).toEqual(expectedResult); + }); }); - test('returns object with a `importIdMap` entry when an inexact match is detected (2+ hits), with n-1 destinations that are exactly matched by another object', async () => { + describe('when an inexact match is detected (2+ hits), with n-1 destinations that are exactly matched by another object', () => { // obj1, obj3, objA, and objB exist in this space // try to import obj1, obj2, obj3, and obj4; simulating a scenario where obj1 and obj3 were filtered out during `checkConflicts`, so we only call `checkOriginConflicts` with the remainder const obj1 = createObject(MULTI_NS_TYPE, 'id-1'); @@ -278,27 +309,46 @@ describe('#checkOriginConflicts', () => { const obj3 = createObject(MULTI_NS_TYPE, 'id-3', 'originId-foo'); const obj4 = createObject(MULTI_NS_TYPE, 'id-4', obj3.originId); const objB = createObject(MULTI_NS_TYPE, 'id-B', obj3.originId); - const options = setupOptions({ - importIds: new Set([ - `${obj1.type}:${obj1.id}`, - `${obj2.type}:${obj2.id}`, - `${obj3.type}:${obj3.id}`, - `${obj4.type}:${obj4.id}`, - ]), - }); - mockFindResult(obj1, objA); // find for obj2: the result is an inexact match with two destinations, but the first destination is exactly matched by obj1 so it is ignored -- accordingly, obj2 has an inexact match with one destination (objA) - mockFindResult(objB, obj3); // find for obj4: the result is an inexact match with two destinations, but the second destination is exactly matched by obj3 so it is ignored -- accordingly, obj4 has an inexact match with one destination (objB) - const checkOriginConflictsResult = await checkOriginConflicts([obj2, obj4], options); - const expectedResult = { - filteredObjects: [obj2, obj4], - importIdMap: new Map([ - [`${obj2.type}:${obj2.id}`, { id: objA.id }], - [`${obj4.type}:${obj4.id}`, { id: objB.id }], - ]), - errors: [], + const setup = (ignoreRegularConflicts: boolean) => { + const options = setupOptions({ + ignoreRegularConflicts, + importIds: new Set([ + `${obj1.type}:${obj1.id}`, + `${obj2.type}:${obj2.id}`, + `${obj3.type}:${obj3.id}`, + `${obj4.type}:${obj4.id}`, + ]), + }); + mockFindResult(obj1, objA); // find for obj2: the result is an inexact match with two destinations, but the first destination is exactly matched by obj1 so it is ignored -- accordingly, obj2 has an inexact match with one destination (objA) + mockFindResult(objB, obj3); // find for obj4: the result is an inexact match with two destinations, but the second destination is exactly matched by obj3 so it is ignored -- accordingly, obj4 has an inexact match with one destination (objB) + return options; }; - expect(checkOriginConflictsResult).toEqual(expectedResult); + + test('returns conflict error when ignoreRegularConflicts=false', async () => { + const options = setup(false); + const checkOriginConflictsResult = await checkOriginConflicts([obj2, obj4], options); + const expectedResult = { + filteredObjects: [], + importIdMap: new Map(), + errors: [createConflictError(obj2, objA.id), createConflictError(obj4, objB.id)], + }; + expect(checkOriginConflictsResult).toEqual(expectedResult); + }); + + test('returns object with a `importIdMap` entry when ignoreRegularConflicts=true', async () => { + const options = setup(true); + const checkOriginConflictsResult = await checkOriginConflicts([obj2, obj4], options); + const expectedResult = { + filteredObjects: [obj2, obj4], + importIdMap: new Map([ + [`${obj2.type}:${obj2.id}`, { id: objA.id }], + [`${obj4.type}:${obj4.id}`, { id: objB.id }], + ]), + errors: [], + }; + expect(checkOriginConflictsResult).toEqual(expectedResult); + }); }); }); @@ -398,7 +448,7 @@ describe('#checkOriginConflicts', () => { }); }); - test('returns mixed results', async () => { + describe('mixed results', () => { // obj3, objA, objB, objC, objD, and objE exist in this space // try to import obj1, obj2, obj3, obj4, obj5, obj6, and obj7; simulating a scenario where obj3 was filtered out during `checkConflicts`, so we only call `checkOriginConflicts` with the remainder // note: this test is non-exhaustive for different permutations of import objects and results, but prior tests exercise these more thoroughly @@ -417,28 +467,52 @@ describe('#checkOriginConflicts', () => { const objE = createObject(MULTI_NS_TYPE, 'id-E', obj7.id); const objects = [obj1, obj2, obj4, obj5, obj6, obj7, obj8]; const importIds = new Set([...objects, obj3].map(({ type, id }) => `${type}:${id}`)); - const options = setupOptions({ importIds }); - - // obj1 is a non-multi-namespace type, so it is skipped while searching - mockFindResult(); // find for obj2: the result is no match - mockFindResult(obj3); // find for obj4: the result is an inexact match with one destination that is exactly matched by obj3 so it is ignored -- accordingly, obj4 has no match - mockFindResult(objA); // find for obj5: the result is an inexact match with one destination - mockFindResult(objB, objC); // find for obj6: the result is an inexact match with two destinations - mockFindResult(objD, objE); // find for obj7: the result is an inexact match with two destinations - mockFindResult(objD, objE); // find for obj8: the result is an inexact match with two destinations - - const checkOriginConflictsResult = await checkOriginConflicts(objects, options); - const expectedResult = { - filteredObjects: [obj1, obj2, obj4, obj5, obj7, obj8], - importIdMap: new Map([ - [`${obj5.type}:${obj5.id}`, { id: objA.id }], - [`${obj7.type}:${obj7.id}`, { id: 'uuidv4', omitOriginId: true }], - [`${obj8.type}:${obj8.id}`, { id: 'uuidv4', omitOriginId: true }], - ]), - errors: [createAmbiguousConflictError(obj6, [objB, objC])], + + const setup = (ignoreRegularConflicts: boolean) => { + const options = setupOptions({ importIds, ignoreRegularConflicts }); + // obj1 is a non-multi-namespace type, so it is skipped while searching + mockFindResult(); // find for obj2: the result is no match + mockFindResult(obj3); // find for obj4: the result is an inexact match with one destination that is exactly matched by obj3 so it is ignored -- accordingly, obj4 has no match + mockFindResult(objA); // find for obj5: the result is an inexact match with one destination + mockFindResult(objB, objC); // find for obj6: the result is an inexact match with two destinations + mockFindResult(objD, objE); // find for obj7: the result is an inexact match with two destinations + mockFindResult(objD, objE); // find for obj8: the result is an inexact match with two destinations + return options; }; - expect(mockUuidv4).toHaveBeenCalledTimes(2); - expect(checkOriginConflictsResult).toEqual(expectedResult); + + test('returns errors for regular conflicts when ignoreRegularConflicts=false', async () => { + const options = setup(false); + const checkOriginConflictsResult = await checkOriginConflicts(objects, options); + const expectedResult = { + filteredObjects: [obj1, obj2, obj4, obj7, obj8], + importIdMap: new Map([ + [`${obj7.type}:${obj7.id}`, { id: 'uuidv4', omitOriginId: true }], + [`${obj8.type}:${obj8.id}`, { id: 'uuidv4', omitOriginId: true }], + ]), + errors: [ + createConflictError(obj5, objA.id), + createAmbiguousConflictError(obj6, [objB, objC]), + ], + }; + expect(mockUuidv4).toHaveBeenCalledTimes(2); + expect(checkOriginConflictsResult).toEqual(expectedResult); + }); + + test('does not return errors for regular conflicts when ignoreRegularConflicts=true', async () => { + const options = setup(true); + const checkOriginConflictsResult = await checkOriginConflicts(objects, options); + const expectedResult = { + filteredObjects: [obj1, obj2, obj4, obj5, obj7, obj8], + importIdMap: new Map([ + [`${obj5.type}:${obj5.id}`, { id: objA.id }], + [`${obj7.type}:${obj7.id}`, { id: 'uuidv4', omitOriginId: true }], + [`${obj8.type}:${obj8.id}`, { id: 'uuidv4', omitOriginId: true }], + ]), + errors: [createAmbiguousConflictError(obj6, [objB, objC])], + }; + expect(mockUuidv4).toHaveBeenCalledTimes(2); + expect(checkOriginConflictsResult).toEqual(expectedResult); + }); }); }); }); diff --git a/src/core/server/saved_objects/import/check_origin_conflicts.ts b/src/core/server/saved_objects/import/check_origin_conflicts.ts index 8244734f42070..34b9a534d5ddf 100644 --- a/src/core/server/saved_objects/import/check_origin_conflicts.ts +++ b/src/core/server/saved_objects/import/check_origin_conflicts.ts @@ -30,6 +30,7 @@ interface CheckOriginConflictsOptions { savedObjectsClient: SavedObjectsClientContract; typeRegistry: ISavedObjectTypeRegistry; namespace?: string; + ignoreRegularConflicts?: boolean; importIds: Set; } @@ -169,8 +170,20 @@ export async function checkOriginConflicts( const { type, id, attributes } = object; if (sources.length === 1 && destinations.length === 1) { // This is a simple "inexact match" result -- a single import object has a single destination conflict. - importIdMap.set(`${type}:${id}`, { id: destinations[0].id }); - filteredObjects.push(object); + if (options.ignoreRegularConflicts) { + importIdMap.set(`${type}:${id}`, { id: destinations[0].id }); + filteredObjects.push(object); + } else { + errors.push({ + type, + id, + title: attributes?.title, + error: { + type: 'conflict', + destinationId: destinations[0].id, + }, + }); + } return; } // This is an ambiguous conflict error, which is one of the following cases: 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 9367297e5e370..28e13e57c8a4b 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 @@ -169,6 +169,7 @@ describe('#importSavedObjectsFromStream', () => { savedObjectsClient, typeRegistry, namespace, + ignoreRegularConflicts: overwrite, importIds, }; expect(checkOriginConflicts).toHaveBeenCalledWith( 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 5283f08dc56a1..9018e8c8c8608 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.ts @@ -86,6 +86,7 @@ export async function importSavedObjectsFromStream({ savedObjectsClient, typeRegistry, namespace, + ignoreRegularConflicts: overwrite, importIds: checkConflictsResult.importIds, }; const checkOriginConflictsResult = await checkOriginConflicts( diff --git a/x-pack/test/saved_object_api_integration/common/suites/import.ts b/x-pack/test/saved_object_api_integration/common/suites/import.ts index 4173d64011ccf..7a73208feee67 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/import.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/import.ts @@ -75,6 +75,9 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe const expectResponseBody = ( testCases: ImportTestCase | ImportTestCase[], statusCode: 200 | 403, + singleRequest: boolean, + overwrite: boolean, + trueCopy: boolean, spaceId = SPACES.DEFAULT.spaceId ): ExpectResponseBody => async (response: Record) => { const testCaseArray = Array.isArray(testCases) ? testCases : [testCases]; @@ -117,13 +120,16 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe } else { expect(destinationId).to.be(undefined); } - const { _source } = await expectResponses.successCreated( - es, - spaceId, - type, - destinationId ?? id - ); - expect(_source[type][NEW_ATTRIBUTE_KEY]).to.eql(NEW_ATTRIBUTE_VAL); + if (!singleRequest || overwrite || trueCopy) { + // even if the object result was a "success" result, it may not have been created if other resolvable errors were returned + const { _source } = await expectResponses.successCreated( + es, + spaceId, + type, + destinationId ?? id + ); + expect(_source[type][NEW_ATTRIBUTE_KEY]).to.eql(NEW_ATTRIBUTE_VAL); + } } for (let i = 0; i < expectedFailures.length; i++) { const { type, id, failure, fail409Param, expectedNewId } = expectedFailures[i]; @@ -191,7 +197,9 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe title: getTestTitle(x, responseStatusCode), request: [createRequest(x)], responseStatusCode, - responseBody: responseBodyOverride || expectResponseBody(x, responseStatusCode, spaceId), + responseBody: + responseBodyOverride || + expectResponseBody(x, responseStatusCode, false, overwrite, trueCopy, spaceId), overwrite, trueCopy, })); @@ -203,7 +211,8 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe request: cases.map((x) => createRequest(x)), responseStatusCode, responseBody: - responseBodyOverride || expectResponseBody(cases, responseStatusCode, spaceId), + responseBodyOverride || + expectResponseBody(cases, responseStatusCode, true, overwrite, trueCopy, spaceId), overwrite, trueCopy, }, diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts index 06740dc11265c..680f73499da38 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts @@ -41,6 +41,7 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { // for each permitted (non-403) outcome, if failure !== undefined then we expect // to receive an error; otherwise, we expect to receive a success result const group1Importable = [ + // when overwrite=true, all of the objects in this group are created successfully, so we can check the created object attributes { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID), @@ -54,6 +55,7 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { const group1NonImportable = [{ ...CASES.HIDDEN, ...fail400() }]; const group1All = group1Importable.concat(group1NonImportable); const group2 = [ + // when overwrite=true, all of the objects in this group are created successfully, so we can check the created object attributes CASES.NEW_MULTI_NAMESPACE_OBJ, { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, @@ -72,18 +74,23 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { }, { ...CASES.CONFLICT_1A_OBJ, ...destinationId() }, // "ambiguous source" conflict which results in a new destination ID { ...CASES.CONFLICT_1B_OBJ, ...destinationId() }, // "ambiguous source" conflict which results in a new destination ID - { ...CASES.CONFLICT_2C_OBJ, ...ambiguousConflict('2c') }, // "ambiguous destination" conflict { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict ]; const group3 = [ + // when overwrite=true, all of the objects in this group are errors, so we cannot check the created object attributes + // grouping errors together simplifies the test suite code + { ...CASES.CONFLICT_2C_OBJ, ...ambiguousConflict('2c') }, // "ambiguous destination" conflict + ]; + const group4 = [ + // when overwrite=true, all of the objects in this group are created successfully, so we can check the created object attributes { ...CASES.CONFLICT_1_OBJ, ...fail409(!overwrite) }, // "exact match" conflict CASES.CONFLICT_1A_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match CASES.CONFLICT_1B_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match { ...CASES.CONFLICT_2C_OBJ, ...destinationId() }, // "ambiguous source and destination" conflict which results in a new destination ID { ...CASES.CONFLICT_2D_OBJ, ...destinationId() }, // "ambiguous source and destination" conflict which results in a new destination ID ]; - return { group1Importable, group1NonImportable, group1All, group2, group3 }; + return { group1Importable, group1NonImportable, group1All, group2, group3, group4 }; }; export default function ({ getService }: FtrProviderContext) { @@ -121,10 +128,14 @@ export default function ({ getService }: FtrProviderContext) { }; } - const { group1Importable, group1NonImportable, group1All, group2, group3 } = createTestCases( - overwrite, - spaceId - ); + const { + group1Importable, + group1NonImportable, + group1All, + group2, + group3, + group4, + } = createTestCases(overwrite, spaceId); return { unauthorized: [ createTestDefinitions(group1Importable, true, { overwrite, spaceId }), @@ -141,11 +152,13 @@ export default function ({ getService }: FtrProviderContext) { }), createTestDefinitions(group2, true, { overwrite, spaceId, singleRequest }), createTestDefinitions(group3, true, { overwrite, spaceId, singleRequest }), + createTestDefinitions(group4, true, { overwrite, spaceId, singleRequest }), ].flat(), authorized: [ createTestDefinitions(group1All, false, { overwrite, spaceId, singleRequest }), createTestDefinitions(group2, false, { overwrite, spaceId, singleRequest }), createTestDefinitions(group3, false, { overwrite, spaceId, singleRequest }), + createTestDefinitions(group4, false, { overwrite, spaceId, singleRequest }), ].flat(), }; }; diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/import.ts b/x-pack/test/saved_object_api_integration/security_only/apis/import.ts index 92bf9ccef9a11..bedd9398d3d58 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/import.ts @@ -35,6 +35,7 @@ const createTestCases = (overwrite: boolean) => { // for each permitted (non-403) outcome, if failure !== undefined then we expect // to receive an error; otherwise, we expect to receive a success result const group1Importable = [ + // when overwrite=true, all of the objects in this group are created successfully, so we can check the created object attributes { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(!overwrite) }, CASES.SINGLE_NAMESPACE_SPACE_1, CASES.SINGLE_NAMESPACE_SPACE_2, @@ -45,24 +46,30 @@ const createTestCases = (overwrite: boolean) => { const group1NonImportable = [{ ...CASES.HIDDEN, ...fail400() }]; const group1All = group1Importable.concat(group1NonImportable); const group2 = [ + // when overwrite=true, all of the objects in this group are created successfully, so we can check the created object attributes CASES.NEW_MULTI_NAMESPACE_OBJ, { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite) }, { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...destinationId() }, { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...destinationId() }, { ...CASES.CONFLICT_1A_OBJ, ...destinationId() }, // "ambiguous source" conflict which results in a new destination ID { ...CASES.CONFLICT_1B_OBJ, ...destinationId() }, // "ambiguous source" conflict which results in a new destination ID - { ...CASES.CONFLICT_2C_OBJ, ...ambiguousConflict('2c') }, // "ambiguous destination" conflict { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict ]; const group3 = [ + // when overwrite=true, all of the objects in this group are errors, so we cannot check the created object attributes + // grouping errors together simplifies the test suite code + { ...CASES.CONFLICT_2C_OBJ, ...ambiguousConflict('2c') }, // "ambiguous destination" conflict + ]; + const group4 = [ + // when overwrite=true, all of the objects in this group are created successfully, so we can check the created object attributes { ...CASES.CONFLICT_1_OBJ, ...fail409(!overwrite) }, // "exact match" conflict CASES.CONFLICT_1A_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match CASES.CONFLICT_1B_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match { ...CASES.CONFLICT_2C_OBJ, ...destinationId() }, // "ambiguous source and destination" conflict which results in a new destination ID { ...CASES.CONFLICT_2D_OBJ, ...destinationId() }, // "ambiguous source and destination" conflict which results in a new destination ID ]; - return { group1Importable, group1NonImportable, group1All, group2, group3 }; + return { group1Importable, group1NonImportable, group1All, group2, group3, group4 }; }; export default function ({ getService }: FtrProviderContext) { @@ -100,9 +107,14 @@ export default function ({ getService }: FtrProviderContext) { }; } - const { group1Importable, group1NonImportable, group1All, group2, group3 } = createTestCases( - overwrite - ); + const { + group1Importable, + group1NonImportable, + group1All, + group2, + group3, + group4, + } = createTestCases(overwrite); return { unauthorized: [ createTestDefinitions(group1Importable, true, { overwrite }), @@ -118,11 +130,13 @@ export default function ({ getService }: FtrProviderContext) { }), createTestDefinitions(group2, true, { overwrite, singleRequest }), createTestDefinitions(group3, true, { overwrite, singleRequest }), + createTestDefinitions(group4, true, { overwrite, singleRequest }), ].flat(), authorized: [ createTestDefinitions(group1All, false, { overwrite, singleRequest }), createTestDefinitions(group2, false, { overwrite, singleRequest }), createTestDefinitions(group3, false, { overwrite, singleRequest }), + createTestDefinitions(group4, false, { overwrite, singleRequest }), ].flat(), }; }; diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts index 1b38e68019feb..3b740fe54f197 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts @@ -36,6 +36,7 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { // for each outcome, if failure !== undefined then we expect to receive // an error; otherwise, we expect to receive a success result const group1 = [ + // when overwrite=true, all of the objects in this group are created successfully, so we can check the created object attributes { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID), @@ -61,7 +62,6 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { { ...CASES.HIDDEN, ...fail400() }, { ...CASES.CONFLICT_1A_OBJ, ...destinationId() }, // "ambiguous source" conflict which results in a new destination ID { ...CASES.CONFLICT_1B_OBJ, ...destinationId() }, // "ambiguous source" conflict which results in a new destination ID - { ...CASES.CONFLICT_2C_OBJ, ...ambiguousConflict('2c') }, // "ambiguous destination" conflict { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict CASES.NEW_SINGLE_NAMESPACE_OBJ, @@ -69,13 +69,19 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, ]; const group2 = [ + // when overwrite=true, all of the objects in this group are errors, so we cannot check the created object attributes + // grouping errors together simplifies the test suite code + { ...CASES.CONFLICT_2C_OBJ, ...ambiguousConflict('2c') }, // "ambiguous destination" conflict + ]; + const group3 = [ + // when overwrite=true, all of the objects in this group are created successfully, so we can check the created object attributes { ...CASES.CONFLICT_1_OBJ, ...fail409(!overwrite) }, // "exact match" conflict CASES.CONFLICT_1A_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match CASES.CONFLICT_1B_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match { ...CASES.CONFLICT_2C_OBJ, ...destinationId() }, // "ambiguous source and destination" conflict which results in a new destination ID { ...CASES.CONFLICT_2D_OBJ, ...destinationId() }, // "ambiguous source and destination" conflict which results in a new destination ID ]; - return { group1, group2 }; + return { group1, group2, group3 }; }; export default function ({ getService }: FtrProviderContext) { @@ -91,10 +97,11 @@ export default function ({ getService }: FtrProviderContext) { return createTestDefinitions(cases, false, { trueCopy, spaceId, singleRequest }); } - const { group1, group2 } = createTestCases(overwrite, spaceId); + const { group1, group2, group3 } = createTestCases(overwrite, spaceId); return [ createTestDefinitions(group1, false, { overwrite, spaceId, singleRequest }), createTestDefinitions(group2, false, { overwrite, spaceId, singleRequest }), + createTestDefinitions(group3, false, { overwrite, spaceId, singleRequest }), ].flat(); }; diff --git a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts index c1656fc6c8d3a..0386a2afccb3d 100644 --- a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts @@ -323,12 +323,8 @@ export function copyToSpaceTestSuiteFactory( }, } as CopyResponse); - // Query ES to ensure that we copied everything we expected - await assertSpaceCounts(destination, { - dashboard: 2, - visualization: 5, - 'index-pattern': 1, - }); + // Query ES to ensure that no objects were created + await assertSpaceCounts(destination, INITIAL_COUNTS[destination]); }; /** From 47590a6db1a70eea3ec4853bda4f6b8af6fec683 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 30 Jun 2020 11:57:02 -0400 Subject: [PATCH 33/55] Added `trueCopy` field to import success results and retries When True Copy mode is not enabled, but an ambiguous source conflict is detected, the source objects will be created as true copies. Before, we had no way for the consumer to differentiate these from other objects. Now, consumers can understand which objects are true copies (e.g., have an empty origin ID), and they can include this flag in retries so the object will be created correctly when resolving import errors. --- ...gin-core-public.savedobjectsimportretry.md | 1 + ...public.savedobjectsimportretry.truecopy.md | 16 ++++++ ...n-core-public.savedobjectsimportsuccess.md | 1 + ...blic.savedobjectsimportsuccess.truecopy.md | 16 ++++++ ...gin-core-server.savedobjectsimportretry.md | 1 + ...server.savedobjectsimportretry.truecopy.md | 16 ++++++ ...n-core-server.savedobjectsimportsuccess.md | 1 + ...rver.savedobjectsimportsuccess.truecopy.md | 16 ++++++ src/core/public/public.api.md | 4 ++ .../import/check_origin_conflicts.test.ts | 15 ++++-- .../import/check_origin_conflicts.ts | 3 +- .../import/import_saved_objects.test.ts | 50 ++++++++++++++----- .../import/import_saved_objects.ts | 9 +++- .../import/resolve_import_errors.test.ts | 9 ++-- .../import/resolve_import_errors.ts | 3 +- src/core/server/saved_objects/import/types.ts | 14 ++++++ .../routes/resolve_import_errors.ts | 1 + src/core/server/server.api.md | 4 ++ .../routes/api/external/copy_to_space.ts | 1 + .../common/suites/import.ts | 12 ++++- .../common/suites/resolve_import_errors.ts | 38 ++++++++++++-- .../security_and_spaces/apis/import.ts | 9 ++-- .../apis/resolve_import_errors.ts | 3 ++ .../security_only/apis/import.ts | 9 ++-- .../apis/resolve_import_errors.ts | 3 ++ .../spaces_only/apis/import.ts | 9 ++-- .../spaces_only/apis/resolve_import_errors.ts | 3 ++ 27 files changed, 226 insertions(+), 41 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.truecopy.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.truecopy.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.truecopy.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.truecopy.md diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.md index c72dd22a68739..148a7b146b47d 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.md @@ -20,5 +20,6 @@ export interface SavedObjectsImportRetry | [id](./kibana-plugin-core-public.savedobjectsimportretry.id.md) | string | | | [overwrite](./kibana-plugin-core-public.savedobjectsimportretry.overwrite.md) | boolean | | | [replaceReferences](./kibana-plugin-core-public.savedobjectsimportretry.replacereferences.md) | Array<{
type: string;
from: string;
to: string;
}> | | +| [trueCopy](./kibana-plugin-core-public.savedobjectsimportretry.truecopy.md) | boolean | | | [type](./kibana-plugin-core-public.savedobjectsimportretry.type.md) | string | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.truecopy.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.truecopy.md new file mode 100644 index 0000000000000..df0be400f15f1 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.truecopy.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportRetry](./kibana-plugin-core-public.savedobjectsimportretry.md) > [trueCopy](./kibana-plugin-core-public.savedobjectsimportretry.truecopy.md) + +## SavedObjectsImportRetry.trueCopy property + +> Warning: This API is now obsolete. +> +> If `trueCopy` is specified, the new object has a new (undefined) origin ID. This is only needed for the case where True Copy mode is disabled and ambiguous source conflicts are detected. When True Copy mode is permanently enabled, this field will be redundant and can be removed. +> + +Signature: + +```typescript +trueCopy?: boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.md index 407e1d9c5efdd..92e6855e2f201 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.md @@ -18,5 +18,6 @@ export interface SavedObjectsImportSuccess | --- | --- | --- | | [destinationId](./kibana-plugin-core-public.savedobjectsimportsuccess.destinationid.md) | string | If destinationId is specified, the new object has a new ID that is different from the import ID. | | [id](./kibana-plugin-core-public.savedobjectsimportsuccess.id.md) | string | | +| [trueCopy](./kibana-plugin-core-public.savedobjectsimportsuccess.truecopy.md) | boolean | | | [type](./kibana-plugin-core-public.savedobjectsimportsuccess.type.md) | string | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.truecopy.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.truecopy.md new file mode 100644 index 0000000000000..6123830a65c5f --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.truecopy.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportSuccess](./kibana-plugin-core-public.savedobjectsimportsuccess.md) > [trueCopy](./kibana-plugin-core-public.savedobjectsimportsuccess.truecopy.md) + +## SavedObjectsImportSuccess.trueCopy property + +> Warning: This API is now obsolete. +> +> If `trueCopy` is specified, the new object has a new (undefined) origin ID. This is only needed for the case where True Copy mode is disabled and ambiguous source conflicts are detected. When True Copy mode is permanently enabled, this field will be redundant and can be removed. +> + +Signature: + +```typescript +trueCopy?: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.md index 73c416696b51f..4053ec49961a2 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.md @@ -20,5 +20,6 @@ export interface SavedObjectsImportRetry | [id](./kibana-plugin-core-server.savedobjectsimportretry.id.md) | string | | | [overwrite](./kibana-plugin-core-server.savedobjectsimportretry.overwrite.md) | boolean | | | [replaceReferences](./kibana-plugin-core-server.savedobjectsimportretry.replacereferences.md) | Array<{
type: string;
from: string;
to: string;
}> | | +| [trueCopy](./kibana-plugin-core-server.savedobjectsimportretry.truecopy.md) | boolean | | | [type](./kibana-plugin-core-server.savedobjectsimportretry.type.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.truecopy.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.truecopy.md new file mode 100644 index 0000000000000..03e14f7e3cf59 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.truecopy.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportRetry](./kibana-plugin-core-server.savedobjectsimportretry.md) > [trueCopy](./kibana-plugin-core-server.savedobjectsimportretry.truecopy.md) + +## SavedObjectsImportRetry.trueCopy property + +> Warning: This API is now obsolete. +> +> If `trueCopy` is specified, the new object has a new (undefined) origin ID. This is only needed for the case where True Copy mode is disabled and ambiguous source conflicts are detected. When True Copy mode is permanently enabled, this field will be redundant and can be removed. +> + +Signature: + +```typescript +trueCopy?: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.md index 9f5bc40cb8c7c..b511ae3635ff0 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.md @@ -18,5 +18,6 @@ export interface SavedObjectsImportSuccess | --- | --- | --- | | [destinationId](./kibana-plugin-core-server.savedobjectsimportsuccess.destinationid.md) | string | If destinationId is specified, the new object has a new ID that is different from the import ID. | | [id](./kibana-plugin-core-server.savedobjectsimportsuccess.id.md) | string | | +| [trueCopy](./kibana-plugin-core-server.savedobjectsimportsuccess.truecopy.md) | boolean | | | [type](./kibana-plugin-core-server.savedobjectsimportsuccess.type.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.truecopy.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.truecopy.md new file mode 100644 index 0000000000000..9e9edc92d58b5 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.truecopy.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportSuccess](./kibana-plugin-core-server.savedobjectsimportsuccess.md) > [trueCopy](./kibana-plugin-core-server.savedobjectsimportsuccess.truecopy.md) + +## SavedObjectsImportSuccess.trueCopy property + +> Warning: This API is now obsolete. +> +> If `trueCopy` is specified, the new object has a new (undefined) origin ID. This is only needed for the case where True Copy mode is disabled and ambiguous source conflicts are detected. When True Copy mode is permanently enabled, this field will be redundant and can be removed. +> + +Signature: + +```typescript +trueCopy?: boolean; +``` diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index d87aaae3aa6ba..49f92d107e7ce 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -1392,6 +1392,8 @@ export interface SavedObjectsImportRetry { from: string; to: string; }>; + // @deprecated (undocumented) + trueCopy?: boolean; // (undocumented) type: string; } @@ -1401,6 +1403,8 @@ export interface SavedObjectsImportSuccess { destinationId?: string; // (undocumented) id: string; + // @deprecated (undocumented) + trueCopy?: boolean; // (undocumented) type: string; } diff --git a/src/core/server/saved_objects/import/check_origin_conflicts.test.ts b/src/core/server/saved_objects/import/check_origin_conflicts.test.ts index 51c9839fbb166..05d08a6f8a1e2 100644 --- a/src/core/server/saved_objects/import/check_origin_conflicts.test.ts +++ b/src/core/server/saved_objects/import/check_origin_conflicts.test.ts @@ -527,10 +527,10 @@ describe('#getImportIdMapForRetries', () => { const createRetry = ( { type, id }: { type: string; id: string }, - options: { destinationId?: string } = {} + options: { destinationId?: string; trueCopy?: boolean } = {} ): SavedObjectsImportRetry => { - const { destinationId } = options; - return { type, id, overwrite: false, destinationId, replaceReferences: [] }; + const { destinationId, trueCopy } = options; + return { type, id, overwrite: false, destinationId, replaceReferences: [], trueCopy }; }; test('throws an error if retry is not found for an object', async () => { @@ -548,17 +548,22 @@ describe('#getImportIdMapForRetries', () => { const obj1 = createObject('type-1', 'id-1'); const obj2 = createObject('type-2', 'id-2'); const obj3 = createObject('type-3', 'id-3'); - const objects = [obj1, obj2, obj3]; + const obj4 = createObject('type-4', 'id-4'); + const objects = [obj1, obj2, obj3, obj4]; const retries = [ createRetry(obj1), // retries that do not have `destinationId` specified are ignored createRetry(obj2, { destinationId: obj2.id }), // retries that have `id` that matches `destinationId` are ignored createRetry(obj3, { destinationId: 'id-X' }), // this retry will get added to the `importIdMap`! + createRetry(obj4, { destinationId: 'id-Y', trueCopy: true }), // this retry will get added to the `importIdMap`! ]; const options = setupOptions(retries); const checkOriginConflictsResult = await getImportIdMapForRetries(objects, options); expect(checkOriginConflictsResult).toEqual( - new Map([[`${obj3.type}:${obj3.id}`, { id: 'id-X', omitOriginId: false }]]) + new Map([ + [`${obj3.type}:${obj3.id}`, { id: 'id-X', omitOriginId: false }], + [`${obj4.type}:${obj4.id}`, { id: 'id-Y', omitOriginId: true }], + ]) ); }); diff --git a/src/core/server/saved_objects/import/check_origin_conflicts.ts b/src/core/server/saved_objects/import/check_origin_conflicts.ts index 34b9a534d5ddf..04364b367904b 100644 --- a/src/core/server/saved_objects/import/check_origin_conflicts.ts +++ b/src/core/server/saved_objects/import/check_origin_conflicts.ts @@ -236,8 +236,9 @@ export function getImportIdMapForRetries( throw new Error(`Retry was expected for "${type}:${id}" but not found`); } const { destinationId } = retry; + const omitOriginId = trueCopy || Boolean(retry.trueCopy); if (destinationId && destinationId !== id) { - importIdMap.set(`${type}:${id}`, { id: destinationId, omitOriginId: trueCopy }); + importIdMap.set(`${type}:${id}`, { id: destinationId, omitOriginId }); } }); 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 28e13e57c8a4b..23271cc0248c3 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 @@ -273,20 +273,46 @@ describe('#importSavedObjectsFromStream', () => { expect(result).toEqual({ success: false, successCount: 0, errors }); }); - test('handles a mix of successes and errors', async () => { - const options = setupOptions(); - const errors = [createError()]; + describe('handles a mix of successes and errors', () => { const obj1 = createObject(); - const obj2 = { ...createObject(), destinationId: 'some-destinationId' }; - getMockFn(createSavedObjects).mockResolvedValue({ errors, createdObjects: [obj1, obj2] }); + const tmp = createObject(); + const obj2 = { ...tmp, destinationId: 'some-destinationId', originId: tmp.id }; + const obj3 = { ...createObject(), destinationId: 'another-destinationId' }; // empty originId + const createdObjects = [obj1, obj2, obj3]; + const errors = [createError()]; - const result = await importSavedObjectsFromStream(options); - // successResults only includes the imported object's type, id, and destinationId (if a new one was generated) - const successResults = [ - { type: obj1.type, id: obj1.id }, - { type: obj2.type, id: obj2.id, destinationId: 'some-destinationId' }, - ]; - expect(result).toEqual({ success: false, successCount: 2, successResults, errors }); + test('with trueCopy disabled', async () => { + const options = setupOptions(); + getMockFn(createSavedObjects).mockResolvedValue({ errors, createdObjects }); + + const result = await importSavedObjectsFromStream(options); + // successResults only includes the imported object's type, id, and destinationId (if a new one was generated) + const successResults = [ + { type: obj1.type, id: obj1.id }, + { type: obj2.type, id: obj2.id, destinationId: obj2.destinationId }, + // trueCopy mode is not enabled, but obj3 ran into an ambiguous source conflict and it was created with an empty originId; hence, + // this specific object is a true copy -- we would need this information for rendering the appropriate originId in the client UI, + // and we would need it to construct a retry for this object if other objects had errors that needed to be resolved + { type: obj3.type, id: obj3.id, destinationId: obj3.destinationId, trueCopy: true }, + ]; + expect(result).toEqual({ success: false, successCount: 3, successResults, errors }); + }); + + test('with trueCopy enabled', async () => { + // however, we include it here for posterity + const options = setupOptions(true); + getMockFn(createSavedObjects).mockResolvedValue({ errors, createdObjects }); + + const result = await importSavedObjectsFromStream(options); + // successResults only includes the imported object's type, id, and destinationId (if a new one was generated) + const successResults = [ + { type: obj1.type, id: obj1.id }, + // obj2 being created with trueCopy mode enabled isn't a realistic test case (all objects would have originId omitted) + { type: obj2.type, id: obj2.id, destinationId: obj2.destinationId }, + { type: obj3.type, id: obj3.id, destinationId: obj3.destinationId }, + ]; + expect(result).toEqual({ success: false, successCount: 3, successResults, errors }); + }); }); test('accumulates multiple errors', async () => { 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 9018e8c8c8608..bd18513c8d10d 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.ts @@ -108,8 +108,13 @@ export async function importSavedObjectsFromStream({ errorAccumulator = [...errorAccumulator, ...createSavedObjectsResult.errors]; const successResults = createSavedObjectsResult.createdObjects.map( - ({ type, id, destinationId }) => { - return { type, id, ...(destinationId && { destinationId }) }; + ({ type, id, destinationId, originId }) => { + return { + type, + id, + ...(destinationId && { destinationId }), + ...(destinationId && !originId && !trueCopy && { trueCopy: true }), + }; } ); 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 7356c770909c6..3f82f528159d8 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 @@ -297,14 +297,16 @@ describe('#importSavedObjectsFromStream', () => { const options = setupOptions(); const errors = [createError()]; const obj1 = createObject(); - const obj2 = { ...createObject(), destinationId: 'some-destinationId' }; + const tmp = createObject(); + const obj2 = { ...tmp, destinationId: 'some-destinationId', originId: tmp.id }; + const obj3 = { ...createObject(), destinationId: 'another-destinationId' }; // empty originId; this is a true copy getMockFn(createSavedObjects).mockResolvedValueOnce({ errors, createdObjects: [obj1], }); getMockFn(createSavedObjects).mockResolvedValueOnce({ errors: [], - createdObjects: [obj2], + createdObjects: [obj2, obj3], }); const result = await resolveSavedObjectsImportErrors(options); @@ -312,8 +314,9 @@ describe('#importSavedObjectsFromStream', () => { const successResults = [ { type: obj1.type, id: obj1.id }, { type: obj2.type, id: obj2.id, destinationId: obj2.destinationId }, + { type: obj3.type, id: obj3.id, destinationId: obj3.destinationId, trueCopy: true }, ]; - expect(result).toEqual({ success: false, successCount: 2, successResults, errors }); + expect(result).toEqual({ success: false, successCount: 3, successResults, errors }); }); test('accumulates multiple errors', async () => { 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 2d7cddb73fd37..2592fa8a28a6d 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.ts @@ -133,10 +133,11 @@ export async function resolveSavedObjectsImportErrors({ successCount += createdObjects.length; successResults = [ ...successResults, - ...createdObjects.map(({ type, id, destinationId }) => ({ + ...createdObjects.map(({ type, id, destinationId, originId }) => ({ type, id, ...(destinationId && { destinationId }), + ...(destinationId && !originId && !trueCopy && { trueCopy: true }), })), ]; }; diff --git a/src/core/server/saved_objects/import/types.ts b/src/core/server/saved_objects/import/types.ts index fb32e7943e2bd..aa8479d4874de 100644 --- a/src/core/server/saved_objects/import/types.ts +++ b/src/core/server/saved_objects/import/types.ts @@ -38,6 +38,13 @@ export interface SavedObjectsImportRetry { from: string; to: string; }>; + /** + * @deprecated + * If `trueCopy` is specified, the new object has a new (undefined) origin ID. This is only needed for the case where True Copy mode is + * disabled and ambiguous source conflicts are detected. When True Copy mode is permanently enabled, this field will be redundant and can + * be removed. + */ + trueCopy?: boolean; } /** @@ -119,6 +126,13 @@ export interface SavedObjectsImportSuccess { * If `destinationId` is specified, the new object has a new ID that is different from the import ID. */ destinationId?: string; + /** + * @deprecated + * If `trueCopy` is specified, the new object has a new (undefined) origin ID. This is only needed for the case where True Copy mode is + * disabled and ambiguous source conflicts are detected. When True Copy mode is permanently enabled, this field will be redundant and can + * be removed. + */ + trueCopy?: boolean; } /** diff --git a/src/core/server/saved_objects/routes/resolve_import_errors.ts b/src/core/server/saved_objects/routes/resolve_import_errors.ts index 9f0a674bdd25e..7cdfb3e571e19 100644 --- a/src/core/server/saved_objects/routes/resolve_import_errors.ts +++ b/src/core/server/saved_objects/routes/resolve_import_errors.ts @@ -64,6 +64,7 @@ export const registerResolveImportErrorsRoute = (router: IRouter, config: SavedO }), { defaultValue: [] } ), + trueCopy: schema.maybe(schema.boolean()), }) ), }), diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index e895a07edd9ed..3b078e5c2d252 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2267,6 +2267,8 @@ export interface SavedObjectsImportRetry { from: string; to: string; }>; + // @deprecated (undocumented) + trueCopy?: boolean; // (undocumented) type: string; } @@ -2276,6 +2278,8 @@ export interface SavedObjectsImportSuccess { destinationId?: string; // (undocumented) id: string; + // @deprecated (undocumented) + trueCopy?: boolean; // (undocumented) type: string; } diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts index 88d71ed9cb0e4..9161d574cddee 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts @@ -123,6 +123,7 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { id: schema.string(), overwrite: schema.boolean({ defaultValue: false }), destinationId: schema.maybe(schema.string()), + trueCopy: schema.maybe(schema.boolean()), }) ) ), diff --git a/x-pack/test/saved_object_api_integration/common/suites/import.ts b/x-pack/test/saved_object_api_integration/common/suites/import.ts index 7a73208feee67..58941cc16b07a 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/import.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/import.ts @@ -114,12 +114,22 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe // the new ID was randomly generated expect(destinationId).to.match(/^[0-9a-f-]{36}$/); } - } else if (successParam === 'trueCopy') { + } else if (successParam === 'trueCopy' || successParam === 'newOrigin') { // the new ID was randomly generated expect(destinationId).to.match(/^[0-9a-f-]{36}$/); } else { expect(destinationId).to.be(undefined); } + + // This assertion is only needed for the case where True Copy mode is disabled and ambiguous source conflicts are detected. When + // True Copy mode is permanently enabled, this field will be removed, and this assertion will be redundant and can be removed too. + const resultTrueCopy = object!.trueCopy as boolean | undefined; + if (successParam === 'newOrigin') { + expect(resultTrueCopy).to.be(true); + } else { + expect(resultTrueCopy).to.be(undefined); + } + if (!singleRequest || overwrite || trueCopy) { // even if the object result was a "success" result, it may not have been created if other resolvable errors were returned const { _source } = await expectResponses.successCreated( diff --git a/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts index f0e4dfe8e3ccc..52b5610a195e5 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts @@ -36,9 +36,21 @@ const NEW_ATTRIBUTE_VAL = `New attribute value ${Date.now()}`; // * id: conflict_2b, originId: conflict_2 // * id: conflict_3 // * id: conflict_4a, originId: conflict_4 -// using the three conflict test case objects below, we can exercise various permutations of exact/inexact/ambiguous conflict scenarios +// using the five conflict test case objects below, we can exercise various permutations of exact/inexact/ambiguous conflict scenarios export const TEST_CASES = Object.freeze({ ...CASES, + CONFLICT_1A_OBJ: Object.freeze({ + type: 'sharedtype', + id: `conflict_1a`, + originId: `conflict_1`, + expectedNewId: 'some-random-id', + }), + CONFLICT_1B_OBJ: Object.freeze({ + type: 'sharedtype', + id: `conflict_1b`, + originId: `conflict_1`, + expectedNewId: 'another-random-id', + }), CONFLICT_2C_OBJ: Object.freeze({ type: 'sharedtype', id: `conflict_2c`, @@ -62,11 +74,19 @@ export const TEST_CASES = Object.freeze({ * Test cases have additional properties that we don't want to send in HTTP Requests */ const createRequest = ( - { type, id, originId, expectedNewId }: ResolveImportErrorsTestCase, + { type, id, originId, expectedNewId, successParam }: ResolveImportErrorsTestCase, overwrite: boolean ): ResolveImportErrorsTestDefinition['request'] => ({ objects: [{ type, id, ...(originId && { originId }) }], - retries: [{ type, id, overwrite, ...(expectedNewId && { destinationId: expectedNewId }) }], + retries: [ + { + type, + id, + overwrite, + ...(expectedNewId && { destinationId: expectedNewId }), + ...(successParam === 'newOrigin' && { trueCopy: true }), + }, + ], }); export function resolveImportErrorsTestSuiteFactory( @@ -114,11 +134,21 @@ export function resolveImportErrorsTestSuiteFactory( // the new ID was randomly generated expect(destinationId).to.match(/^[0-9a-f-]{36}$/); } - } else if (successParam === 'trueCopy') { + } else if (successParam === 'trueCopy' || successParam === 'newOrigin') { expect(destinationId).to.be(expectedNewId!); } else { expect(destinationId).to.be(undefined); } + + // This assertion is only needed for the case where True Copy mode is disabled and ambiguous source conflicts are detected. When + // True Copy mode is permanently enabled, this field will be removed, and this assertion will be redundant and can be removed too. + const resultTrueCopy = object!.trueCopy as boolean | undefined; + if (successParam === 'newOrigin') { + expect(resultTrueCopy).to.be(true); + } else { + expect(resultTrueCopy).to.be(undefined); + } + const { _source } = await expectResponses.successCreated( es, spaceId, diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts index 680f73499da38..164a01a3c2618 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts @@ -22,6 +22,7 @@ const { const { fail400, fail409 } = testCaseFailures; const destinationId = (condition?: boolean) => condition !== false ? { successParam: 'destinationId' } : {}; +const newOrigin = () => ({ successParam: 'newOrigin' }); const ambiguousConflict = (suffix: string) => ({ failure: 409 as 409, fail409Param: `ambiguous_conflict_${suffix}`, @@ -72,8 +73,8 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { ...fail409(!overwrite && spaceId === SPACE_2_ID), ...destinationId(spaceId !== SPACE_2_ID), }, - { ...CASES.CONFLICT_1A_OBJ, ...destinationId() }, // "ambiguous source" conflict which results in a new destination ID - { ...CASES.CONFLICT_1B_OBJ, ...destinationId() }, // "ambiguous source" conflict which results in a new destination ID + { ...CASES.CONFLICT_1A_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_1B_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict ]; @@ -87,8 +88,8 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { { ...CASES.CONFLICT_1_OBJ, ...fail409(!overwrite) }, // "exact match" conflict CASES.CONFLICT_1A_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match CASES.CONFLICT_1B_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match - { ...CASES.CONFLICT_2C_OBJ, ...destinationId() }, // "ambiguous source and destination" conflict which results in a new destination ID - { ...CASES.CONFLICT_2D_OBJ, ...destinationId() }, // "ambiguous source and destination" conflict which results in a new destination ID + { ...CASES.CONFLICT_2C_OBJ, ...newOrigin() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_2D_OBJ, ...newOrigin() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID ]; return { group1Importable, group1NonImportable, group1All, group2, group3, group4 }; }; diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts index 0a66b3479482f..163fb53253c74 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts @@ -23,6 +23,7 @@ const { const { fail400, fail409 } = testCaseFailures; const destinationId = (condition?: boolean) => condition !== false ? { successParam: 'destinationId' } : {}; +const newOrigin = () => ({ successParam: 'newOrigin' }); const createTrueCopyTestCases = () => { // for each outcome, if failure !== undefined then we expect to receive @@ -69,6 +70,8 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { ...fail409(!overwrite && spaceId === SPACE_2_ID), ...destinationId(spaceId !== SPACE_2_ID), }, + { ...CASES.CONFLICT_1A_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_1B_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID // all of the cases below represent imports that had an inexact match conflict or an ambiguous conflict // if we call _resolve_import_errors and don't specify overwrite, each of these will result in a conflict because an object with that // `expectedDestinationId` already exists diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/import.ts b/x-pack/test/saved_object_api_integration/security_only/apis/import.ts index bedd9398d3d58..3ff7925628d38 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/import.ts @@ -16,6 +16,7 @@ import { const { fail400, fail409 } = testCaseFailures; const destinationId = (condition?: boolean) => condition !== false ? { successParam: 'destinationId' } : {}; +const newOrigin = () => ({ successParam: 'newOrigin' }); const ambiguousConflict = (suffix: string) => ({ failure: 409 as 409, fail409Param: `ambiguous_conflict_${suffix}`, @@ -51,8 +52,8 @@ const createTestCases = (overwrite: boolean) => { { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite) }, { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...destinationId() }, { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...destinationId() }, - { ...CASES.CONFLICT_1A_OBJ, ...destinationId() }, // "ambiguous source" conflict which results in a new destination ID - { ...CASES.CONFLICT_1B_OBJ, ...destinationId() }, // "ambiguous source" conflict which results in a new destination ID + { ...CASES.CONFLICT_1A_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_1B_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict ]; @@ -66,8 +67,8 @@ const createTestCases = (overwrite: boolean) => { { ...CASES.CONFLICT_1_OBJ, ...fail409(!overwrite) }, // "exact match" conflict CASES.CONFLICT_1A_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match CASES.CONFLICT_1B_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match - { ...CASES.CONFLICT_2C_OBJ, ...destinationId() }, // "ambiguous source and destination" conflict which results in a new destination ID - { ...CASES.CONFLICT_2D_OBJ, ...destinationId() }, // "ambiguous source and destination" conflict which results in a new destination ID + { ...CASES.CONFLICT_2C_OBJ, ...newOrigin() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_2D_OBJ, ...newOrigin() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID ]; return { group1Importable, group1NonImportable, group1All, group2, group3, group4 }; }; diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts index 1cf732e45a39f..dacf1d4745274 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts @@ -17,6 +17,7 @@ import { const { fail400, fail409 } = testCaseFailures; const destinationId = (condition?: boolean) => condition !== false ? { successParam: 'destinationId' } : {}; +const newOrigin = () => ({ successParam: 'newOrigin' }); const createTrueCopyTestCases = () => { // for each outcome, if failure !== undefined then we expect to receive @@ -43,6 +44,8 @@ const createTestCases = (overwrite: boolean) => { const group1All = [...group1Importable, ...group1NonImportable]; const group2 = [ { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite) }, + { ...CASES.CONFLICT_1A_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_1B_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID // all of the cases below represent imports that had an inexact match conflict or an ambiguous conflict // if we call _resolve_import_errors and don't specify overwrite, each of these will result in a conflict because an object with that // `expectedDestinationId` already exists diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts index 3b740fe54f197..fcb66da3b0ed1 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts @@ -17,6 +17,7 @@ const { const { fail400, fail409 } = testCaseFailures; const destinationId = (condition?: boolean) => condition !== false ? { successParam: 'destinationId' } : {}; +const newOrigin = () => ({ successParam: 'newOrigin' }); const ambiguousConflict = (suffix: string) => ({ failure: 409 as 409, fail409Param: `ambiguous_conflict_${suffix}`, @@ -60,8 +61,8 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { }, { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, { ...CASES.HIDDEN, ...fail400() }, - { ...CASES.CONFLICT_1A_OBJ, ...destinationId() }, // "ambiguous source" conflict which results in a new destination ID - { ...CASES.CONFLICT_1B_OBJ, ...destinationId() }, // "ambiguous source" conflict which results in a new destination ID + { ...CASES.CONFLICT_1A_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_1B_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict CASES.NEW_SINGLE_NAMESPACE_OBJ, @@ -78,8 +79,8 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { { ...CASES.CONFLICT_1_OBJ, ...fail409(!overwrite) }, // "exact match" conflict CASES.CONFLICT_1A_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match CASES.CONFLICT_1B_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match - { ...CASES.CONFLICT_2C_OBJ, ...destinationId() }, // "ambiguous source and destination" conflict which results in a new destination ID - { ...CASES.CONFLICT_2D_OBJ, ...destinationId() }, // "ambiguous source and destination" conflict which results in a new destination ID + { ...CASES.CONFLICT_2C_OBJ, ...newOrigin() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_2D_OBJ, ...newOrigin() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID ]; return { group1, group2, group3 }; }; diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts index a45abeb644845..f95b3e4b13bec 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts @@ -21,6 +21,7 @@ const { const { fail400, fail409 } = testCaseFailures; const destinationId = (condition?: boolean) => condition !== false ? { successParam: 'destinationId' } : {}; +const newOrigin = () => ({ successParam: 'newOrigin' }); const createTrueCopyTestCases = () => { // for each outcome, if failure !== undefined then we expect to receive @@ -60,6 +61,8 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { }, { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, { ...CASES.HIDDEN, ...fail400() }, + { ...CASES.CONFLICT_1A_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_1B_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID // all of the cases below represent imports that had an inexact match conflict or an ambiguous conflict // if we call _resolve_import_errors and don't specify overwrite, each of these will result in a conflict because an object with that // `expectedDestinationId` already exists From 19dc294828bb9f6cf48bc058ddfa71796d2a71e4 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 7 Jul 2020 18:56:04 -0400 Subject: [PATCH 34/55] Fix CI errors after merge --- .../server/services/epm/kibana/assets/install.ts | 6 +++++- .../spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts | 2 ++ .../lib/copy_to_spaces/resolve_copy_conflicts.test.ts | 2 ++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts index ae6493d4716e8..4086f09c8451d 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts @@ -12,7 +12,11 @@ import { import * as Registry from '../../registry'; import { AssetType, KibanaAssetType, AssetReference } from '../../../../types'; -type SavedObjectToBe = Required & { type: AssetType }; +type SavedObjectToBe = Required< + Pick +> & { + type: AssetType; +}; export type ArchiveAsset = Pick< SavedObject, 'id' | 'attributes' | 'migrationVersion' | 'references' diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts index d02e21f9c222d..700e55821fc2e 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts @@ -257,6 +257,7 @@ describe('copySavedObjectsToSpaces', () => { "getImportableAndExportableTypes": [MockFunction], "getIndex": [MockFunction], "getType": [MockFunction], + "getVisibleTypes": [MockFunction], "isHidden": [MockFunction], "isImportableAndExportable": [MockFunction], "isMultiNamespace": [MockFunction], @@ -332,6 +333,7 @@ describe('copySavedObjectsToSpaces', () => { "getImportableAndExportableTypes": [MockFunction], "getIndex": [MockFunction], "getType": [MockFunction], + "getVisibleTypes": [MockFunction], "isHidden": [MockFunction], "isImportableAndExportable": [MockFunction], "isMultiNamespace": [MockFunction], diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts index 0778c379c53b9..8e53a2468c9ee 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts @@ -280,6 +280,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { "getImportableAndExportableTypes": [MockFunction], "getIndex": [MockFunction], "getType": [MockFunction], + "getVisibleTypes": [MockFunction], "isHidden": [MockFunction], "isImportableAndExportable": [MockFunction], "isMultiNamespace": [MockFunction], @@ -362,6 +363,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { "getImportableAndExportableTypes": [MockFunction], "getIndex": [MockFunction], "getType": [MockFunction], + "getVisibleTypes": [MockFunction], "isHidden": [MockFunction], "isImportableAndExportable": [MockFunction], "isMultiNamespace": [MockFunction], From ad92b1a27df566c256700b5c2f779bb3271f7985 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 7 Jul 2020 18:57:26 -0400 Subject: [PATCH 35/55] Change import API's `importIdMap` 1. Get rid of `importIds` set and broaden the scope of `importIdMap` to be used for that too. 2. Generate the initial `importIdMap` when objects are collected (before `validateReferences` is called); this is necessary so that "exact match" conflicts are detected properly. Originally I did not implement it this way because I intended to move the call to `validateReferences` to take place after conflict checks, however, I wound up scrapping that idea. --- .../import/check_conflicts.test.ts | 2 - .../saved_objects/import/check_conflicts.ts | 8 +-- .../import/check_origin_conflicts.test.ts | 37 ++++++----- .../import/check_origin_conflicts.ts | 5 +- .../import/collect_saved_objects.test.ts | 15 +++-- .../import/collect_saved_objects.ts | 3 + .../import/create_saved_objects.ts | 6 +- .../import/import_saved_objects.test.ts | 65 ++++++++++++++----- .../import/import_saved_objects.ts | 20 +++--- .../import/resolve_import_errors.test.ts | 42 ++++++++---- .../import/resolve_import_errors.ts | 2 +- 11 files changed, 133 insertions(+), 72 deletions(-) diff --git a/src/core/server/saved_objects/import/check_conflicts.test.ts b/src/core/server/saved_objects/import/check_conflicts.test.ts index d2daab87aa0df..85fe7a482e886 100644 --- a/src/core/server/saved_objects/import/check_conflicts.test.ts +++ b/src/core/server/saved_objects/import/check_conflicts.test.ts @@ -102,7 +102,6 @@ describe('#checkConflicts', () => { filteredObjects: [], errors: [], importIdMap: new Map(), - importIds: new Set(), }); }); @@ -132,7 +131,6 @@ describe('#checkConflicts', () => { }, ], importIdMap: new Map([[`${obj3.type}:${obj3.id}`, { id: `new-object-id` }]]), - importIds: new Set(objects.map(({ type, id }) => `${type}:${id}`)), }); }); diff --git a/src/core/server/saved_objects/import/check_conflicts.ts b/src/core/server/saved_objects/import/check_conflicts.ts index 907b8dcc6d77c..5dcd5bdfe68ba 100644 --- a/src/core/server/saved_objects/import/check_conflicts.ts +++ b/src/core/server/saved_objects/import/check_conflicts.ts @@ -41,12 +41,11 @@ export async function checkConflicts( ) { const filteredObjects: Array> = []; const errors: SavedObjectsImportError[] = []; - const importIdMap = new Map(); - const importIds = new Set(); + const importIdMap = new Map(); // exit early if there are no objects to check if (objects.length === 0) { - return { filteredObjects, errors, importIdMap, importIds }; + return { filteredObjects, errors, importIdMap }; } const { savedObjectsClient, namespace, ignoreRegularConflicts, trueCopy } = options; @@ -62,7 +61,6 @@ export async function checkConflicts( id, attributes: { title }, } = object; - importIds.add(`${type}:${id}`); const errorObj = errorMap.get(`${type}:${id}`); if (errorObj && isUnresolvableConflict(errorObj)) { // Any object create attempt that would result in an unresolvable conflict should have its ID regenerated. This way, when an object @@ -79,5 +77,5 @@ export async function checkConflicts( filteredObjects.push(object); } }); - return { filteredObjects, errors, importIdMap, importIds }; + return { filteredObjects, errors, importIdMap }; } diff --git a/src/core/server/saved_objects/import/check_origin_conflicts.test.ts b/src/core/server/saved_objects/import/check_origin_conflicts.test.ts index 05d08a6f8a1e2..6ba5ef2c85d43 100644 --- a/src/core/server/saved_objects/import/check_origin_conflicts.test.ts +++ b/src/core/server/saved_objects/import/check_origin_conflicts.test.ts @@ -67,17 +67,17 @@ describe('#checkOriginConflicts', () => { const setupOptions = ( options: { namespace?: string; - importIds?: Set; + importIdMap?: Map; ignoreRegularConflicts?: boolean; } = {} ): CheckOriginConflictsOptions => { - const { namespace, importIds = new Set(), ignoreRegularConflicts } = options; + const { namespace, importIdMap = new Map(), ignoreRegularConflicts } = options; savedObjectsClient = savedObjectsClientMock.create(); find = savedObjectsClient.find; find.mockResolvedValue(getResultMock()); // mock zero hits response by default typeRegistry = typeRegistryMock.create(); typeRegistry.isMultiNamespace.mockImplementation((type) => type === MULTI_NS_TYPE); - return { savedObjectsClient, typeRegistry, namespace, ignoreRegularConflicts, importIds }; + return { savedObjectsClient, typeRegistry, namespace, ignoreRegularConflicts, importIdMap }; }; const mockFindResult = (...objects: SavedObjectType[]) => { @@ -218,11 +218,11 @@ describe('#checkOriginConflicts', () => { const obj3 = createObject(MULTI_NS_TYPE, 'id-3', 'originId-foo'); const obj4 = createObject(MULTI_NS_TYPE, 'id-4', obj3.originId); const options = setupOptions({ - importIds: new Set([ - `${obj1.type}:${obj1.id}`, - `${obj2.type}:${obj2.id}`, - `${obj3.type}:${obj3.id}`, - `${obj4.type}:${obj4.id}`, + importIdMap: new Map([ + [`${obj1.type}:${obj1.id}`, {}], + [`${obj2.type}:${obj2.id}`, {}], + [`${obj3.type}:${obj3.id}`, {}], + [`${obj4.type}:${obj4.id}`, {}], ]), }); mockFindResult(obj1); // find for obj2: the result is an inexact match with one destination that is exactly matched by obj1 so it is ignored -- accordingly, obj2 has no match @@ -244,7 +244,11 @@ describe('#checkOriginConflicts', () => { const obj2 = createObject(MULTI_NS_TYPE, 'id-2', obj1.id); const obj3 = createObject(MULTI_NS_TYPE, 'id-3', obj1.id); const options = setupOptions({ - importIds: new Set([`${obj1.type}:${obj1.id}`, `${obj2.type}:${obj2.id}`]), + importIdMap: new Map([ + [`${obj1.type}:${obj1.id}`, {}], + [`${obj2.type}:${obj2.id}`, {}], + [`${obj3.type}:${obj3.id}`, {}], + ]), }); mockFindResult(obj1, obj2); // find for obj3: the result is an inexact match with two destinations that are exactly matched by obj1 and obj2 so they are ignored -- accordingly, obj3 has no match @@ -313,11 +317,11 @@ describe('#checkOriginConflicts', () => { const setup = (ignoreRegularConflicts: boolean) => { const options = setupOptions({ ignoreRegularConflicts, - importIds: new Set([ - `${obj1.type}:${obj1.id}`, - `${obj2.type}:${obj2.id}`, - `${obj3.type}:${obj3.id}`, - `${obj4.type}:${obj4.id}`, + importIdMap: new Map([ + [`${obj1.type}:${obj1.id}`, {}], + [`${obj2.type}:${obj2.id}`, {}], + [`${obj3.type}:${obj3.id}`, {}], + [`${obj4.type}:${obj4.id}`, {}], ]), }); mockFindResult(obj1, objA); // find for obj2: the result is an inexact match with two destinations, but the first destination is exactly matched by obj1 so it is ignored -- accordingly, obj2 has an inexact match with one destination (objA) @@ -466,10 +470,11 @@ describe('#checkOriginConflicts', () => { const objD = createObject(MULTI_NS_TYPE, 'id-D', obj7.id); const objE = createObject(MULTI_NS_TYPE, 'id-E', obj7.id); const objects = [obj1, obj2, obj4, obj5, obj6, obj7, obj8]; - const importIds = new Set([...objects, obj3].map(({ type, id }) => `${type}:${id}`)); + + const importIdMap = new Map([...objects, obj3].map(({ type, id }) => [`${type}:${id}`, {}])); const setup = (ignoreRegularConflicts: boolean) => { - const options = setupOptions({ importIds, ignoreRegularConflicts }); + const options = setupOptions({ importIdMap, ignoreRegularConflicts }); // obj1 is a non-multi-namespace type, so it is skipped while searching mockFindResult(); // find for obj2: the result is no match mockFindResult(obj3); // find for obj4: the result is an inexact match with one destination that is exactly matched by obj3 so it is ignored -- accordingly, obj4 has no match diff --git a/src/core/server/saved_objects/import/check_origin_conflicts.ts b/src/core/server/saved_objects/import/check_origin_conflicts.ts index 04364b367904b..b71f560ba8a83 100644 --- a/src/core/server/saved_objects/import/check_origin_conflicts.ts +++ b/src/core/server/saved_objects/import/check_origin_conflicts.ts @@ -31,7 +31,7 @@ interface CheckOriginConflictsOptions { typeRegistry: ISavedObjectTypeRegistry; namespace?: string; ignoreRegularConflicts?: boolean; - importIds: Set; + importIdMap: Map; } interface GetImportIdMapForRetriesOptions { @@ -85,7 +85,8 @@ const checkOriginConflict = async ( object: SavedObject<{ title?: string }>, options: CheckOriginConflictsOptions ): Promise> => { - const { savedObjectsClient, typeRegistry, namespace, importIds } = options; + const { savedObjectsClient, typeRegistry, namespace, importIdMap } = options; + const importIds = new Set(importIdMap.keys()); const { type, originId } = object; if (!typeRegistry.isMultiNamespace(type)) { diff --git a/src/core/server/saved_objects/import/collect_saved_objects.test.ts b/src/core/server/saved_objects/import/collect_saved_objects.test.ts index 6f8a98e7e3216..4e83a7e837790 100644 --- a/src/core/server/saved_objects/import/collect_saved_objects.test.ts +++ b/src/core/server/saved_objects/import/collect_saved_objects.test.ts @@ -130,7 +130,7 @@ describe('collectSavedObjects()', () => { const readStream = createReadStream(); const result = await collectSavedObjects({ readStream, supportedTypes: [], objectLimit }); - expect(result).toEqual({ collectedObjects: [], errors: [] }); + expect(result).toEqual({ collectedObjects: [], errors: [], importIdMap: new Map() }); }); test('collects objects from stream', async () => { @@ -139,7 +139,8 @@ describe('collectSavedObjects()', () => { const result = await collectSavedObjects({ readStream, supportedTypes, objectLimit }); const collectedObjects = [{ ...obj1, migrationVersion: {} }]; - expect(result).toEqual({ collectedObjects, errors: [] }); + const importIdMap = new Map([[`${obj1.type}:${obj1.id}`, {}]]); + expect(result).toEqual({ collectedObjects, errors: [], importIdMap }); }); test('unsupported types return as import errors', async () => { @@ -149,7 +150,7 @@ describe('collectSavedObjects()', () => { const error = { type: 'unsupported_type' }; const errors = [{ error, type: obj1.type, id: obj1.id, title: obj1.attributes.title }]; - expect(result).toEqual({ collectedObjects: [], errors }); + expect(result).toEqual({ collectedObjects: [], errors, importIdMap: new Map() }); }); test('returns mixed results', async () => { @@ -158,9 +159,10 @@ describe('collectSavedObjects()', () => { const result = await collectSavedObjects({ readStream, supportedTypes, objectLimit }); const collectedObjects = [{ ...obj2, migrationVersion: {} }]; + const importIdMap = new Map([[`${obj2.type}:${obj2.id}`, {}]]); const error = { type: 'unsupported_type' }; const errors = [{ error, type: obj1.type, id: obj1.id, title: obj1.attributes.title }]; - expect(result).toEqual({ collectedObjects, errors }); + expect(result).toEqual({ collectedObjects, errors, importIdMap }); }); describe('with optional filter', () => { @@ -177,7 +179,7 @@ describe('collectSavedObjects()', () => { const error = { type: 'unsupported_type' }; const errors = [{ error, type: obj1.type, id: obj1.id, title: obj1.attributes.title }]; - expect(result).toEqual({ collectedObjects: [], errors }); + expect(result).toEqual({ collectedObjects: [], errors, importIdMap: new Map() }); }); test('does not filter out objects when result === true', async () => { @@ -192,9 +194,10 @@ describe('collectSavedObjects()', () => { }); const collectedObjects = [{ ...obj2, migrationVersion: {} }]; + const importIdMap = new Map([[`${obj2.type}:${obj2.id}`, {}]]); const error = { type: 'unsupported_type' }; const errors = [{ error, type: obj1.type, id: obj1.id, title: obj1.attributes.title }]; - expect(result).toEqual({ collectedObjects, errors }); + expect(result).toEqual({ collectedObjects, errors, importIdMap }); }); }); }); diff --git a/src/core/server/saved_objects/import/collect_saved_objects.ts b/src/core/server/saved_objects/import/collect_saved_objects.ts index 6983da0231b25..45275ecce5432 100644 --- a/src/core/server/saved_objects/import/collect_saved_objects.ts +++ b/src/core/server/saved_objects/import/collect_saved_objects.ts @@ -45,6 +45,7 @@ export async function collectSavedObjects({ }: CollectSavedObjectsOptions) { const errors: SavedObjectsImportError[] = []; const entries: Array<{ type: string; id: string }> = []; + const importIdMap = new Map(); const collectedObjects: Array> = await createPromiseFromStreams([ readStream, createLimitStream(objectLimit), @@ -65,6 +66,7 @@ export async function collectSavedObjects({ }), createFilterStream((obj) => (filter ? filter(obj) : true)), createMapStream((obj: SavedObject) => { + importIdMap.set(`${obj.type}:${obj.id}`, {}); // Ensure migrations execute on every saved object return Object.assign({ migrationVersion: {} }, obj); }), @@ -82,5 +84,6 @@ export async function collectSavedObjects({ return { errors, collectedObjects, + importIdMap, }; } diff --git a/src/core/server/saved_objects/import/create_saved_objects.ts b/src/core/server/saved_objects/import/create_saved_objects.ts index 53965ddef7857..6aabf12593783 100644 --- a/src/core/server/saved_objects/import/create_saved_objects.ts +++ b/src/core/server/saved_objects/import/create_saved_objects.ts @@ -22,7 +22,7 @@ import { extractErrors } from './extract_errors'; interface CreateSavedObjectsOptions { savedObjectsClient: SavedObjectsClientContract; - importIdMap: Map; + importIdMap: Map; namespace?: string; overwrite?: boolean; } @@ -58,7 +58,7 @@ export const createSavedObjects = async ( // use the import ID map to ensure that each object is being created with the correct ID, also ensure that the `originId` is set on // the created object if it did not have one (or is omitted if specified) const importIdEntry = importIdMap.get(`${object.type}:${object.id}`); - if (importIdEntry) { + if (importIdEntry?.id) { objectIdMap.set(`${object.type}:${importIdEntry.id}`, object); const originId = importIdEntry.omitOriginId ? undefined : object.originId ?? object.id; return { ...object, id: importIdEntry.id, originId }; @@ -70,7 +70,7 @@ export const createSavedObjects = async ( const references = object.references?.map((reference) => { const { type, id } = reference; const importIdEntry = importIdMap.get(`${type}:${id}`); - if (importIdEntry) { + if (importIdEntry?.id) { return { ...reference, id: importIdEntry.id }; } return reference; 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 23271cc0248c3..0a155a051974e 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 @@ -51,14 +51,17 @@ describe('#importSavedObjectsFromStream', () => { beforeEach(() => { jest.clearAllMocks(); // mock empty output of each of these mocked modules so the import doesn't throw an error - getMockFn(collectSavedObjects).mockResolvedValue({ errors: [], collectedObjects: [] }); + getMockFn(collectSavedObjects).mockResolvedValue({ + errors: [], + collectedObjects: [], + importIdMap: new Map(), + }); getMockFn(regenerateIds).mockReturnValue({ importIdMap: new Map() }); getMockFn(validateReferences).mockResolvedValue({ errors: [], filteredObjects: [] }); getMockFn(checkConflicts).mockResolvedValue({ errors: [], filteredObjects: [], importIdMap: new Map(), - importIds: new Set(), }); getMockFn(checkOriginConflicts).mockResolvedValue({ errors: [], @@ -119,7 +122,11 @@ describe('#importSavedObjectsFromStream', () => { test('validates references', async () => { const options = setupOptions(); const collectedObjects = [createObject()]; - getMockFn(collectSavedObjects).mockResolvedValue({ errors: [], collectedObjects }); + getMockFn(collectSavedObjects).mockResolvedValue({ + errors: [], + collectedObjects, + importIdMap: new Map(), + }); await importSavedObjectsFromStream(options); expect(validateReferences).toHaveBeenCalledWith( @@ -133,7 +140,11 @@ describe('#importSavedObjectsFromStream', () => { test('does not regenerate object IDs', async () => { const options = setupOptions(); const collectedObjects = [createObject()]; - getMockFn(collectSavedObjects).mockResolvedValue({ errors: [], collectedObjects }); + getMockFn(collectSavedObjects).mockResolvedValue({ + errors: [], + collectedObjects, + importIdMap: new Map(), + }); await importSavedObjectsFromStream(options); expect(regenerateIds).not.toHaveBeenCalled(); @@ -156,12 +167,11 @@ describe('#importSavedObjectsFromStream', () => { test('checks origin conflicts', async () => { const options = setupOptions(); const filteredObjects = [createObject()]; - const importIds = new Set(); + const importIdMap = new Map(); getMockFn(checkConflicts).mockResolvedValue({ errors: [], filteredObjects, - importIdMap: new Map(), - importIds, + importIdMap, }); await importSavedObjectsFromStream(options); @@ -170,7 +180,7 @@ describe('#importSavedObjectsFromStream', () => { typeRegistry, namespace, ignoreRegularConflicts: overwrite, - importIds, + importIdMap, }; expect(checkOriginConflicts).toHaveBeenCalledWith( filteredObjects, @@ -185,6 +195,11 @@ describe('#importSavedObjectsFromStream', () => { getMockFn(collectSavedObjects).mockResolvedValue({ errors: [errors[0]], collectedObjects: [], // doesn't matter + importIdMap: new Map([ + ['foo', {}], + ['bar', {}], + ['baz', {}], + ]), }); getMockFn(validateReferences).mockResolvedValue({ errors: [errors[1]], @@ -193,17 +208,20 @@ describe('#importSavedObjectsFromStream', () => { getMockFn(checkConflicts).mockResolvedValue({ errors: [errors[2]], filteredObjects, - importIdMap: new Map().set(`id1`, { id: `newId1` }), - importIds: new Set(), + importIdMap: new Map([['bar', { id: 'newId1' }]]), }); getMockFn(checkOriginConflicts).mockResolvedValue({ errors: [errors[3]], filteredObjects, - importIdMap: new Map().set(`id2`, { id: `newId2` }), + importIdMap: new Map([['baz', { id: 'newId2' }]]), }); await importSavedObjectsFromStream(options); - const importIdMap = new Map().set(`id1`, { id: `newId1` }).set(`id2`, { id: `newId2` }); + const importIdMap = new Map([ + ['foo', {}], + ['bar', { id: 'newId1' }], + ['baz', { id: 'newId2' }], + ]); const createSavedObjectsOptions = { savedObjectsClient, importIdMap, overwrite, namespace }; expect(createSavedObjects).toHaveBeenCalledWith( filteredObjects, @@ -217,7 +235,11 @@ describe('#importSavedObjectsFromStream', () => { test('regenerates object IDs', async () => { const options = setupOptions(true); const collectedObjects = [createObject()]; - getMockFn(collectSavedObjects).mockResolvedValue({ errors: [], collectedObjects }); + getMockFn(collectSavedObjects).mockResolvedValue({ + errors: [], + collectedObjects, + importIdMap: new Map(), // doesn't matter + }); await importSavedObjectsFromStream(options); expect(regenerateIds).toHaveBeenCalledWith(collectedObjects); @@ -240,8 +262,13 @@ describe('#importSavedObjectsFromStream', () => { getMockFn(collectSavedObjects).mockResolvedValue({ errors: [errors[0]], collectedObjects: [], // doesn't matter + importIdMap: new Map([ + ['foo', {}], + ['bar', {}], + ]), }); getMockFn(validateReferences).mockResolvedValue({ errors: [errors[1]], filteredObjects }); + // this importIdMap is not composed with the one obtained from `collectSavedObjects` const importIdMap = new Map().set(`id1`, { id: `newId1` }); getMockFn(regenerateIds).mockReturnValue({ importIdMap }); @@ -267,7 +294,11 @@ describe('#importSavedObjectsFromStream', () => { test('returns success=false if an error occurred', async () => { const options = setupOptions(); const errors = [createError()]; - getMockFn(collectSavedObjects).mockResolvedValue({ errors, collectedObjects: [] }); + getMockFn(collectSavedObjects).mockResolvedValue({ + errors, + collectedObjects: [], + importIdMap: new Map(), // doesn't matter + }); const result = await importSavedObjectsFromStream(options); expect(result).toEqual({ success: false, successCount: 0, errors }); @@ -321,18 +352,18 @@ describe('#importSavedObjectsFromStream', () => { getMockFn(collectSavedObjects).mockResolvedValue({ errors: [errors[0]], collectedObjects: [], + importIdMap: new Map(), // doesn't matter }); getMockFn(validateReferences).mockResolvedValue({ errors: [errors[1]], filteredObjects: [] }); getMockFn(checkConflicts).mockResolvedValue({ errors: [errors[2]], filteredObjects: [], - importIdMap: new Map(), - importIds: new Set(), + importIdMap: new Map(), // doesn't matters }); getMockFn(checkOriginConflicts).mockResolvedValue({ errors: [errors[3]], filteredObjects: [], - importIdMap: new Map(), + importIdMap: new Map(), // doesn't matters }); getMockFn(createSavedObjects).mockResolvedValue({ errors: [errors[4]], createdObjects: [] }); 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 bd18513c8d10d..c6ce179843a93 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.ts @@ -45,19 +45,21 @@ export async function importSavedObjectsFromStream({ namespace, }: SavedObjectsImportOptions): Promise { let errorAccumulator: SavedObjectsImportError[] = []; - let importIdMap: Map = new Map(); const supportedTypes = typeRegistry.getImportableAndExportableTypes().map((type) => type.name); // Get the objects to import - const { - errors: collectorErrors, - collectedObjects: objectsFromStream, - } = await collectSavedObjects({ readStream, objectLimit, supportedTypes }); - errorAccumulator = [...errorAccumulator, ...collectorErrors]; + const collectSavedObjectsResult = await collectSavedObjects({ + readStream, + objectLimit, + supportedTypes, + }); + errorAccumulator = [...errorAccumulator, ...collectSavedObjectsResult.errors]; + /** Map of all IDs for objects that we are attempting to import; each value is empty by default */ + let importIdMap = collectSavedObjectsResult.importIdMap; // Validate references const validateReferencesResult = await validateReferences( - objectsFromStream, + collectSavedObjectsResult.collectedObjects, savedObjectsClient, namespace ); @@ -65,7 +67,7 @@ export async function importSavedObjectsFromStream({ let objectsToCreate = validateReferencesResult.filteredObjects; if (trueCopy) { - const regenerateIdsResult = regenerateIds(objectsFromStream); + const regenerateIdsResult = regenerateIds(collectSavedObjectsResult.collectedObjects); importIdMap = regenerateIdsResult.importIdMap; } else { // Check single-namespace objects for conflicts in this namespace, and check multi-namespace objects for conflicts across all namespaces @@ -87,7 +89,7 @@ export async function importSavedObjectsFromStream({ typeRegistry, namespace, ignoreRegularConflicts: overwrite, - importIds: checkConflictsResult.importIds, + importIdMap, }; const checkOriginConflictsResult = await checkOriginConflicts( checkConflictsResult.filteredObjects, 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 3f82f528159d8..a128443d8ee63 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 @@ -58,13 +58,16 @@ describe('#importSavedObjectsFromStream', () => { jest.clearAllMocks(); // mock empty output of each of these mocked modules so the import doesn't throw an error getMockFn(createObjectsFilter).mockReturnValue(() => false); - getMockFn(collectSavedObjects).mockResolvedValue({ errors: [], collectedObjects: [] }); + getMockFn(collectSavedObjects).mockResolvedValue({ + errors: [], + collectedObjects: [], + importIdMap: new Map(), + }); getMockFn(validateReferences).mockResolvedValue({ errors: [], filteredObjects: [] }); getMockFn(checkConflicts).mockResolvedValue({ errors: [], filteredObjects: [], importIdMap: new Map(), - importIds: new Set(), }); getMockFn(getImportIdMapForRetries).mockReturnValue(new Map()); getMockFn(splitOverwrites).mockReturnValue({ @@ -157,7 +160,11 @@ describe('#importSavedObjectsFromStream', () => { test('validates references', async () => { const options = setupOptions(); const collectedObjects = [createObject()]; - getMockFn(collectSavedObjects).mockResolvedValue({ errors: [], collectedObjects }); + getMockFn(collectSavedObjects).mockResolvedValue({ + errors: [], + collectedObjects, + importIdMap: new Map(), // doesn't matter + }); await resolveSavedObjectsImportErrors(options); expect(validateReferences).toHaveBeenCalledWith( @@ -174,7 +181,11 @@ describe('#importSavedObjectsFromStream', () => { replaceReferences: [{ type: 'bar-type', from: 'abc', to: 'def' }], }); const options = setupOptions([retry]); - getMockFn(collectSavedObjects).mockResolvedValue({ errors: [], collectedObjects: [object] }); + getMockFn(collectSavedObjects).mockResolvedValue({ + errors: [], + collectedObjects: [object], + importIdMap: new Map(), // doesn't matter + }); await resolveSavedObjectsImportErrors(options); const objectWithReplacedReferences = { @@ -211,7 +222,6 @@ describe('#importSavedObjectsFromStream', () => { errors: [], filteredObjects, importIdMap: new Map(), - importIds: new Set(), }); await resolveSavedObjectsImportErrors(options); @@ -227,7 +237,6 @@ describe('#importSavedObjectsFromStream', () => { errors: [], filteredObjects, importIdMap: new Map(), - importIds: new Set(), }); await resolveSavedObjectsImportErrors(options); @@ -240,6 +249,7 @@ describe('#importSavedObjectsFromStream', () => { getMockFn(collectSavedObjects).mockResolvedValue({ errors: [errors[0]], collectedObjects: [], // doesn't matter + importIdMap: new Map(), // doesn't matter }); getMockFn(validateReferences).mockResolvedValue({ errors: [errors[1]], @@ -248,11 +258,16 @@ describe('#importSavedObjectsFromStream', () => { getMockFn(checkConflicts).mockResolvedValue({ errors: [errors[2]], filteredObjects: [], - importIdMap: new Map().set(`id1`, { id: `newId1` }), - importIds: new Set(), + importIdMap: new Map([ + ['foo', {}], + ['bar', {}], + ]), }); - getMockFn(getImportIdMapForRetries).mockReturnValue(new Map().set(`id2`, { id: `newId2` })); - const importIdMap = new Map().set(`id1`, { id: `newId1` }).set(`id2`, { id: `newId2` }); + getMockFn(getImportIdMapForRetries).mockReturnValue(new Map([['bar', { id: 'newId' }]])); + const importIdMap = new Map([ + ['foo', {}], + ['bar', { id: 'newId' }], + ]); const objectsToOverwrite = [createObject()]; const objectsToNotOverwrite = [createObject()]; getMockFn(splitOverwrites).mockReturnValue({ objectsToOverwrite, objectsToNotOverwrite }); @@ -287,7 +302,11 @@ describe('#importSavedObjectsFromStream', () => { test('returns success=false if an error occurred', async () => { const options = setupOptions(); const errors = [createError()]; - getMockFn(collectSavedObjects).mockResolvedValue({ errors, collectedObjects: [] }); + getMockFn(collectSavedObjects).mockResolvedValue({ + errors, + collectedObjects: [], + importIdMap: new Map(), // doesn't matter + }); const result = await resolveSavedObjectsImportErrors(options); expect(result).toEqual({ success: false, successCount: 0, errors }); @@ -325,6 +344,7 @@ describe('#importSavedObjectsFromStream', () => { getMockFn(collectSavedObjects).mockResolvedValue({ errors: [errors[0]], collectedObjects: [], + importIdMap: new Map(), // doesn't matter }); getMockFn(validateReferences).mockResolvedValue({ errors: [errors[1]], filteredObjects: [] }); getMockFn(createSavedObjects).mockResolvedValueOnce({ 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 2592fa8a28a6d..4b601a36f3cf8 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.ts @@ -51,7 +51,7 @@ export async function resolveSavedObjectsImportErrors({ let successCount = 0; let errorAccumulator: SavedObjectsImportError[] = []; - let importIdMap: Map = new Map(); + let importIdMap: Map = new Map(); const supportedTypes = typeRegistry.getImportableAndExportableTypes().map((type) => type.name); const filter = createObjectsFilter(retries); From e45adabed8104cf0e2a1b5c5017992f4428868b1 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 7 Jul 2020 18:57:44 -0400 Subject: [PATCH 36/55] True copy: regenerate IDs for resolved missing_reference errors This was an oversight on my part due to a badly written integration test. In reality, a missing_reference error will never have a `destinationId` associated with it, so it would be retried without getting a new ID. This commit fixes that behavior, so that upon resolving import errors, we first regenerate IDs for all objects, then allow those to be overridden by retries. --- .../import/resolve_import_errors.test.ts | 176 +++++++++++++----- .../import/resolve_import_errors.ts | 8 + .../resolve_import_errors.test.ts | 6 +- 3 files changed, 146 insertions(+), 44 deletions(-) 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 a128443d8ee63..c7af59d65e91e 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 @@ -34,6 +34,7 @@ import { resolveSavedObjectsImportErrors } from './resolve_import_errors'; import { validateRetries } from './validate_retries'; import { collectSavedObjects } from './collect_saved_objects'; +import { regenerateIds } from './regenerate_ids'; import { validateReferences } from './validate_references'; import { checkConflicts } from './check_conflicts'; import { getImportIdMapForRetries } from './check_origin_conflicts'; @@ -44,6 +45,7 @@ import { createObjectsFilter } from './create_objects_filter'; jest.mock('./validate_retries'); jest.mock('./create_objects_filter'); jest.mock('./collect_saved_objects'); +jest.mock('./regenerate_ids'); jest.mock('./validate_references'); jest.mock('./check_conflicts'); jest.mock('./check_origin_conflicts'); @@ -63,6 +65,7 @@ describe('#importSavedObjectsFromStream', () => { collectedObjects: [], importIdMap: new Map(), }); + getMockFn(regenerateIds).mockReturnValue({ importIdMap: new Map() }); getMockFn(validateReferences).mockResolvedValue({ errors: [], filteredObjects: [] }); getMockFn(checkConflicts).mockResolvedValue({ errors: [], @@ -82,10 +85,10 @@ describe('#importSavedObjectsFromStream', () => { let savedObjectsClient: jest.Mocked; let typeRegistry: jest.Mocked; const namespace = 'some-namespace'; - const trueCopy = false; const setupOptions = ( - retries: SavedObjectsImportRetry[] = [] + retries: SavedObjectsImportRetry[] = [], + trueCopy: boolean = false ): SavedObjectsResolveImportErrorsOptions => { readStream = new Readable(); savedObjectsClient = savedObjectsClientMock.create(); @@ -200,7 +203,8 @@ describe('#importSavedObjectsFromStream', () => { }); test('checks conflicts', async () => { - const options = setupOptions(); + const trueCopy = (Symbol() as unknown) as boolean; + const options = setupOptions([], trueCopy); const filteredObjects = [createObject()]; getMockFn(validateReferences).mockResolvedValue({ errors: [], filteredObjects }); @@ -216,7 +220,8 @@ describe('#importSavedObjectsFromStream', () => { test('gets import ID map for retries', async () => { const retries = [createRetry()]; - const options = setupOptions(retries); + const trueCopy = (Symbol() as unknown) as boolean; + const options = setupOptions(retries, trueCopy); const filteredObjects = [createObject()]; getMockFn(checkConflicts).mockResolvedValue({ errors: [], @@ -243,51 +248,136 @@ describe('#importSavedObjectsFromStream', () => { expect(splitOverwrites).toHaveBeenCalledWith(filteredObjects, retries); }); - test('creates saved objects', async () => { - const options = setupOptions(); - const errors = [createError(), createError(), createError()]; - getMockFn(collectSavedObjects).mockResolvedValue({ - errors: [errors[0]], - collectedObjects: [], // doesn't matter - importIdMap: new Map(), // doesn't matter - }); - getMockFn(validateReferences).mockResolvedValue({ - errors: [errors[1]], - filteredObjects: [], // doesn't matter + describe('with trueCopy disabled', () => { + test('does not regenerate object IDs', async () => { + const options = setupOptions(); + const collectedObjects = [createObject()]; + getMockFn(collectSavedObjects).mockResolvedValue({ + errors: [], + collectedObjects, + importIdMap: new Map(), // doesn't matter + }); + + await resolveSavedObjectsImportErrors(options); + expect(regenerateIds).not.toHaveBeenCalled(); }); - getMockFn(checkConflicts).mockResolvedValue({ - errors: [errors[2]], - filteredObjects: [], - importIdMap: new Map([ + + test('creates saved objects', async () => { + const options = setupOptions(); + const errors = [createError(), createError(), createError()]; + getMockFn(collectSavedObjects).mockResolvedValue({ + errors: [errors[0]], + collectedObjects: [], // doesn't matter + importIdMap: new Map(), // doesn't matter + }); + getMockFn(validateReferences).mockResolvedValue({ + errors: [errors[1]], + filteredObjects: [], // doesn't matter + }); + getMockFn(checkConflicts).mockResolvedValue({ + errors: [errors[2]], + filteredObjects: [], + importIdMap: new Map([ + ['foo', {}], + ['bar', {}], + ]), + }); + getMockFn(getImportIdMapForRetries).mockReturnValue(new Map([['bar', { id: 'newId' }]])); + const importIdMap = new Map([ ['foo', {}], - ['bar', {}], - ]), + ['bar', { id: 'newId' }], + ]); + const objectsToOverwrite = [createObject()]; + const objectsToNotOverwrite = [createObject()]; + getMockFn(splitOverwrites).mockReturnValue({ objectsToOverwrite, objectsToNotOverwrite }); + getMockFn(createSavedObjects).mockResolvedValueOnce({ + errors: [createError()], // this error will NOT be passed to the second `createSavedObjects` call + createdObjects: [], + }); + + await resolveSavedObjectsImportErrors(options); + const createSavedObjectsOptions = { savedObjectsClient, importIdMap, namespace }; + expect(createSavedObjects).toHaveBeenNthCalledWith(1, objectsToOverwrite, errors, { + ...createSavedObjectsOptions, + overwrite: true, + }); + expect(createSavedObjects).toHaveBeenNthCalledWith( + 2, + objectsToNotOverwrite, + errors, + createSavedObjectsOptions + ); }); - getMockFn(getImportIdMapForRetries).mockReturnValue(new Map([['bar', { id: 'newId' }]])); - const importIdMap = new Map([ - ['foo', {}], - ['bar', { id: 'newId' }], - ]); - const objectsToOverwrite = [createObject()]; - const objectsToNotOverwrite = [createObject()]; - getMockFn(splitOverwrites).mockReturnValue({ objectsToOverwrite, objectsToNotOverwrite }); - getMockFn(createSavedObjects).mockResolvedValueOnce({ - errors: [createError()], // this error will NOT be passed to the second `createSavedObjects` call - createdObjects: [], + }); + + describe('with trueCopy enabled', () => { + test('regenerates object IDs', async () => { + const options = setupOptions([], true); + const collectedObjects = [createObject()]; + getMockFn(collectSavedObjects).mockResolvedValue({ + errors: [], + collectedObjects, + importIdMap: new Map(), // doesn't matter + }); + + await resolveSavedObjectsImportErrors(options); + expect(regenerateIds).toHaveBeenCalledWith(collectedObjects); }); - await resolveSavedObjectsImportErrors(options); - const createSavedObjectsOptions = { savedObjectsClient, importIdMap, namespace }; - expect(createSavedObjects).toHaveBeenNthCalledWith(1, objectsToOverwrite, errors, { - ...createSavedObjectsOptions, - overwrite: true, + test('creates saved objects', async () => { + const options = setupOptions([], true); + const errors = [createError(), createError(), createError()]; + getMockFn(collectSavedObjects).mockResolvedValue({ + errors: [errors[0]], + collectedObjects: [], // doesn't matter + importIdMap: new Map(), // doesn't matter + }); + getMockFn(validateReferences).mockResolvedValue({ + errors: [errors[1]], + filteredObjects: [], // doesn't matter + }); + getMockFn(regenerateIds).mockReturnValue({ + importIdMap: new Map([ + ['foo', { id: 'randomId1' }], + ['bar', { id: 'randomId2' }], + ['baz', { id: 'randomId3' }], + ]), + }); + getMockFn(checkConflicts).mockResolvedValue({ + errors: [errors[2]], + filteredObjects: [], + importIdMap: new Map([ + ['bar', {}], + ['baz', {}], + ]), + }); + getMockFn(getImportIdMapForRetries).mockReturnValue(new Map([['baz', { id: 'newId' }]])); + const importIdMap = new Map([ + ['foo', { id: 'randomId1' }], + ['bar', {}], + ['baz', { id: 'newId' }], + ]); + const objectsToOverwrite = [createObject()]; + const objectsToNotOverwrite = [createObject()]; + getMockFn(splitOverwrites).mockReturnValue({ objectsToOverwrite, objectsToNotOverwrite }); + getMockFn(createSavedObjects).mockResolvedValueOnce({ + errors: [createError()], // this error will NOT be passed to the second `createSavedObjects` call + createdObjects: [], + }); + + await resolveSavedObjectsImportErrors(options); + const createSavedObjectsOptions = { savedObjectsClient, importIdMap, namespace }; + expect(createSavedObjects).toHaveBeenNthCalledWith(1, objectsToOverwrite, errors, { + ...createSavedObjectsOptions, + overwrite: true, + }); + expect(createSavedObjects).toHaveBeenNthCalledWith( + 2, + objectsToNotOverwrite, + errors, + createSavedObjectsOptions + ); }); - expect(createSavedObjects).toHaveBeenNthCalledWith( - 2, - objectsToNotOverwrite, - errors, - createSavedObjectsOptions - ); }); }); 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 4b601a36f3cf8..103c87e26875b 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.ts @@ -24,6 +24,7 @@ import { SavedObjectsImportResponse, SavedObjectsResolveImportErrorsOptions, } from './types'; +import { regenerateIds } from './regenerate_ids'; import { validateReferences } from './validate_references'; import { validateRetries } from './validate_retries'; import { createSavedObjects } from './create_saved_objects'; @@ -98,6 +99,13 @@ export async function resolveSavedObjectsImportErrors({ ); errorAccumulator = [...errorAccumulator, ...validateReferencesResult.errors]; + if (trueCopy) { + // In case any missing reference errors were resolved, ensure that we regenerate those object IDs as well + // This is because a retry to resolve a missing reference error may not necessarily specify a destinationId + const regenerateIdsResult = regenerateIds(objectsToResolve); + importIdMap = regenerateIdsResult.importIdMap; + } + // Check single-namespace objects for conflicts in this namespace, and check multi-namespace objects for conflicts across all namespaces const checkConflictsOptions = { savedObjectsClient, diff --git a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts index 536ea874cfd9f..7a3d933f1d2e6 100644 --- a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts @@ -17,6 +17,7 @@ * under the License. */ +import { mockUuidv4 } from '../../import/__mocks__'; import supertest from 'supertest'; import { UnwrapPromise } from '@kbn/utility-types'; import { registerResolveImportErrorsRoute } from '../resolve_import_errors'; @@ -57,6 +58,8 @@ describe(`POST ${URL}`, () => { }; beforeEach(async () => { + mockUuidv4.mockReset(); + mockUuidv4.mockImplementation(() => jest.requireActual('uuidv4')); ({ server, httpSetup, handlerContext } = await setupServer()); handlerContext.savedObjects.typeRegistry.getImportableAndExportableTypes.mockReturnValue( allowedTypes.map(createExportableType) @@ -243,6 +246,7 @@ describe(`POST ${URL}`, () => { describe('trueCopy enabled', () => { it('imports objects, regenerating all IDs/reference IDs present, and resetting all origin IDs', async () => { + mockUuidv4.mockReturnValue('new-id-1'); savedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [mockIndexPattern] }); savedObjectsClient.bulkCreate.mockResolvedValueOnce({ saved_objects: [ @@ -265,7 +269,7 @@ describe(`POST ${URL}`, () => { '--EXAMPLE', 'Content-Disposition: form-data; name="retries"', '', - '[{"type":"visualization","id":"my-vis","destinationId":"new-id-1","replaceReferences":[{"type":"index-pattern","from":"my-pattern","to":"existing"}]},{"type":"dashboard","id":"my-dashboard","destinationId":"new-id-2"}]', + '[{"type":"visualization","id":"my-vis","replaceReferences":[{"type":"index-pattern","from":"my-pattern","to":"existing"}]},{"type":"dashboard","id":"my-dashboard","destinationId":"new-id-2"}]', '--EXAMPLE--', ].join('\r\n') ) From 25cc72ad3f35353844ac21dd5ce214fba34c9ab5 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 7 Jul 2020 19:13:11 -0400 Subject: [PATCH 37/55] Address 2nd round of reviews / nits --- .../import/check_origin_conflicts.ts | 2 +- .../import/collect_saved_objects.test.ts | 4 +- .../import/collect_saved_objects.ts | 2 +- .../import/create_saved_objects.ts | 53 +++++++++---------- .../import/extract_errors.test.ts | 3 +- .../saved_objects/import/extract_errors.ts | 4 +- ...test.ts => get_non_unique_entries.test.ts} | 2 +- ...utilities.ts => get_non_unique_entries.ts} | 0 src/core/server/saved_objects/import/types.ts | 4 +- .../import/validate_retries.test.ts | 4 +- .../saved_objects/import/validate_retries.ts | 2 +- .../common/suites/import.ts | 6 --- 12 files changed, 39 insertions(+), 47 deletions(-) rename src/core/server/saved_objects/import/{utilities.test.ts => get_non_unique_entries.test.ts} (95%) rename src/core/server/saved_objects/import/{utilities.ts => get_non_unique_entries.ts} (100%) diff --git a/src/core/server/saved_objects/import/check_origin_conflicts.ts b/src/core/server/saved_objects/import/check_origin_conflicts.ts index b71f560ba8a83..26fcbdb916dbc 100644 --- a/src/core/server/saved_objects/import/check_origin_conflicts.ts +++ b/src/core/server/saved_objects/import/check_origin_conflicts.ts @@ -217,7 +217,7 @@ export async function checkOriginConflicts( } /** - * Assume that all objects exist in the `retries` map (due to filtering at the beginnning of `resolveSavedObjectsImportErrors`). + * Assume that all objects exist in the `retries` map (due to filtering at the beginning of `resolveSavedObjectsImportErrors`). */ export function getImportIdMapForRetries( objects: Array>, diff --git a/src/core/server/saved_objects/import/collect_saved_objects.test.ts b/src/core/server/saved_objects/import/collect_saved_objects.test.ts index 4e83a7e837790..f2be46ce960d2 100644 --- a/src/core/server/saved_objects/import/collect_saved_objects.test.ts +++ b/src/core/server/saved_objects/import/collect_saved_objects.test.ts @@ -20,10 +20,10 @@ import { Readable, PassThrough } from 'stream'; import { collectSavedObjects } from './collect_saved_objects'; import { createLimitStream } from './create_limit_stream'; -import { getNonUniqueEntries } from './utilities'; +import { getNonUniqueEntries } from './get_non_unique_entries'; jest.mock('./create_limit_stream'); -jest.mock('./utilities'); +jest.mock('./get_non_unique_entries'); const getMockFn = any, U>(fn: (...args: Parameters) => U) => fn as jest.MockedFunction<(...args: Parameters) => U>; diff --git a/src/core/server/saved_objects/import/collect_saved_objects.ts b/src/core/server/saved_objects/import/collect_saved_objects.ts index 45275ecce5432..c3b581dc6478f 100644 --- a/src/core/server/saved_objects/import/collect_saved_objects.ts +++ b/src/core/server/saved_objects/import/collect_saved_objects.ts @@ -27,7 +27,7 @@ import { import { SavedObject } from '../types'; import { createLimitStream } from './create_limit_stream'; import { SavedObjectsImportError } from './types'; -import { getNonUniqueEntries } from './utilities'; +import { getNonUniqueEntries } from './get_non_unique_entries'; import { SavedObjectsErrorHelpers } from '..'; interface CollectSavedObjectsOptions { diff --git a/src/core/server/saved_objects/import/create_saved_objects.ts b/src/core/server/saved_objects/import/create_saved_objects.ts index 6aabf12593783..f5e721d9e435c 100644 --- a/src/core/server/saved_objects/import/create_saved_objects.ts +++ b/src/core/server/saved_objects/import/create_saved_objects.ts @@ -19,6 +19,7 @@ import { SavedObject, SavedObjectsClientContract, SavedObjectsImportError } from '../types'; import { extractErrors } from './extract_errors'; +import { CreatedObject } from './types'; interface CreateSavedObjectsOptions { savedObjectsClient: SavedObjectsClientContract; @@ -27,7 +28,7 @@ interface CreateSavedObjectsOptions { overwrite?: boolean; } interface CreateSavedObjectsResult { - createdObjects: Array & { destinationId?: string }>; + createdObjects: Array>; errors: SavedObjectsImportError[]; } @@ -53,30 +54,26 @@ export const createSavedObjects = async ( new Map>() ); - const objectsToCreate = objects - .map((object) => { - // use the import ID map to ensure that each object is being created with the correct ID, also ensure that the `originId` is set on - // the created object if it did not have one (or is omitted if specified) - const importIdEntry = importIdMap.get(`${object.type}:${object.id}`); + const objectsToCreate = objects.map((object) => { + // use the import ID map to ensure that each reference is being created with the correct ID + const references = object.references?.map((reference) => { + const { type, id } = reference; + const importIdEntry = importIdMap.get(`${type}:${id}`); if (importIdEntry?.id) { - objectIdMap.set(`${object.type}:${importIdEntry.id}`, object); - const originId = importIdEntry.omitOriginId ? undefined : object.originId ?? object.id; - return { ...object, id: importIdEntry.id, originId }; + return { ...reference, id: importIdEntry.id }; } - return object; - }) - .map((object) => { - // use the import ID map to ensure that each reference is being created with the correct ID - const references = object.references?.map((reference) => { - const { type, id } = reference; - const importIdEntry = importIdMap.get(`${type}:${id}`); - if (importIdEntry?.id) { - return { ...reference, id: importIdEntry.id }; - } - return reference; - }); - return { ...object, ...(references && { references }) }; + return reference; }); + // use the import ID map to ensure that each object is being created with the correct ID, also ensure that the `originId` is set on + // the created object if it did not have one (or is omitted if specified) + const importIdEntry = importIdMap.get(`${object.type}:${object.id}`); + if (importIdEntry?.id) { + objectIdMap.set(`${object.type}:${importIdEntry.id}`, object); + const originId = importIdEntry.omitOriginId ? undefined : object.originId ?? object.id; + return { ...object, id: importIdEntry.id, originId, ...(references && { references }) }; + } + return { ...object, ...(references && { references }) }; + }); const resolvableErrors = ['conflict', 'ambiguous_conflict', 'missing_references']; let expectedResults = objectsToCreate; @@ -90,13 +87,11 @@ export const createSavedObjects = async ( // remap results to reflect the object IDs that were submitted for import // this ensures that consumers understand the results - const remappedResults = expectedResults.map & { destinationId?: string }>( - (result) => { - const { id } = objectIdMap.get(`${result.type}:${result.id}`)!; - // also, include a `destinationId` field if the object create attempt was made with a different ID - return { ...result, id, ...(id !== result.id && { destinationId: result.id }) }; - } - ); + const remappedResults = expectedResults.map>((result) => { + const { id } = objectIdMap.get(`${result.type}:${result.id}`)!; + // also, include a `destinationId` field if the object create attempt was made with a different ID + return { ...result, id, ...(id !== result.id && { destinationId: result.id }) }; + }); return { createdObjects: remappedResults.filter((obj) => !obj.error), diff --git a/src/core/server/saved_objects/import/extract_errors.test.ts b/src/core/server/saved_objects/import/extract_errors.test.ts index 1e061d9fb5055..f0c9e4ac292b8 100644 --- a/src/core/server/saved_objects/import/extract_errors.test.ts +++ b/src/core/server/saved_objects/import/extract_errors.test.ts @@ -20,6 +20,7 @@ import { SavedObject } from '../types'; import { extractErrors } from './extract_errors'; import { SavedObjectsErrorHelpers } from '..'; +import { CreatedObject } from './types'; describe('extractErrors()', () => { test('returns empty array when no errors exist', () => { @@ -29,7 +30,7 @@ describe('extractErrors()', () => { }); test('extracts errors from saved objects', () => { - const savedObjects: Array = [ + const savedObjects: Array> = [ { id: '1', type: 'dashboard', diff --git a/src/core/server/saved_objects/import/extract_errors.ts b/src/core/server/saved_objects/import/extract_errors.ts index 92196afc47ecd..b3223ade4b59d 100644 --- a/src/core/server/saved_objects/import/extract_errors.ts +++ b/src/core/server/saved_objects/import/extract_errors.ts @@ -17,11 +17,11 @@ * under the License. */ import { SavedObject } from '../types'; -import { SavedObjectsImportError } from './types'; +import { SavedObjectsImportError, CreatedObject } from './types'; export function extractErrors( // TODO: define saved object type - savedObjectResults: Array & { destinationId?: string }>, + savedObjectResults: Array>, savedObjectsToImport: Array> ) { const errors: SavedObjectsImportError[] = []; diff --git a/src/core/server/saved_objects/import/utilities.test.ts b/src/core/server/saved_objects/import/get_non_unique_entries.test.ts similarity index 95% rename from src/core/server/saved_objects/import/utilities.test.ts rename to src/core/server/saved_objects/import/get_non_unique_entries.test.ts index ccd40fa6a5621..a66fa437142d3 100644 --- a/src/core/server/saved_objects/import/utilities.test.ts +++ b/src/core/server/saved_objects/import/get_non_unique_entries.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { getNonUniqueEntries } from './utilities'; +import { getNonUniqueEntries } from './get_non_unique_entries'; const foo1 = { type: 'foo', id: '1' }; const foo2 = { type: 'foo', id: '2' }; // same type as foo1, different ID diff --git a/src/core/server/saved_objects/import/utilities.ts b/src/core/server/saved_objects/import/get_non_unique_entries.ts similarity index 100% rename from src/core/server/saved_objects/import/utilities.ts rename to src/core/server/saved_objects/import/get_non_unique_entries.ts diff --git a/src/core/server/saved_objects/import/types.ts b/src/core/server/saved_objects/import/types.ts index aa8479d4874de..6da78f8735ccb 100644 --- a/src/core/server/saved_objects/import/types.ts +++ b/src/core/server/saved_objects/import/types.ts @@ -18,7 +18,7 @@ */ import { Readable } from 'stream'; -import { SavedObjectsClientContract } from '../types'; +import { SavedObjectsClientContract, SavedObject } from '../types'; import { ISavedObjectTypeRegistry } from '..'; /** @@ -200,3 +200,5 @@ export interface SavedObjectsResolveImportErrorsOptions { */ trueCopy: boolean; } + +export type CreatedObject = SavedObject & { destinationId?: string }; diff --git a/src/core/server/saved_objects/import/validate_retries.test.ts b/src/core/server/saved_objects/import/validate_retries.test.ts index 7861e60a323c5..fd3c1e9795f9f 100644 --- a/src/core/server/saved_objects/import/validate_retries.test.ts +++ b/src/core/server/saved_objects/import/validate_retries.test.ts @@ -20,8 +20,8 @@ import { validateRetries } from './validate_retries'; import { SavedObjectsImportRetry } from '.'; -import { getNonUniqueEntries } from './utilities'; -jest.mock('./utilities'); +import { getNonUniqueEntries } from './get_non_unique_entries'; +jest.mock('./get_non_unique_entries'); const mockGetNonUniqueEntries = getNonUniqueEntries as jest.MockedFunction< typeof getNonUniqueEntries >; diff --git a/src/core/server/saved_objects/import/validate_retries.ts b/src/core/server/saved_objects/import/validate_retries.ts index f32c5bf82f989..f625436edb636 100644 --- a/src/core/server/saved_objects/import/validate_retries.ts +++ b/src/core/server/saved_objects/import/validate_retries.ts @@ -18,7 +18,7 @@ */ import { SavedObjectsImportRetry } from './types'; -import { getNonUniqueEntries } from './utilities'; +import { getNonUniqueEntries } from './get_non_unique_entries'; import { SavedObjectsErrorHelpers } from '..'; export const validateRetries = (retries: SavedObjectsImportRetry[]) => { diff --git a/x-pack/test/saved_object_api_integration/common/suites/import.ts b/x-pack/test/saved_object_api_integration/common/suites/import.ts index 58941cc16b07a..c52bf57a6f708 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/import.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/import.ts @@ -168,12 +168,6 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe type: 'ambiguous_conflict', destinations: [getConflictDest(`${CID}2a`), getConflictDest(`${CID}2b`)], }; - } else if (fail409Param === 'ambiguous_conflict_2c2d') { - // "ambiguous source and destination" conflict - error = { - type: 'ambiguous_conflict', - destinations: [getConflictDest(`${CID}2a`), getConflictDest(`${CID}2b`)], - }; } expect(object!.error).to.eql(error); } From 51e05f5e09a53a6eeaafc4072e4d4217c5392068 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 7 Jul 2020 19:44:18 -0400 Subject: [PATCH 38/55] Rename `trueCopy` mode to `createNewCopies` Also renamed `trueCopy` field on retries to `createNewCopy`, which better distinguishes it from the operation-level field. --- ...c.savedobjectsimportretry.createnewcopy.md | 16 ++++++++ ...gin-core-public.savedobjectsimportretry.md | 2 +- ...public.savedobjectsimportretry.truecopy.md | 16 -------- ...savedobjectsimportsuccess.createnewcopy.md | 16 ++++++++ ...n-core-public.savedobjectsimportsuccess.md | 2 +- ...blic.savedobjectsimportsuccess.truecopy.md | 16 -------- ...ore-server.importsavedobjectsfromstream.md | 4 +- .../core/server/kibana-plugin-core-server.md | 4 +- ...-server.resolvesavedobjectsimporterrors.md | 4 +- ...edobjectsimportoptions.createnewcopies.md} | 6 +-- ...n-core-server.savedobjectsimportoptions.md | 2 +- ...ver.savedobjectsimportoptions.overwrite.md | 2 +- ...r.savedobjectsimportretry.createnewcopy.md | 16 ++++++++ ...gin-core-server.savedobjectsimportretry.md | 2 +- ...server.savedobjectsimportretry.truecopy.md | 16 -------- ...savedobjectsimportsuccess.createnewcopy.md | 16 ++++++++ ...n-core-server.savedobjectsimportsuccess.md | 2 +- ...rver.savedobjectsimportsuccess.truecopy.md | 16 -------- ...lveimporterrorsoptions.createnewcopies.md} | 6 +-- ....savedobjectsresolveimporterrorsoptions.md | 2 +- src/core/public/public.api.md | 8 ++-- .../import/check_conflicts.test.ts | 10 ++--- .../saved_objects/import/check_conflicts.ts | 6 +-- .../import/check_origin_conflicts.test.ts | 14 +++---- .../import/check_origin_conflicts.ts | 8 ++-- .../import/import_saved_objects.test.ts | 28 ++++++++------ .../import/import_saved_objects.ts | 6 +-- .../import/resolve_import_errors.test.ts | 26 ++++++------- .../import/resolve_import_errors.ts | 10 ++--- src/core/server/saved_objects/import/types.ts | 22 +++++------ .../server/saved_objects/routes/import.ts | 10 ++--- .../routes/integration_tests/import.test.ts | 4 +- .../resolve_import_errors.test.ts | 4 +- .../routes/resolve_import_errors.ts | 6 +-- src/core/server/server.api.md | 20 +++++----- .../lib/copy_to_spaces/copy_to_spaces.test.ts | 10 ++--- .../lib/copy_to_spaces/copy_to_spaces.ts | 2 +- .../resolve_copy_conflicts.test.ts | 10 ++--- .../copy_to_spaces/resolve_copy_conflicts.ts | 6 +-- .../spaces/server/lib/copy_to_spaces/types.ts | 4 +- .../routes/api/external/copy_to_space.test.ts | 6 +-- .../routes/api/external/copy_to_space.ts | 18 ++++----- .../common/suites/import.ts | 37 +++++++++++-------- .../common/suites/resolve_import_errors.ts | 27 +++++++------- .../security_and_spaces/apis/import.ts | 28 ++++++++------ .../apis/resolve_import_errors.ts | 28 ++++++++------ .../security_only/apis/import.ts | 26 ++++++------- .../apis/resolve_import_errors.ts | 26 ++++++------- .../spaces_only/apis/import.ts | 20 +++++----- .../spaces_only/apis/resolve_import_errors.ts | 28 ++++++++------ 50 files changed, 324 insertions(+), 300 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.createnewcopy.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.truecopy.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.createnewcopy.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.truecopy.md rename docs/development/core/server/{kibana-plugin-core-server.savedobjectsimportoptions.truecopy.md => kibana-plugin-core-server.savedobjectsimportoptions.createnewcopies.md} (68%) create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.createnewcopy.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.truecopy.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.createnewcopy.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.truecopy.md rename docs/development/core/server/{kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.truecopy.md => kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.createnewcopies.md} (71%) diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.createnewcopy.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.createnewcopy.md new file mode 100644 index 0000000000000..69b5111daf691 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.createnewcopy.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportRetry](./kibana-plugin-core-public.savedobjectsimportretry.md) > [createNewCopy](./kibana-plugin-core-public.savedobjectsimportretry.createnewcopy.md) + +## SavedObjectsImportRetry.createNewCopy property + +> Warning: This API is now obsolete. +> +> If `createNewCopy` is specified, the new object has a new (undefined) origin ID. This is only needed for the case where `createNewCopies` mode is disabled and ambiguous source conflicts are detected. When `createNewCopies` mode is permanently enabled, this field will be redundant and can be removed. +> + +Signature: + +```typescript +createNewCopy?: boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.md index 148a7b146b47d..82c92fe0fb8e3 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.md @@ -16,10 +16,10 @@ export interface SavedObjectsImportRetry | Property | Type | Description | | --- | --- | --- | +| [createNewCopy](./kibana-plugin-core-public.savedobjectsimportretry.createnewcopy.md) | boolean | | | [destinationId](./kibana-plugin-core-public.savedobjectsimportretry.destinationid.md) | string | The object ID that will be created or overwritten. If not specified, the id field will be used. | | [id](./kibana-plugin-core-public.savedobjectsimportretry.id.md) | string | | | [overwrite](./kibana-plugin-core-public.savedobjectsimportretry.overwrite.md) | boolean | | | [replaceReferences](./kibana-plugin-core-public.savedobjectsimportretry.replacereferences.md) | Array<{
type: string;
from: string;
to: string;
}> | | -| [trueCopy](./kibana-plugin-core-public.savedobjectsimportretry.truecopy.md) | boolean | | | [type](./kibana-plugin-core-public.savedobjectsimportretry.type.md) | string | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.truecopy.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.truecopy.md deleted file mode 100644 index df0be400f15f1..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.truecopy.md +++ /dev/null @@ -1,16 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportRetry](./kibana-plugin-core-public.savedobjectsimportretry.md) > [trueCopy](./kibana-plugin-core-public.savedobjectsimportretry.truecopy.md) - -## SavedObjectsImportRetry.trueCopy property - -> Warning: This API is now obsolete. -> -> If `trueCopy` is specified, the new object has a new (undefined) origin ID. This is only needed for the case where True Copy mode is disabled and ambiguous source conflicts are detected. When True Copy mode is permanently enabled, this field will be redundant and can be removed. -> - -Signature: - -```typescript -trueCopy?: boolean; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.createnewcopy.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.createnewcopy.md new file mode 100644 index 0000000000000..0598691fbd525 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.createnewcopy.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportSuccess](./kibana-plugin-core-public.savedobjectsimportsuccess.md) > [createNewCopy](./kibana-plugin-core-public.savedobjectsimportsuccess.createnewcopy.md) + +## SavedObjectsImportSuccess.createNewCopy property + +> Warning: This API is now obsolete. +> +> If `createNewCopy` is specified, the new object has a new (undefined) origin ID. This is only needed for the case where `createNewCopies` mode is disabled and ambiguous source conflicts are detected. When `createNewCopies` mode is permanently enabled, this field will be redundant and can be removed. +> + +Signature: + +```typescript +createNewCopy?: boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.md index 92e6855e2f201..9eb39c96a1b78 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.md @@ -16,8 +16,8 @@ export interface SavedObjectsImportSuccess | Property | Type | Description | | --- | --- | --- | +| [createNewCopy](./kibana-plugin-core-public.savedobjectsimportsuccess.createnewcopy.md) | boolean | | | [destinationId](./kibana-plugin-core-public.savedobjectsimportsuccess.destinationid.md) | string | If destinationId is specified, the new object has a new ID that is different from the import ID. | | [id](./kibana-plugin-core-public.savedobjectsimportsuccess.id.md) | string | | -| [trueCopy](./kibana-plugin-core-public.savedobjectsimportsuccess.truecopy.md) | boolean | | | [type](./kibana-plugin-core-public.savedobjectsimportsuccess.type.md) | string | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.truecopy.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.truecopy.md deleted file mode 100644 index 6123830a65c5f..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.truecopy.md +++ /dev/null @@ -1,16 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportSuccess](./kibana-plugin-core-public.savedobjectsimportsuccess.md) > [trueCopy](./kibana-plugin-core-public.savedobjectsimportsuccess.truecopy.md) - -## SavedObjectsImportSuccess.trueCopy property - -> Warning: This API is now obsolete. -> -> If `trueCopy` is specified, the new object has a new (undefined) origin ID. This is only needed for the case where True Copy mode is disabled and ambiguous source conflicts are detected. When True Copy mode is permanently enabled, this field will be redundant and can be removed. -> - -Signature: - -```typescript -trueCopy?: boolean; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.importsavedobjectsfromstream.md b/docs/development/core/server/kibana-plugin-core-server.importsavedobjectsfromstream.md index bea33149927d7..cebebbaf94fe6 100644 --- a/docs/development/core/server/kibana-plugin-core-server.importsavedobjectsfromstream.md +++ b/docs/development/core/server/kibana-plugin-core-server.importsavedobjectsfromstream.md @@ -9,14 +9,14 @@ Import saved objects from given stream. See the [options](./kibana-plugin-core-s Signature: ```typescript -export declare function importSavedObjectsFromStream({ readStream, objectLimit, overwrite, trueCopy, savedObjectsClient, typeRegistry, namespace, }: SavedObjectsImportOptions): Promise; +export declare function importSavedObjectsFromStream({ readStream, objectLimit, overwrite, createNewCopies, savedObjectsClient, typeRegistry, namespace, }: SavedObjectsImportOptions): Promise; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| { readStream, objectLimit, overwrite, trueCopy, savedObjectsClient, typeRegistry, namespace, } | SavedObjectsImportOptions | | +| { readStream, objectLimit, overwrite, createNewCopies, savedObjectsClient, typeRegistry, namespace, } | SavedObjectsImportOptions | | Returns: diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 042c0ded4f1fc..19eb6357f6c26 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -45,10 +45,10 @@ The plugin integrates with the core system via lifecycle events: `setup` | [deepFreeze(object)](./kibana-plugin-core-server.deepfreeze.md) | Apply Object.freeze to a value recursively and convert the return type to Readonly variant recursively | | [exportSavedObjectsToStream({ types, objects, search, savedObjectsClient, exportSizeLimit, includeReferencesDeep, excludeExportDetails, namespace, })](./kibana-plugin-core-server.exportsavedobjectstostream.md) | Generates sorted saved object stream to be used for export. See the [options](./kibana-plugin-core-server.savedobjectsexportoptions.md) for more detailed information. | | [getFlattenedObject(rootValue)](./kibana-plugin-core-server.getflattenedobject.md) | Flattens a deeply nested object to a map of dot-separated paths pointing to all primitive values \*\*and arrays\*\* from rootValue.example: getFlattenedObject({ a: { b: 1, c: \[2,3\] } }) // => { 'a.b': 1, 'a.c': \[2,3\] } | -| [importSavedObjectsFromStream({ readStream, objectLimit, overwrite, trueCopy, savedObjectsClient, typeRegistry, namespace, })](./kibana-plugin-core-server.importsavedobjectsfromstream.md) | Import saved objects from given stream. See the [options](./kibana-plugin-core-server.savedobjectsimportoptions.md) for more detailed information. | +| [importSavedObjectsFromStream({ readStream, objectLimit, overwrite, createNewCopies, savedObjectsClient, typeRegistry, namespace, })](./kibana-plugin-core-server.importsavedobjectsfromstream.md) | Import saved objects from given stream. See the [options](./kibana-plugin-core-server.savedobjectsimportoptions.md) for more detailed information. | | [isRelativeUrl(candidatePath)](./kibana-plugin-core-server.isrelativeurl.md) | Determine if a url is relative. Any url including a protocol, hostname, or port is not considered relative. This means that absolute \*paths\* are considered to be relative \*urls\* | | [modifyUrl(url, urlModifier)](./kibana-plugin-core-server.modifyurl.md) | Takes a URL and a function that takes the meaningful parts of the URL as a key-value object, modifies some or all of the parts, and returns the modified parts formatted again as a url.Url Parts sent: - protocol - slashes (does the url have the //) - auth - hostname (just the name of the host, no port or auth information) - port - pathname (the path after the hostname, no query or hash, starts with a slash if there was a path) - query (always an object, even when no query on original url) - hashWhy? - The default url library in node produces several conflicting properties on the "parsed" output. Modifying any of these might lead to the modifications being ignored (depending on which property was modified) - It's not always clear whether to use path/pathname, host/hostname, so this tries to add helpful constraints | -| [resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, typeRegistry, namespace, trueCopy, })](./kibana-plugin-core-server.resolvesavedobjectsimporterrors.md) | Resolve and return saved object import errors. See the [options](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md) for more detailed informations. | +| [resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, typeRegistry, namespace, createNewCopies, })](./kibana-plugin-core-server.resolvesavedobjectsimporterrors.md) | Resolve and return saved object import errors. See the [options](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md) for more detailed informations. | ## Interfaces diff --git a/docs/development/core/server/kibana-plugin-core-server.resolvesavedobjectsimporterrors.md b/docs/development/core/server/kibana-plugin-core-server.resolvesavedobjectsimporterrors.md index 9be4db86a328d..a2255613e0f6c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.resolvesavedobjectsimporterrors.md +++ b/docs/development/core/server/kibana-plugin-core-server.resolvesavedobjectsimporterrors.md @@ -9,14 +9,14 @@ Resolve and return saved object import errors. See the [options](./kibana-plugin Signature: ```typescript -export declare function resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, typeRegistry, namespace, trueCopy, }: SavedObjectsResolveImportErrorsOptions): Promise; +export declare function resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, typeRegistry, namespace, createNewCopies, }: SavedObjectsResolveImportErrorsOptions): Promise; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| { readStream, objectLimit, retries, savedObjectsClient, typeRegistry, namespace, trueCopy, } | SavedObjectsResolveImportErrorsOptions | | +| { readStream, objectLimit, retries, savedObjectsClient, typeRegistry, namespace, createNewCopies, } | SavedObjectsResolveImportErrorsOptions | | Returns: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.truecopy.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.createnewcopies.md similarity index 68% rename from docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.truecopy.md rename to docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.createnewcopies.md index 89918e1ffc59e..86551edde097b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.truecopy.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.createnewcopies.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportOptions](./kibana-plugin-core-server.savedobjectsimportoptions.md) > [trueCopy](./kibana-plugin-core-server.savedobjectsimportoptions.truecopy.md) +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportOptions](./kibana-plugin-core-server.savedobjectsimportoptions.md) > [createNewCopies](./kibana-plugin-core-server.savedobjectsimportoptions.createnewcopies.md) -## SavedObjectsImportOptions.trueCopy property +## SavedObjectsImportOptions.createNewCopies property > Warning: This API is now obsolete. > @@ -12,5 +12,5 @@ Signature: ```typescript -trueCopy: boolean; +createNewCopies: boolean; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.md index ad2930a999490..dddc1889c3dd2 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.md @@ -16,11 +16,11 @@ export interface SavedObjectsImportOptions | Property | Type | Description | | --- | --- | --- | +| [createNewCopies](./kibana-plugin-core-server.savedobjectsimportoptions.createnewcopies.md) | boolean | | | [namespace](./kibana-plugin-core-server.savedobjectsimportoptions.namespace.md) | string | if specified, will import in given namespace, else will import as global object | | [objectLimit](./kibana-plugin-core-server.savedobjectsimportoptions.objectlimit.md) | number | The maximum number of object to import | | [overwrite](./kibana-plugin-core-server.savedobjectsimportoptions.overwrite.md) | boolean | | | [readStream](./kibana-plugin-core-server.savedobjectsimportoptions.readstream.md) | Readable | The stream of [saved objects](./kibana-plugin-core-server.savedobject.md) to import | | [savedObjectsClient](./kibana-plugin-core-server.savedobjectsimportoptions.savedobjectsclient.md) | SavedObjectsClientContract | [client](./kibana-plugin-core-server.savedobjectsclientcontract.md) to use to perform the import operation | -| [trueCopy](./kibana-plugin-core-server.savedobjectsimportoptions.truecopy.md) | boolean | | | [typeRegistry](./kibana-plugin-core-server.savedobjectsimportoptions.typeregistry.md) | ISavedObjectTypeRegistry | The registry of all known saved object types | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.overwrite.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.overwrite.md index e38e370d601ff..395071772c80e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.overwrite.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.overwrite.md @@ -8,7 +8,7 @@ > > If true, will override existing object if present. This option will be removed and permanently disabled in a future release. > -> Note: this has no effect when used with the `trueCopy` option. +> Note: this has no effect when used with the `createNewCopies` option. > Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.createnewcopy.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.createnewcopy.md new file mode 100644 index 0000000000000..0b59c98e6eda3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.createnewcopy.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportRetry](./kibana-plugin-core-server.savedobjectsimportretry.md) > [createNewCopy](./kibana-plugin-core-server.savedobjectsimportretry.createnewcopy.md) + +## SavedObjectsImportRetry.createNewCopy property + +> Warning: This API is now obsolete. +> +> If `createNewCopy` is specified, the new object has a new (undefined) origin ID. This is only needed for the case where `createNewCopies` mode is disabled and ambiguous source conflicts are detected. When `createNewCopies` mode is permanently enabled, this field will be redundant and can be removed. +> + +Signature: + +```typescript +createNewCopy?: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.md index 4053ec49961a2..3759bb4e72ff8 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.md @@ -16,10 +16,10 @@ export interface SavedObjectsImportRetry | Property | Type | Description | | --- | --- | --- | +| [createNewCopy](./kibana-plugin-core-server.savedobjectsimportretry.createnewcopy.md) | boolean | | | [destinationId](./kibana-plugin-core-server.savedobjectsimportretry.destinationid.md) | string | The object ID that will be created or overwritten. If not specified, the id field will be used. | | [id](./kibana-plugin-core-server.savedobjectsimportretry.id.md) | string | | | [overwrite](./kibana-plugin-core-server.savedobjectsimportretry.overwrite.md) | boolean | | | [replaceReferences](./kibana-plugin-core-server.savedobjectsimportretry.replacereferences.md) | Array<{
type: string;
from: string;
to: string;
}> | | -| [trueCopy](./kibana-plugin-core-server.savedobjectsimportretry.truecopy.md) | boolean | | | [type](./kibana-plugin-core-server.savedobjectsimportretry.type.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.truecopy.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.truecopy.md deleted file mode 100644 index 03e14f7e3cf59..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.truecopy.md +++ /dev/null @@ -1,16 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportRetry](./kibana-plugin-core-server.savedobjectsimportretry.md) > [trueCopy](./kibana-plugin-core-server.savedobjectsimportretry.truecopy.md) - -## SavedObjectsImportRetry.trueCopy property - -> Warning: This API is now obsolete. -> -> If `trueCopy` is specified, the new object has a new (undefined) origin ID. This is only needed for the case where True Copy mode is disabled and ambiguous source conflicts are detected. When True Copy mode is permanently enabled, this field will be redundant and can be removed. -> - -Signature: - -```typescript -trueCopy?: boolean; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.createnewcopy.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.createnewcopy.md new file mode 100644 index 0000000000000..66b7a268f2ed5 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.createnewcopy.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportSuccess](./kibana-plugin-core-server.savedobjectsimportsuccess.md) > [createNewCopy](./kibana-plugin-core-server.savedobjectsimportsuccess.createnewcopy.md) + +## SavedObjectsImportSuccess.createNewCopy property + +> Warning: This API is now obsolete. +> +> If `createNewCopy` is specified, the new object has a new (undefined) origin ID. This is only needed for the case where `createNewCopies` mode is disabled and ambiguous source conflicts are detected. When `createNewCopies` mode is permanently enabled, this field will be redundant and can be removed. +> + +Signature: + +```typescript +createNewCopy?: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.md index b511ae3635ff0..8887dbe33d019 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.md @@ -16,8 +16,8 @@ export interface SavedObjectsImportSuccess | Property | Type | Description | | --- | --- | --- | +| [createNewCopy](./kibana-plugin-core-server.savedobjectsimportsuccess.createnewcopy.md) | boolean | | | [destinationId](./kibana-plugin-core-server.savedobjectsimportsuccess.destinationid.md) | string | If destinationId is specified, the new object has a new ID that is different from the import ID. | | [id](./kibana-plugin-core-server.savedobjectsimportsuccess.id.md) | string | | -| [trueCopy](./kibana-plugin-core-server.savedobjectsimportsuccess.truecopy.md) | boolean | | | [type](./kibana-plugin-core-server.savedobjectsimportsuccess.type.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.truecopy.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.truecopy.md deleted file mode 100644 index 9e9edc92d58b5..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.truecopy.md +++ /dev/null @@ -1,16 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportSuccess](./kibana-plugin-core-server.savedobjectsimportsuccess.md) > [trueCopy](./kibana-plugin-core-server.savedobjectsimportsuccess.truecopy.md) - -## SavedObjectsImportSuccess.trueCopy property - -> Warning: This API is now obsolete. -> -> If `trueCopy` is specified, the new object has a new (undefined) origin ID. This is only needed for the case where True Copy mode is disabled and ambiguous source conflicts are detected. When True Copy mode is permanently enabled, this field will be redundant and can be removed. -> - -Signature: - -```typescript -trueCopy?: boolean; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.truecopy.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.createnewcopies.md similarity index 71% rename from docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.truecopy.md rename to docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.createnewcopies.md index 394e332b208e6..511ecaafc27e4 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.truecopy.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.createnewcopies.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsResolveImportErrorsOptions](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md) > [trueCopy](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.truecopy.md) +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsResolveImportErrorsOptions](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md) > [createNewCopies](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.createnewcopies.md) -## SavedObjectsResolveImportErrorsOptions.trueCopy property +## SavedObjectsResolveImportErrorsOptions.createNewCopies property > Warning: This API is now obsolete. > @@ -12,5 +12,5 @@ Signature: ```typescript -trueCopy: boolean; +createNewCopies: boolean; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md index 8c3f2d446ffff..8de15c9ff407b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md @@ -16,11 +16,11 @@ export interface SavedObjectsResolveImportErrorsOptions | Property | Type | Description | | --- | --- | --- | +| [createNewCopies](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.createnewcopies.md) | boolean | | | [namespace](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.namespace.md) | string | if specified, will import in given namespace | | [objectLimit](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.objectlimit.md) | number | The maximum number of object to import | | [readStream](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.readstream.md) | Readable | The stream of [saved objects](./kibana-plugin-core-server.savedobject.md) to resolve errors from | | [retries](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.retries.md) | SavedObjectsImportRetry[] | saved object import references to retry | | [savedObjectsClient](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.savedobjectsclient.md) | SavedObjectsClientContract | client to use to perform the import operation | -| [trueCopy](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.truecopy.md) | boolean | | | [typeRegistry](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.typeregistry.md) | ISavedObjectTypeRegistry | The registry of all known saved object types | diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index a32292dd293c5..62e7393626902 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -1375,6 +1375,8 @@ export interface SavedObjectsImportResponse { // @public export interface SavedObjectsImportRetry { + // @deprecated (undocumented) + createNewCopy?: boolean; destinationId?: string; // (undocumented) id: string; @@ -1386,19 +1388,17 @@ export interface SavedObjectsImportRetry { from: string; to: string; }>; - // @deprecated (undocumented) - trueCopy?: boolean; // (undocumented) type: string; } // @public export interface SavedObjectsImportSuccess { + // @deprecated (undocumented) + createNewCopy?: boolean; destinationId?: string; // (undocumented) id: string; - // @deprecated (undocumented) - trueCopy?: boolean; // (undocumented) type: string; } diff --git a/src/core/server/saved_objects/import/check_conflicts.test.ts b/src/core/server/saved_objects/import/check_conflicts.test.ts index 85fe7a482e886..8fbc1578be185 100644 --- a/src/core/server/saved_objects/import/check_conflicts.test.ts +++ b/src/core/server/saved_objects/import/check_conflicts.test.ts @@ -77,14 +77,14 @@ describe('#checkConflicts', () => { options: { namespace?: string; ignoreRegularConflicts?: boolean; - trueCopy?: boolean; + createNewCopies?: boolean; } = {} ): CheckConflictsOptions => { - const { namespace, ignoreRegularConflicts, trueCopy } = options; + const { namespace, ignoreRegularConflicts, createNewCopies } = options; savedObjectsClient = savedObjectsClientMock.create(); socCheckConflicts = savedObjectsClient.checkConflicts; socCheckConflicts.mockResolvedValue({ errors: [] }); // by default, mock to empty results - return { savedObjectsClient, namespace, ignoreRegularConflicts, trueCopy }; + return { savedObjectsClient, namespace, ignoreRegularConflicts, createNewCopies }; }; beforeEach(() => { @@ -154,9 +154,9 @@ describe('#checkConflicts', () => { ); }); - it('adds `omitOriginId` field to `importIdMap` entries when trueCopy=true', async () => { + it('adds `omitOriginId` field to `importIdMap` entries when createNewCopies=true', async () => { const namespace = 'foo-namespace'; - const options = setupOptions({ namespace, trueCopy: true }); + const options = setupOptions({ namespace, createNewCopies: true }); socCheckConflicts.mockResolvedValue({ errors: [obj2Error, obj3Error, obj4Error] }); const checkConflictsResult = await checkConflicts(objects, options); diff --git a/src/core/server/saved_objects/import/check_conflicts.ts b/src/core/server/saved_objects/import/check_conflicts.ts index 5dcd5bdfe68ba..65b078021792a 100644 --- a/src/core/server/saved_objects/import/check_conflicts.ts +++ b/src/core/server/saved_objects/import/check_conflicts.ts @@ -29,7 +29,7 @@ interface CheckConflictsOptions { savedObjectsClient: SavedObjectsClientContract; namespace?: string; ignoreRegularConflicts?: boolean; - trueCopy?: boolean; + createNewCopies?: boolean; } const isUnresolvableConflict = (error: SavedObjectError) => @@ -48,7 +48,7 @@ export async function checkConflicts( return { filteredObjects, errors, importIdMap }; } - const { savedObjectsClient, namespace, ignoreRegularConflicts, trueCopy } = options; + const { savedObjectsClient, namespace, ignoreRegularConflicts, createNewCopies } = options; const checkConflictsResult = await savedObjectsClient.checkConflicts(objects, { namespace }); const errorMap = checkConflictsResult.errors.reduce( (acc, { type, id, error }) => acc.set(`${type}:${id}`, error), @@ -67,7 +67,7 @@ export async function checkConflicts( // with a "multi-namespace" type is exported from one namespace and imported to another, it does not result in an error, but instead a // new object is created. const destinationId = uuidv4(); - importIdMap.set(`${type}:${id}`, { id: destinationId, omitOriginId: trueCopy }); + importIdMap.set(`${type}:${id}`, { id: destinationId, omitOriginId: createNewCopies }); filteredObjects.push(object); } else if (errorObj && errorObj.statusCode !== 409) { errors.push({ type, id, title, error: { ...errorObj, type: 'unknown' } }); diff --git a/src/core/server/saved_objects/import/check_origin_conflicts.test.ts b/src/core/server/saved_objects/import/check_origin_conflicts.test.ts index 6ba5ef2c85d43..92d83e675cf6c 100644 --- a/src/core/server/saved_objects/import/check_origin_conflicts.test.ts +++ b/src/core/server/saved_objects/import/check_origin_conflicts.test.ts @@ -525,17 +525,17 @@ describe('#checkOriginConflicts', () => { describe('#getImportIdMapForRetries', () => { const setupOptions = ( retries: SavedObjectsImportRetry[], - trueCopy: boolean = false + createNewCopies: boolean = false ): GetImportIdMapForRetriesOptions => { - return { retries, trueCopy }; + return { retries, createNewCopies }; }; const createRetry = ( { type, id }: { type: string; id: string }, - options: { destinationId?: string; trueCopy?: boolean } = {} + options: { destinationId?: string; createNewCopy?: boolean } = {} ): SavedObjectsImportRetry => { - const { destinationId, trueCopy } = options; - return { type, id, overwrite: false, destinationId, replaceReferences: [], trueCopy }; + const { destinationId, createNewCopy } = options; + return { type, id, overwrite: false, destinationId, replaceReferences: [], createNewCopy }; }; test('throws an error if retry is not found for an object', async () => { @@ -559,7 +559,7 @@ describe('#getImportIdMapForRetries', () => { createRetry(obj1), // retries that do not have `destinationId` specified are ignored createRetry(obj2, { destinationId: obj2.id }), // retries that have `id` that matches `destinationId` are ignored createRetry(obj3, { destinationId: 'id-X' }), // this retry will get added to the `importIdMap`! - createRetry(obj4, { destinationId: 'id-Y', trueCopy: true }), // this retry will get added to the `importIdMap`! + createRetry(obj4, { destinationId: 'id-Y', createNewCopy: true }), // this retry will get added to the `importIdMap`! ]; const options = setupOptions(retries); @@ -572,7 +572,7 @@ describe('#getImportIdMapForRetries', () => { ); }); - test('omits origin ID in `importIdMap` entries when trueCopy=true', async () => { + test('omits origin ID in `importIdMap` entries when createNewCopies=true', async () => { const obj = createObject('type-1', 'id-1'); const objects = [obj]; const retries = [createRetry(obj, { destinationId: 'id-X' })]; diff --git a/src/core/server/saved_objects/import/check_origin_conflicts.ts b/src/core/server/saved_objects/import/check_origin_conflicts.ts index 26fcbdb916dbc..db838b949dae8 100644 --- a/src/core/server/saved_objects/import/check_origin_conflicts.ts +++ b/src/core/server/saved_objects/import/check_origin_conflicts.ts @@ -36,7 +36,7 @@ interface CheckOriginConflictsOptions { interface GetImportIdMapForRetriesOptions { retries: SavedObjectsImportRetry[]; - trueCopy: boolean; + createNewCopies: boolean; } interface InexactMatch { @@ -193,7 +193,7 @@ export async function checkOriginConflicts( // - 2+ import objects have the same 2+ destination conflicts ("ambiguous source and destination") if (sources.length > 1) { // In the case of ambiguous source conflicts, don't treat them as errors; instead, regenerate the object ID and reset its origin - // (e.g., make a "true copy"). + // (e.g., the same outcome as if `createNewCopies` was enabled for the entire import operation). importIdMap.set(`${type}:${id}`, { id: uuidv4(), omitOriginId: true }); filteredObjects.push(object); return; @@ -223,7 +223,7 @@ export function getImportIdMapForRetries( objects: Array>, options: GetImportIdMapForRetriesOptions ) { - const { retries, trueCopy } = options; + const { retries, createNewCopies } = options; const retryMap = retries.reduce( (acc, cur) => acc.set(`${cur.type}:${cur.id}`, cur), @@ -237,7 +237,7 @@ export function getImportIdMapForRetries( throw new Error(`Retry was expected for "${type}:${id}" but not found`); } const { destinationId } = retry; - const omitOriginId = trueCopy || Boolean(retry.trueCopy); + const omitOriginId = createNewCopies || Boolean(retry.createNewCopy); if (destinationId && destinationId !== id) { importIdMap.set(`${type}:${id}`, { id: destinationId, omitOriginId }); } 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 0a155a051974e..62a0e4d3639e4 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 @@ -78,7 +78,7 @@ describe('#importSavedObjectsFromStream', () => { let typeRegistry: jest.Mocked; const namespace = 'some-namespace'; - const setupOptions = (trueCopy: boolean = false): SavedObjectsImportOptions => { + const setupOptions = (createNewCopies: boolean = false): SavedObjectsImportOptions => { readStream = new Readable(); savedObjectsClient = savedObjectsClientMock.create(); typeRegistry = typeRegistryMock.create(); @@ -89,7 +89,7 @@ describe('#importSavedObjectsFromStream', () => { savedObjectsClient, typeRegistry, namespace, - trueCopy, + createNewCopies, }; }; const createObject = () => { @@ -136,7 +136,7 @@ describe('#importSavedObjectsFromStream', () => { ); }); - describe('with trueCopy disabled', () => { + describe('with createNewCopies disabled', () => { test('does not regenerate object IDs', async () => { const options = setupOptions(); const collectedObjects = [createObject()]; @@ -231,7 +231,7 @@ describe('#importSavedObjectsFromStream', () => { }); }); - describe('with trueCopy enabled', () => { + describe('with createNewCopies enabled', () => { test('regenerates object IDs', async () => { const options = setupOptions(true); const collectedObjects = [createObject()]; @@ -312,7 +312,7 @@ describe('#importSavedObjectsFromStream', () => { const createdObjects = [obj1, obj2, obj3]; const errors = [createError()]; - test('with trueCopy disabled', async () => { + test('with createNewCopies disabled', async () => { const options = setupOptions(); getMockFn(createSavedObjects).mockResolvedValue({ errors, createdObjects }); @@ -321,15 +321,21 @@ describe('#importSavedObjectsFromStream', () => { const successResults = [ { type: obj1.type, id: obj1.id }, { type: obj2.type, id: obj2.id, destinationId: obj2.destinationId }, - // trueCopy mode is not enabled, but obj3 ran into an ambiguous source conflict and it was created with an empty originId; hence, - // this specific object is a true copy -- we would need this information for rendering the appropriate originId in the client UI, - // and we would need it to construct a retry for this object if other objects had errors that needed to be resolved - { type: obj3.type, id: obj3.id, destinationId: obj3.destinationId, trueCopy: true }, + // `createNewCopies` mode is not enabled, but obj3 ran into an ambiguous source conflict and it was created with an empty + // originId; hence, this specific object is a new copy -- we would need this information for rendering the appropriate originId + // in the client UI, and we would need it to construct a retry for this object if other objects had errors that needed to be + // resolved + { + type: obj3.type, + id: obj3.id, + destinationId: obj3.destinationId, + createNewCopy: true, + }, ]; expect(result).toEqual({ success: false, successCount: 3, successResults, errors }); }); - test('with trueCopy enabled', async () => { + test('with createNewCopies enabled', async () => { // however, we include it here for posterity const options = setupOptions(true); getMockFn(createSavedObjects).mockResolvedValue({ errors, createdObjects }); @@ -338,7 +344,7 @@ describe('#importSavedObjectsFromStream', () => { // successResults only includes the imported object's type, id, and destinationId (if a new one was generated) const successResults = [ { type: obj1.type, id: obj1.id }, - // obj2 being created with trueCopy mode enabled isn't a realistic test case (all objects would have originId omitted) + // obj2 being created with createNewCopies mode enabled isn't a realistic test case (all objects would have originId omitted) { type: obj2.type, id: obj2.id, destinationId: obj2.destinationId }, { type: obj3.type, id: obj3.id, destinationId: obj3.destinationId }, ]; 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 c6ce179843a93..d3244471378d3 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.ts @@ -39,7 +39,7 @@ export async function importSavedObjectsFromStream({ readStream, objectLimit, overwrite, - trueCopy, + createNewCopies, savedObjectsClient, typeRegistry, namespace, @@ -66,7 +66,7 @@ export async function importSavedObjectsFromStream({ errorAccumulator = [...errorAccumulator, ...validateReferencesResult.errors]; let objectsToCreate = validateReferencesResult.filteredObjects; - if (trueCopy) { + if (createNewCopies) { const regenerateIdsResult = regenerateIds(collectSavedObjectsResult.collectedObjects); importIdMap = regenerateIdsResult.importIdMap; } else { @@ -115,7 +115,7 @@ export async function importSavedObjectsFromStream({ type, id, ...(destinationId && { destinationId }), - ...(destinationId && !originId && !trueCopy && { trueCopy: true }), + ...(destinationId && !originId && !createNewCopies && { createNewCopy: true }), }; } ); 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 c7af59d65e91e..b617ce3efec8e 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 @@ -88,7 +88,7 @@ describe('#importSavedObjectsFromStream', () => { const setupOptions = ( retries: SavedObjectsImportRetry[] = [], - trueCopy: boolean = false + createNewCopies: boolean = false ): SavedObjectsResolveImportErrorsOptions => { readStream = new Readable(); savedObjectsClient = savedObjectsClientMock.create(); @@ -99,9 +99,9 @@ describe('#importSavedObjectsFromStream', () => { retries, savedObjectsClient, typeRegistry, - // namespace and trueCopy don't matter, as they don't change the logic in this module, they just get passed to sub-module methods + // namespace and createNewCopies don't matter, as they don't change the logic in this module, they just get passed to sub-module methods namespace, - trueCopy, + createNewCopies, }; }; @@ -203,8 +203,8 @@ describe('#importSavedObjectsFromStream', () => { }); test('checks conflicts', async () => { - const trueCopy = (Symbol() as unknown) as boolean; - const options = setupOptions([], trueCopy); + const createNewCopies = (Symbol() as unknown) as boolean; + const options = setupOptions([], createNewCopies); const filteredObjects = [createObject()]; getMockFn(validateReferences).mockResolvedValue({ errors: [], filteredObjects }); @@ -213,15 +213,15 @@ describe('#importSavedObjectsFromStream', () => { savedObjectsClient, namespace, ignoreRegularConflicts: true, - trueCopy, + createNewCopies, }; expect(checkConflicts).toHaveBeenCalledWith(filteredObjects, checkConflictsOptions); }); test('gets import ID map for retries', async () => { const retries = [createRetry()]; - const trueCopy = (Symbol() as unknown) as boolean; - const options = setupOptions(retries, trueCopy); + const createNewCopies = (Symbol() as unknown) as boolean; + const options = setupOptions(retries, createNewCopies); const filteredObjects = [createObject()]; getMockFn(checkConflicts).mockResolvedValue({ errors: [], @@ -230,7 +230,7 @@ describe('#importSavedObjectsFromStream', () => { }); await resolveSavedObjectsImportErrors(options); - const opts = { retries, trueCopy }; + const opts = { retries, createNewCopies }; expect(getImportIdMapForRetries).toHaveBeenCalledWith(filteredObjects, opts); }); @@ -248,7 +248,7 @@ describe('#importSavedObjectsFromStream', () => { expect(splitOverwrites).toHaveBeenCalledWith(filteredObjects, retries); }); - describe('with trueCopy disabled', () => { + describe('with createNewCopies disabled', () => { test('does not regenerate object IDs', async () => { const options = setupOptions(); const collectedObjects = [createObject()]; @@ -310,7 +310,7 @@ describe('#importSavedObjectsFromStream', () => { }); }); - describe('with trueCopy enabled', () => { + describe('with createNewCopies enabled', () => { test('regenerates object IDs', async () => { const options = setupOptions([], true); const collectedObjects = [createObject()]; @@ -408,7 +408,7 @@ describe('#importSavedObjectsFromStream', () => { const obj1 = createObject(); const tmp = createObject(); const obj2 = { ...tmp, destinationId: 'some-destinationId', originId: tmp.id }; - const obj3 = { ...createObject(), destinationId: 'another-destinationId' }; // empty originId; this is a true copy + const obj3 = { ...createObject(), destinationId: 'another-destinationId' }; // empty originId; this is a new copy getMockFn(createSavedObjects).mockResolvedValueOnce({ errors, createdObjects: [obj1], @@ -423,7 +423,7 @@ describe('#importSavedObjectsFromStream', () => { const successResults = [ { type: obj1.type, id: obj1.id }, { type: obj2.type, id: obj2.id, destinationId: obj2.destinationId }, - { type: obj3.type, id: obj3.id, destinationId: obj3.destinationId, trueCopy: true }, + { type: obj3.type, id: obj3.id, destinationId: obj3.destinationId, createNewCopy: true }, ]; expect(result).toEqual({ success: false, successCount: 3, successResults, errors }); }); 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 103c87e26875b..81b48ef51338f 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.ts @@ -45,7 +45,7 @@ export async function resolveSavedObjectsImportErrors({ savedObjectsClient, typeRegistry, namespace, - trueCopy, + createNewCopies, }: SavedObjectsResolveImportErrorsOptions): Promise { // throw a BadRequest error if we see invalid retries validateRetries(retries); @@ -99,7 +99,7 @@ export async function resolveSavedObjectsImportErrors({ ); errorAccumulator = [...errorAccumulator, ...validateReferencesResult.errors]; - if (trueCopy) { + if (createNewCopies) { // In case any missing reference errors were resolved, ensure that we regenerate those object IDs as well // This is because a retry to resolve a missing reference error may not necessarily specify a destinationId const regenerateIdsResult = regenerateIds(objectsToResolve); @@ -111,7 +111,7 @@ export async function resolveSavedObjectsImportErrors({ savedObjectsClient, namespace, ignoreRegularConflicts: true, - trueCopy, + createNewCopies, }; const checkConflictsResult = await checkConflicts( validateReferencesResult.filteredObjects, @@ -123,7 +123,7 @@ export async function resolveSavedObjectsImportErrors({ // Check multi-namespace object types for regular conflicts and ambiguous conflicts const importIdMapForRetries = getImportIdMapForRetries(checkConflictsResult.filteredObjects, { retries, - trueCopy, + createNewCopies, }); importIdMap = new Map([...importIdMap, ...importIdMapForRetries]); @@ -145,7 +145,7 @@ export async function resolveSavedObjectsImportErrors({ type, id, ...(destinationId && { destinationId }), - ...(destinationId && !originId && !trueCopy && { trueCopy: true }), + ...(destinationId && !originId && !createNewCopies && { createNewCopy: true }), })), ]; }; diff --git a/src/core/server/saved_objects/import/types.ts b/src/core/server/saved_objects/import/types.ts index 6da78f8735ccb..0d153f26f2033 100644 --- a/src/core/server/saved_objects/import/types.ts +++ b/src/core/server/saved_objects/import/types.ts @@ -40,11 +40,11 @@ export interface SavedObjectsImportRetry { }>; /** * @deprecated - * If `trueCopy` is specified, the new object has a new (undefined) origin ID. This is only needed for the case where True Copy mode is - * disabled and ambiguous source conflicts are detected. When True Copy mode is permanently enabled, this field will be redundant and can - * be removed. + * If `createNewCopy` is specified, the new object has a new (undefined) origin ID. This is only needed for the case where + * `createNewCopies` mode is disabled and ambiguous source conflicts are detected. When `createNewCopies` mode is permanently enabled, + * this field will be redundant and can be removed. */ - trueCopy?: boolean; + createNewCopy?: boolean; } /** @@ -128,11 +128,11 @@ export interface SavedObjectsImportSuccess { destinationId?: string; /** * @deprecated - * If `trueCopy` is specified, the new object has a new (undefined) origin ID. This is only needed for the case where True Copy mode is - * disabled and ambiguous source conflicts are detected. When True Copy mode is permanently enabled, this field will be redundant and can - * be removed. + * If `createNewCopy` is specified, the new object has a new (undefined) origin ID. This is only needed for the case where + * `createNewCopies` mode is disabled and ambiguous source conflicts are detected. When `createNewCopies` mode is permanently enabled, + * this field will be redundant and can be removed. */ - trueCopy?: boolean; + createNewCopy?: boolean; } /** @@ -159,7 +159,7 @@ export interface SavedObjectsImportOptions { * @deprecated * If true, will override existing object if present. This option will be removed and permanently disabled in a future release. * - * Note: this has no effect when used with the `trueCopy` option. + * Note: this has no effect when used with the `createNewCopies` option. */ overwrite: boolean; /** {@link SavedObjectsClientContract | client} to use to perform the import operation */ @@ -173,7 +173,7 @@ export interface SavedObjectsImportOptions { * If true, will create new copies of import objects, each with a random `id` and undefined `originId`. This option will be removed and * permanently enabled in a future release. */ - trueCopy: boolean; + createNewCopies: boolean; } /** @@ -198,7 +198,7 @@ export interface SavedObjectsResolveImportErrorsOptions { * If true, will create new copies of import objects, each with a random `id` and undefined `originId`. This option will be removed and * permanently enabled in a future release. */ - trueCopy: boolean; + createNewCopies: boolean; } export type CreatedObject = SavedObject & { destinationId?: string }; diff --git a/src/core/server/saved_objects/routes/import.ts b/src/core/server/saved_objects/routes/import.ts index acdb00e6c12bf..4fac8fede0cd9 100644 --- a/src/core/server/saved_objects/routes/import.ts +++ b/src/core/server/saved_objects/routes/import.ts @@ -48,12 +48,12 @@ export const registerImportRoute = (router: IRouter, config: SavedObjectConfig) query: schema.object( { overwrite: schema.boolean({ defaultValue: false }), - trueCopy: schema.boolean({ defaultValue: false }), + createNewCopies: schema.boolean({ defaultValue: false }), }, { validate: (object) => { - if (object.overwrite && object.trueCopy) { - return 'cannot use [overwrite] with [trueCopy]'; + if (object.overwrite && object.createNewCopies) { + return 'cannot use [overwrite] with [createNewCopies]'; } }, } @@ -64,7 +64,7 @@ export const registerImportRoute = (router: IRouter, config: SavedObjectConfig) }, }, router.handleLegacyErrors(async (context, req, res) => { - const { overwrite, trueCopy } = req.query; + const { overwrite, createNewCopies } = req.query; const file = req.body.file as FileStream; const fileExtension = extname(file.hapi.filename).toLowerCase(); if (fileExtension !== '.ndjson') { @@ -77,7 +77,7 @@ export const registerImportRoute = (router: IRouter, config: SavedObjectConfig) readStream: createSavedObjectsStreamFromNdJson(file), objectLimit: maxImportExportSize, overwrite, - trueCopy, + createNewCopies, }); return res.ok({ body: result }); diff --git a/src/core/server/saved_objects/routes/integration_tests/import.test.ts b/src/core/server/saved_objects/routes/integration_tests/import.test.ts index a9722c13c6563..4759e36160698 100644 --- a/src/core/server/saved_objects/routes/integration_tests/import.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/import.test.ts @@ -254,7 +254,7 @@ describe(`POST ${URL}`, () => { expect(savedObjectsClient.bulkCreate).not.toHaveBeenCalled(); // no objects were created }); - describe('trueCopy enabled', () => { + describe('createNewCopies enabled', () => { it('imports objects, regenerating all IDs/reference IDs present, and resetting all origin IDs', async () => { mockUuidv4.mockReturnValueOnce('new-id-1').mockReturnValueOnce('new-id-2'); savedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [mockIndexPattern] }); @@ -266,7 +266,7 @@ describe(`POST ${URL}`, () => { }); const result = await supertest(httpSetup.server.listener) - .post(`${URL}?trueCopy=true`) + .post(`${URL}?createNewCopies=true`) .set('content-Type', 'multipart/form-data; boundary=EXAMPLE') .send( [ diff --git a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts index 7a3d933f1d2e6..d063adbdbd9e9 100644 --- a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts @@ -244,7 +244,7 @@ describe(`POST ${URL}`, () => { ); }); - describe('trueCopy enabled', () => { + describe('createNewCopies enabled', () => { it('imports objects, regenerating all IDs/reference IDs present, and resetting all origin IDs', async () => { mockUuidv4.mockReturnValue('new-id-1'); savedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [mockIndexPattern] }); @@ -256,7 +256,7 @@ describe(`POST ${URL}`, () => { }); const result = await supertest(httpSetup.server.listener) - .post(`${URL}?trueCopy=true`) + .post(`${URL}?createNewCopies=true`) .set('content-Type', 'multipart/form-data; boundary=EXAMPLE') .send( [ diff --git a/src/core/server/saved_objects/routes/resolve_import_errors.ts b/src/core/server/saved_objects/routes/resolve_import_errors.ts index 7cdfb3e571e19..bdafdaaaf019a 100644 --- a/src/core/server/saved_objects/routes/resolve_import_errors.ts +++ b/src/core/server/saved_objects/routes/resolve_import_errors.ts @@ -46,7 +46,7 @@ export const registerResolveImportErrorsRoute = (router: IRouter, config: SavedO }, validate: { query: schema.object({ - trueCopy: schema.boolean({ defaultValue: false }), + createNewCopies: schema.boolean({ defaultValue: false }), }), body: schema.object({ file: schema.stream(), @@ -64,7 +64,7 @@ export const registerResolveImportErrorsRoute = (router: IRouter, config: SavedO }), { defaultValue: [] } ), - trueCopy: schema.maybe(schema.boolean()), + createNewCopy: schema.maybe(schema.boolean()), }) ), }), @@ -83,7 +83,7 @@ export const registerResolveImportErrorsRoute = (router: IRouter, config: SavedO readStream: createSavedObjectsStreamFromNdJson(file), retries: req.body.retries, objectLimit: maxImportExportSize, - trueCopy: req.query.trueCopy, + createNewCopies: req.query.createNewCopies, }); return res.ok({ body: result }); diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 85a59e2c46058..6db0117234dff 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -835,7 +835,7 @@ export interface ImageValidation { } // @public -export function importSavedObjectsFromStream({ readStream, objectLimit, overwrite, trueCopy, savedObjectsClient, typeRegistry, namespace, }: SavedObjectsImportOptions): Promise; +export function importSavedObjectsFromStream({ readStream, objectLimit, overwrite, createNewCopies, savedObjectsClient, typeRegistry, namespace, }: SavedObjectsImportOptions): Promise; // @public (undocumented) export interface IndexSettingsDeprecationInfo { @@ -1702,7 +1702,7 @@ export type RequestHandlerContextProvider(handler: RequestHandler) => RequestHandler; // @public -export function resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, typeRegistry, namespace, trueCopy, }: SavedObjectsResolveImportErrorsOptions): Promise; +export function resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, typeRegistry, namespace, createNewCopies, }: SavedObjectsResolveImportErrorsOptions): Promise; // @public export type ResponseError = string | Error | { @@ -2228,14 +2228,14 @@ export interface SavedObjectsImportMissingReferencesError { // @public export interface SavedObjectsImportOptions { + // @deprecated (undocumented) + createNewCopies: boolean; namespace?: string; objectLimit: number; // @deprecated (undocumented) overwrite: boolean; readStream: Readable; savedObjectsClient: SavedObjectsClientContract; - // @deprecated (undocumented) - trueCopy: boolean; typeRegistry: ISavedObjectTypeRegistry; } @@ -2253,6 +2253,8 @@ export interface SavedObjectsImportResponse { // @public export interface SavedObjectsImportRetry { + // @deprecated (undocumented) + createNewCopy?: boolean; destinationId?: string; // (undocumented) id: string; @@ -2264,19 +2266,17 @@ export interface SavedObjectsImportRetry { from: string; to: string; }>; - // @deprecated (undocumented) - trueCopy?: boolean; // (undocumented) type: string; } // @public export interface SavedObjectsImportSuccess { + // @deprecated (undocumented) + createNewCopy?: boolean; destinationId?: string; // (undocumented) id: string; - // @deprecated (undocumented) - trueCopy?: boolean; // (undocumented) type: string; } @@ -2406,13 +2406,13 @@ export interface SavedObjectsRepositoryFactory { // @public export interface SavedObjectsResolveImportErrorsOptions { + // @deprecated (undocumented) + createNewCopies: boolean; namespace?: string; objectLimit: number; readStream: Readable; retries: SavedObjectsImportRetry[]; savedObjectsClient: SavedObjectsClientContract; - // @deprecated (undocumented) - trueCopy: boolean; typeRegistry: ISavedObjectTypeRegistry; } diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts index 700e55821fc2e..af25d6f56f7ce 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts @@ -132,7 +132,7 @@ describe('copySavedObjectsToSpaces', () => { id: 'my-dashboard', }, ], - trueCopy: false, + createNewCopies: false, }); expect(result).toMatchInlineSnapshot(` @@ -193,6 +193,7 @@ describe('copySavedObjectsToSpaces', () => { Array [ Array [ Object { + "createNewCopies": false, "namespace": "destination1", "objectLimit": 1000, "overwrite": true, @@ -251,7 +252,6 @@ describe('copySavedObjectsToSpaces', () => { "get": [MockFunction], "update": [MockFunction], }, - "trueCopy": false, "typeRegistry": Object { "getAllTypes": [MockFunction], "getImportableAndExportableTypes": [MockFunction], @@ -269,6 +269,7 @@ describe('copySavedObjectsToSpaces', () => { ], Array [ Object { + "createNewCopies": false, "namespace": "destination2", "objectLimit": 1000, "overwrite": true, @@ -327,7 +328,6 @@ describe('copySavedObjectsToSpaces', () => { "get": [MockFunction], "update": [MockFunction], }, - "trueCopy": false, "typeRegistry": Object { "getAllTypes": [MockFunction], "getImportableAndExportableTypes": [MockFunction], @@ -401,7 +401,7 @@ describe('copySavedObjectsToSpaces', () => { id: 'my-dashboard', }, ], - trueCopy: false, + createNewCopies: false, } ); @@ -486,7 +486,7 @@ describe('copySavedObjectsToSpaces', () => { id: 'my-dashboard', }, ], - trueCopy: false, + createNewCopies: false, } ) ).rejects.toThrowErrorMatchingInlineSnapshot( diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts index 4f2c83ba1d548..41fcd42677767 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts @@ -55,7 +55,7 @@ export function copySavedObjectsToSpacesFactory( savedObjectsClient, typeRegistry: getTypeRegistry(), readStream: objectsStream, - trueCopy: options.trueCopy, + createNewCopies: options.createNewCopies, }); return { diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts index 8e53a2468c9ee..4373593ddd663 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts @@ -148,7 +148,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { }, ], }, - trueCopy: false, + createNewCopies: false, }); expect(result).toMatchInlineSnapshot(` @@ -209,6 +209,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { Array [ Array [ Object { + "createNewCopies": false, "namespace": "destination1", "objectLimit": 1000, "readStream": Readable { @@ -274,7 +275,6 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { "get": [MockFunction], "update": [MockFunction], }, - "trueCopy": false, "typeRegistry": Object { "getAllTypes": [MockFunction], "getImportableAndExportableTypes": [MockFunction], @@ -292,6 +292,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { ], Array [ Object { + "createNewCopies": false, "namespace": "destination2", "objectLimit": 1000, "readStream": Readable { @@ -357,7 +358,6 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { "get": [MockFunction], "update": [MockFunction], }, - "trueCopy": false, "typeRegistry": Object { "getAllTypes": [MockFunction], "getImportableAndExportableTypes": [MockFunction], @@ -450,7 +450,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { }, ], }, - trueCopy: false, + createNewCopies: false, }); expect(result).toMatchInlineSnapshot(` @@ -510,7 +510,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { includeReferences: true, objects: [], retries: {}, - trueCopy: false, + createNewCopies: false, }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Something went wrong while reading this stream"` diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts index d8bbb3e2c2644..700b34c067d8e 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts @@ -45,7 +45,7 @@ export function resolveCopySavedObjectsToSpacesConflictsFactory( spaceId: string, objectsStream: Readable, retries: SavedObjectsImportRetry[], - trueCopy: boolean + createNewCopies: boolean ) => { try { const importResponse = await resolveSavedObjectsImportErrors({ @@ -55,7 +55,7 @@ export function resolveCopySavedObjectsToSpacesConflictsFactory( typeRegistry: getTypeRegistry(), readStream: objectsStream, retries, - trueCopy, + createNewCopies, }); return { @@ -89,7 +89,7 @@ export function resolveCopySavedObjectsToSpacesConflictsFactory( spaceId, createReadableStreamFromArray(exportedSavedObjects), retries, - options.trueCopy + options.createNewCopies ); } diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/types.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/types.ts index 4301d3790ce60..8d4169f972795 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/types.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/types.ts @@ -15,7 +15,7 @@ export interface CopyOptions { objects: Array<{ type: string; id: string }>; overwrite: boolean; includeReferences: boolean; - trueCopy: boolean; + createNewCopies: boolean; } export interface ResolveConflictsOptions { @@ -24,7 +24,7 @@ export interface ResolveConflictsOptions { retries: { [spaceId: string]: Array>; }; - trueCopy: boolean; + createNewCopies: boolean; } export interface CopyResponse { diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts index 77f1751f8a104..bec3a5dcb0b71 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts @@ -191,19 +191,19 @@ describe('copy to space', () => { ); }); - it(`does not allow "overwrite" to be used with "trueCopy"`, async () => { + it(`does not allow "overwrite" to be used with "createNewCopies"`, async () => { const payload = { spaces: ['a-space'], objects: [{ type: 'foo', id: 'bar' }], overwrite: true, - trueCopy: true, + createNewCopies: true, }; const { copyToSpace } = await setup(); expect(() => (copyToSpace.routeValidation.body as ObjectType).validate(payload) - ).toThrowErrorMatchingInlineSnapshot(`"cannot use [overwrite] with [trueCopy]"`); + ).toThrowErrorMatchingInlineSnapshot(`"cannot use [overwrite] with [createNewCopies]"`); }); it(`requires objects to be unique`, async () => { diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts index 252d7e439798c..804820cd42b92 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts @@ -63,12 +63,12 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { ), includeReferences: schema.boolean({ defaultValue: false }), overwrite: schema.boolean({ defaultValue: false }), - trueCopy: schema.boolean({ defaultValue: false }), + createNewCopies: schema.boolean({ defaultValue: false }), }, { validate: (object) => { - if (object.overwrite && object.trueCopy) { - return 'cannot use [overwrite] with [trueCopy]'; + if (object.overwrite && object.createNewCopies) { + return 'cannot use [overwrite] with [createNewCopies]'; } }, } @@ -88,14 +88,14 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { objects, includeReferences, overwrite, - trueCopy, + createNewCopies, } = request.body; const sourceSpaceId = spacesService.getSpaceId(request); const copyResponse = await copySavedObjectsToSpaces(sourceSpaceId, destinationSpaceIds, { objects, includeReferences, overwrite, - trueCopy, + createNewCopies, }); return response.ok({ body: copyResponse }); }) @@ -123,7 +123,7 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { id: schema.string(), overwrite: schema.boolean({ defaultValue: false }), destinationId: schema.maybe(schema.string()), - trueCopy: schema.maybe(schema.boolean()), + createNewCopy: schema.maybe(schema.boolean()), }) ) ), @@ -141,7 +141,7 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { } ), includeReferences: schema.boolean({ defaultValue: false }), - trueCopy: schema.boolean({ defaultValue: false }), + createNewCopies: schema.boolean({ defaultValue: false }), }), }, }, @@ -153,7 +153,7 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { getImportExportObjectLimit, request ); - const { objects, includeReferences, retries, trueCopy } = request.body; + const { objects, includeReferences, retries, createNewCopies } = request.body; const sourceSpaceId = spacesService.getSpaceId(request); const resolveConflictsResponse = await resolveCopySavedObjectsToSpacesConflicts( sourceSpaceId, @@ -161,7 +161,7 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { objects, includeReferences, retries, - trueCopy, + createNewCopies, } ); return response.ok({ body: resolveConflictsResponse }); diff --git a/x-pack/test/saved_object_api_integration/common/suites/import.ts b/x-pack/test/saved_object_api_integration/common/suites/import.ts index c52bf57a6f708..2e63537e8f4d5 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/import.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/import.ts @@ -14,7 +14,7 @@ import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/ export interface ImportTestDefinition extends TestDefinition { request: Array<{ type: string; id: string; originId?: string }>; overwrite: boolean; - trueCopy: boolean; + createNewCopies: boolean; } export type ImportTestSuite = TestSuite; export interface ImportTestCase extends TestCase { @@ -77,7 +77,7 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe statusCode: 200 | 403, singleRequest: boolean, overwrite: boolean, - trueCopy: boolean, + createNewCopies: boolean, spaceId = SPACES.DEFAULT.spaceId ): ExpectResponseBody => async (response: Record) => { const testCaseArray = Array.isArray(testCases) ? testCases : [testCases]; @@ -114,23 +114,24 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe // the new ID was randomly generated expect(destinationId).to.match(/^[0-9a-f-]{36}$/); } - } else if (successParam === 'trueCopy' || successParam === 'newOrigin') { + } else if (successParam === 'createNewCopies' || successParam === 'newOrigin') { // the new ID was randomly generated expect(destinationId).to.match(/^[0-9a-f-]{36}$/); } else { expect(destinationId).to.be(undefined); } - // This assertion is only needed for the case where True Copy mode is disabled and ambiguous source conflicts are detected. When - // True Copy mode is permanently enabled, this field will be removed, and this assertion will be redundant and can be removed too. - const resultTrueCopy = object!.trueCopy as boolean | undefined; + // This assertion is only needed for the case where `createNewCopies` mode is disabled and ambiguous source conflicts are detected. + // When `createNewCopies` mode is permanently enabled, this field will be removed, and this assertion will be redundant and can be + // removed too. + const resultNewCopies = object!.createNewCopies as boolean | undefined; if (successParam === 'newOrigin') { - expect(resultTrueCopy).to.be(true); + expect(resultNewCopies).to.be(true); } else { - expect(resultTrueCopy).to.be(undefined); + expect(resultNewCopies).to.be(undefined); } - if (!singleRequest || overwrite || trueCopy) { + if (!singleRequest || overwrite || createNewCopies) { // even if the object result was a "success" result, it may not have been created if other resolvable errors were returned const { _source } = await expectResponses.successCreated( es, @@ -179,7 +180,7 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe forbidden: boolean, options: { overwrite?: boolean; - trueCopy?: boolean; + createNewCopies?: boolean; spaceId?: string; singleRequest?: boolean; responseBodyOverride?: ExpectResponseBody; @@ -189,7 +190,7 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe const responseStatusCode = forbidden ? 403 : 200; const { overwrite = false, - trueCopy = false, + createNewCopies = false, spaceId, singleRequest, responseBodyOverride, @@ -203,9 +204,9 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe responseStatusCode, responseBody: responseBodyOverride || - expectResponseBody(x, responseStatusCode, false, overwrite, trueCopy, spaceId), + expectResponseBody(x, responseStatusCode, false, overwrite, createNewCopies, spaceId), overwrite, - trueCopy, + createNewCopies, })); } // batch into a single request to save time during test execution @@ -216,9 +217,9 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe responseStatusCode, responseBody: responseBodyOverride || - expectResponseBody(cases, responseStatusCode, true, overwrite, trueCopy, spaceId), + expectResponseBody(cases, responseStatusCode, true, overwrite, createNewCopies, spaceId), overwrite, - trueCopy, + createNewCopies, }, ]; }; @@ -240,7 +241,11 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe const requestBody = test.request .map((obj) => JSON.stringify({ ...obj, ...attrs })) .join('\n'); - const query = test.overwrite ? '?overwrite=true' : test.trueCopy ? '?trueCopy=true' : ''; + const query = test.overwrite + ? '?overwrite=true' + : test.createNewCopies + ? '?createNewCopies=true' + : ''; await supertest .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_import${query}`) .auth(user?.username, user?.password) diff --git a/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts index 52b5610a195e5..6c9d2596400ea 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts @@ -17,7 +17,7 @@ export interface ResolveImportErrorsTestDefinition extends TestDefinition { retries: Array<{ type: string; id: string; overwrite: boolean; destinationId?: string }>; }; overwrite: boolean; - trueCopy: boolean; + createNewCopies: boolean; } export type ResolveImportErrorsTestSuite = TestSuite; export interface ResolveImportErrorsTestCase extends TestCase { @@ -84,7 +84,7 @@ const createRequest = ( id, overwrite, ...(expectedNewId && { destinationId: expectedNewId }), - ...(successParam === 'newOrigin' && { trueCopy: true }), + ...(successParam === 'newOrigin' && { createNewCopy: true }), }, ], }); @@ -134,19 +134,20 @@ export function resolveImportErrorsTestSuiteFactory( // the new ID was randomly generated expect(destinationId).to.match(/^[0-9a-f-]{36}$/); } - } else if (successParam === 'trueCopy' || successParam === 'newOrigin') { + } else if (successParam === 'createNewCopies' || successParam === 'newOrigin') { expect(destinationId).to.be(expectedNewId!); } else { expect(destinationId).to.be(undefined); } - // This assertion is only needed for the case where True Copy mode is disabled and ambiguous source conflicts are detected. When - // True Copy mode is permanently enabled, this field will be removed, and this assertion will be redundant and can be removed too. - const resultTrueCopy = object!.trueCopy as boolean | undefined; + // This assertion is only needed for the case where `createNewCopies` mode is disabled and ambiguous source conflicts are detected. + // When `createNewCopies` mode is permanently enabled, this field will be removed, and this assertion will be redundant and can be + // removed too. + const resultNewCopies = object!.createNewCopies as boolean | undefined; if (successParam === 'newOrigin') { - expect(resultTrueCopy).to.be(true); + expect(resultNewCopies).to.be(true); } else { - expect(resultTrueCopy).to.be(undefined); + expect(resultNewCopies).to.be(undefined); } const { _source } = await expectResponses.successCreated( @@ -181,7 +182,7 @@ export function resolveImportErrorsTestSuiteFactory( forbidden: boolean, options: { overwrite?: boolean; - trueCopy?: boolean; + createNewCopies?: boolean; spaceId?: string; singleRequest?: boolean; responseBodyOverride?: ExpectResponseBody; @@ -191,7 +192,7 @@ export function resolveImportErrorsTestSuiteFactory( const responseStatusCode = forbidden ? 403 : 200; const { overwrite = false, - trueCopy = false, + createNewCopies = false, spaceId, singleRequest, responseBodyOverride, @@ -205,7 +206,7 @@ export function resolveImportErrorsTestSuiteFactory( responseStatusCode, responseBody: responseBodyOverride || expectResponseBody(x, responseStatusCode, spaceId), overwrite, - trueCopy, + createNewCopies, })); } // batch into a single request to save time during test execution @@ -222,7 +223,7 @@ export function resolveImportErrorsTestSuiteFactory( responseBody: responseBodyOverride || expectResponseBody(cases, responseStatusCode, spaceId), overwrite, - trueCopy, + createNewCopies, }, ]; }; @@ -244,7 +245,7 @@ export function resolveImportErrorsTestSuiteFactory( const requestBody = test.request.objects .map((obj) => JSON.stringify({ ...obj, ...attrs })) .join('\n'); - const query = test.trueCopy ? '?trueCopy=true' : ''; + const query = test.createNewCopies ? '?createNewCopies=true' : ''; await supertest .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_resolve_import_errors${query}`) .auth(user?.username, user?.password) diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts index 164a01a3c2618..9d3a763057fed 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts @@ -28,11 +28,11 @@ const ambiguousConflict = (suffix: string) => ({ fail409Param: `ambiguous_conflict_${suffix}`, }); -const createTrueCopyTestCases = () => { +const createNewCopiesTestCases = () => { // for each outcome, if failure !== undefined then we expect to receive // an error; otherwise, we expect to receive a success result const cases = Object.entries(CASES).filter(([key]) => key !== 'HIDDEN'); - const importable = cases.map(([, val]) => ({ ...val, successParam: 'trueCopy' })); + const importable = cases.map(([, val]) => ({ ...val, successParam: 'createNewCopies' })); const nonImportable = [{ ...CASES.HIDDEN, ...fail400() }]; const all = [...importable, ...nonImportable]; return { importable, nonImportable, all }; @@ -104,17 +104,17 @@ export default function ({ getService }: FtrProviderContext) { esArchiver, supertest ); - const createTests = (overwrite: boolean, trueCopy: boolean, spaceId: string) => { + const createTests = (overwrite: boolean, createNewCopies: boolean, spaceId: string) => { const singleRequest = true; - if (trueCopy) { - const { importable, nonImportable, all } = createTrueCopyTestCases(); + if (createNewCopies) { + const { importable, nonImportable, all } = createNewCopiesTestCases(); return { unauthorized: [ - createTestDefinitions(importable, true, { trueCopy, spaceId }), - createTestDefinitions(nonImportable, false, { trueCopy, spaceId, singleRequest }), + createTestDefinitions(importable, true, { createNewCopies, spaceId }), + createTestDefinitions(nonImportable, false, { createNewCopies, spaceId, singleRequest }), createTestDefinitions(all, true, { - trueCopy, + createNewCopies, spaceId, singleRequest, responseBodyOverride: expectForbidden('bulk_create')([ @@ -125,7 +125,7 @@ export default function ({ getService }: FtrProviderContext) { ]), }), ].flat(), - authorized: createTestDefinitions(all, false, { trueCopy, spaceId, singleRequest }), + authorized: createTestDefinitions(all, false, { createNewCopies, spaceId, singleRequest }), }; } @@ -170,11 +170,15 @@ export default function ({ getService }: FtrProviderContext) { [false, true], [true, false], ]).securityAndSpaces.forEach(({ spaceId, users, modifier }) => { - const [overwrite, trueCopy] = modifier!; + const [overwrite, createNewCopies] = modifier!; const suffix = ` within the ${spaceId} space${ - overwrite ? ' with overwrite enabled' : trueCopy ? ' with trueCopy enabled' : '' + overwrite + ? ' with overwrite enabled' + : createNewCopies + ? ' with createNewCopies enabled' + : '' }`; - const { unauthorized, authorized } = createTests(overwrite, trueCopy, spaceId); + const { unauthorized, authorized } = createTests(overwrite, createNewCopies, spaceId); const _addTests = (user: TestUser, tests: ImportTestDefinition[]) => { addTests(`${user.description}${suffix}`, { user, spaceId, tests }); }; diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts index 163fb53253c74..ec8fa2390c852 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts @@ -25,13 +25,13 @@ const destinationId = (condition?: boolean) => condition !== false ? { successParam: 'destinationId' } : {}; const newOrigin = () => ({ successParam: 'newOrigin' }); -const createTrueCopyTestCases = () => { +const createNewCopiesTestCases = () => { // for each outcome, if failure !== undefined then we expect to receive // an error; otherwise, we expect to receive a success result const cases = Object.entries(CASES).filter(([key]) => key !== 'HIDDEN'); const importable = cases.map(([, val]) => ({ ...val, - successParam: 'trueCopy', + successParam: 'createNewCopies', expectedNewId: uuidv4(), })); const nonImportable = [{ ...CASES.HIDDEN, ...fail400() }]; @@ -92,24 +92,24 @@ export default function ({ getService }: FtrProviderContext) { esArchiver, supertest ); - const createTests = (overwrite: boolean, trueCopy: boolean, spaceId: string) => { + const createTests = (overwrite: boolean, createNewCopies: boolean, spaceId: string) => { // use singleRequest to reduce execution time and/or test combined cases const singleRequest = true; - if (trueCopy) { - const { importable, nonImportable, all } = createTrueCopyTestCases(); + if (createNewCopies) { + const { importable, nonImportable, all } = createNewCopiesTestCases(); return { unauthorized: [ - createTestDefinitions(importable, true, { trueCopy, spaceId }), - createTestDefinitions(nonImportable, false, { trueCopy, spaceId, singleRequest }), + createTestDefinitions(importable, true, { createNewCopies, spaceId }), + createTestDefinitions(nonImportable, false, { createNewCopies, spaceId, singleRequest }), createTestDefinitions(all, true, { - trueCopy, + createNewCopies, spaceId, singleRequest, responseBodyOverride: expectForbidden(['globaltype', 'isolatedtype', 'sharedtype']), }), ].flat(), - authorized: createTestDefinitions(all, false, { trueCopy, spaceId, singleRequest }), + authorized: createTestDefinitions(all, false, { createNewCopies, spaceId, singleRequest }), }; } @@ -142,11 +142,15 @@ export default function ({ getService }: FtrProviderContext) { [false, true], [true, false], ]).securityAndSpaces.forEach(({ spaceId, users, modifier }) => { - const [overwrite, trueCopy] = modifier!; + const [overwrite, createNewCopies] = modifier!; const suffix = ` within the ${spaceId} space${ - overwrite ? ' with overwrite enabled' : trueCopy ? ' with trueCopy enabled' : '' + overwrite + ? ' with overwrite enabled' + : createNewCopies + ? ' with createNewCopies enabled' + : '' }`; - const { unauthorized, authorized } = createTests(overwrite, trueCopy, spaceId); + const { unauthorized, authorized } = createTests(overwrite, createNewCopies, spaceId); const _addTests = (user: TestUser, tests: ResolveImportErrorsTestDefinition[]) => { addTests(`${user.description}${suffix}`, { user, spaceId, tests }); }; diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/import.ts b/x-pack/test/saved_object_api_integration/security_only/apis/import.ts index 3ff7925628d38..61e42c4dce422 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/import.ts @@ -22,11 +22,11 @@ const ambiguousConflict = (suffix: string) => ({ fail409Param: `ambiguous_conflict_${suffix}`, }); -const createTrueCopyTestCases = () => { +const createNewCopiesTestCases = () => { // for each outcome, if failure !== undefined then we expect to receive // an error; otherwise, we expect to receive a success result const cases = Object.entries(CASES).filter(([key]) => key !== 'HIDDEN'); - const importable = cases.map(([, val]) => ({ ...val, successParam: 'trueCopy' })); + const importable = cases.map(([, val]) => ({ ...val, successParam: 'createNewCopies' })); const nonImportable = [{ ...CASES.HIDDEN, ...fail400() }]; const all = [...importable, ...nonImportable]; return { importable, nonImportable, all }; @@ -83,18 +83,18 @@ export default function ({ getService }: FtrProviderContext) { esArchiver, supertest ); - const createTests = (overwrite: boolean, trueCopy: boolean) => { + const createTests = (overwrite: boolean, createNewCopies: boolean) => { // use singleRequest to reduce execution time and/or test combined cases const singleRequest = true; - if (trueCopy) { - const { importable, nonImportable, all } = createTrueCopyTestCases(); + if (createNewCopies) { + const { importable, nonImportable, all } = createNewCopiesTestCases(); return { unauthorized: [ - createTestDefinitions(importable, true, { trueCopy }), - createTestDefinitions(nonImportable, false, { trueCopy, singleRequest }), + createTestDefinitions(importable, true, { createNewCopies }), + createTestDefinitions(nonImportable, false, { createNewCopies, singleRequest }), createTestDefinitions(all, true, { - trueCopy, + createNewCopies, singleRequest, responseBodyOverride: expectForbidden('bulk_create')([ 'dashboard', @@ -104,7 +104,7 @@ export default function ({ getService }: FtrProviderContext) { ]), }), ].flat(), - authorized: createTestDefinitions(all, false, { trueCopy, singleRequest }), + authorized: createTestDefinitions(all, false, { createNewCopies, singleRequest }), }; } @@ -148,13 +148,13 @@ export default function ({ getService }: FtrProviderContext) { [false, true], [true, false], ]).security.forEach(({ users, modifier }) => { - const [overwrite, trueCopy] = modifier!; + const [overwrite, createNewCopies] = modifier!; const suffix = overwrite ? ' with overwrite enabled' - : trueCopy - ? ' with trueCopy enabled' + : createNewCopies + ? ' with createNewCopies enabled' : ''; - const { unauthorized, authorized } = createTests(overwrite, trueCopy); + const { unauthorized, authorized } = createTests(overwrite, createNewCopies); const _addTests = (user: TestUser, tests: ImportTestDefinition[]) => { addTests(`${user.description}${suffix}`, { user, tests }); }; diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts index dacf1d4745274..7c0bb9dadf123 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts @@ -19,13 +19,13 @@ const destinationId = (condition?: boolean) => condition !== false ? { successParam: 'destinationId' } : {}; const newOrigin = () => ({ successParam: 'newOrigin' }); -const createTrueCopyTestCases = () => { +const createNewCopiesTestCases = () => { // for each outcome, if failure !== undefined then we expect to receive // an error; otherwise, we expect to receive a success result const cases = Object.entries(CASES).filter(([key]) => key !== 'HIDDEN'); const importable = cases.map(([, val]) => ({ ...val, - successParam: 'trueCopy', + successParam: 'createNewCopies', expectedNewId: uuidv4(), })); const nonImportable = [{ ...CASES.HIDDEN, ...fail400() }]; @@ -66,23 +66,23 @@ export default function ({ getService }: FtrProviderContext) { esArchiver, supertest ); - const createTests = (overwrite: boolean, trueCopy: boolean) => { + const createTests = (overwrite: boolean, createNewCopies: boolean) => { // use singleRequest to reduce execution time and/or test combined cases const singleRequest = true; - if (trueCopy) { - const { importable, nonImportable, all } = createTrueCopyTestCases(); + if (createNewCopies) { + const { importable, nonImportable, all } = createNewCopiesTestCases(); return { unauthorized: [ - createTestDefinitions(importable, true, { trueCopy }), - createTestDefinitions(nonImportable, false, { trueCopy, singleRequest }), + createTestDefinitions(importable, true, { createNewCopies }), + createTestDefinitions(nonImportable, false, { createNewCopies, singleRequest }), createTestDefinitions(all, true, { - trueCopy, + createNewCopies, singleRequest, responseBodyOverride: expectForbidden(['globaltype', 'isolatedtype', 'sharedtype']), }), ].flat(), - authorized: createTestDefinitions(all, false, { trueCopy, singleRequest }), + authorized: createTestDefinitions(all, false, { createNewCopies, singleRequest }), }; } @@ -111,13 +111,13 @@ export default function ({ getService }: FtrProviderContext) { [false, true], [true, false], ]).security.forEach(({ users, modifier }) => { - const [overwrite, trueCopy] = modifier!; + const [overwrite, createNewCopies] = modifier!; const suffix = overwrite ? ' with overwrite enabled' - : trueCopy - ? ' with trueCopy enabled' + : createNewCopies + ? ' with createNewCopies enabled' : ''; - const { unauthorized, authorized } = createTests(overwrite, trueCopy); + const { unauthorized, authorized } = createTests(overwrite, createNewCopies); const _addTests = (user: TestUser, tests: ResolveImportErrorsTestDefinition[]) => { addTests(`${user.description}${suffix}`, { user, tests }); }; diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts index fcb66da3b0ed1..1340f86683a1d 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts @@ -23,12 +23,12 @@ const ambiguousConflict = (suffix: string) => ({ fail409Param: `ambiguous_conflict_${suffix}`, }); -const createTrueCopyTestCases = () => { +const createNewCopiesTestCases = () => { // for each outcome, if failure !== undefined then we expect to receive // an error; otherwise, we expect to receive a success result const cases = Object.entries(CASES).filter(([key]) => key !== 'HIDDEN'); return [ - ...cases.map(([, val]) => ({ ...val, successParam: 'trueCopy' })), + ...cases.map(([, val]) => ({ ...val, successParam: 'createNewCopies' })), { ...CASES.HIDDEN, ...fail400() }, ]; }; @@ -91,11 +91,11 @@ export default function ({ getService }: FtrProviderContext) { const es = getService('legacyEs'); const { addTests, createTestDefinitions } = importTestSuiteFactory(es, esArchiver, supertest); - const createTests = (overwrite: boolean, trueCopy: boolean, spaceId: string) => { + const createTests = (overwrite: boolean, createNewCopies: boolean, spaceId: string) => { const singleRequest = true; - if (trueCopy) { - const cases = createTrueCopyTestCases(); - return createTestDefinitions(cases, false, { trueCopy, spaceId, singleRequest }); + if (createNewCopies) { + const cases = createNewCopiesTestCases(); + return createTestDefinitions(cases, false, { createNewCopies, spaceId, singleRequest }); } const { group1, group2, group3 } = createTestCases(overwrite, spaceId); @@ -112,13 +112,13 @@ export default function ({ getService }: FtrProviderContext) { [false, true], [true, false], ]).spaces.forEach(({ spaceId, modifier }) => { - const [overwrite, trueCopy] = modifier!; + const [overwrite, createNewCopies] = modifier!; const suffix = overwrite ? ' with overwrite enabled' - : trueCopy - ? ' with trueCopy enabled' + : createNewCopies + ? ' with createNewCopies enabled' : ''; - const tests = createTests(overwrite, trueCopy, spaceId); + const tests = createTests(overwrite, createNewCopies, spaceId); addTests(`within the ${spaceId} space${suffix}`, { spaceId, tests }); }); }); diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts index f95b3e4b13bec..c135de588b2de 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts @@ -23,12 +23,16 @@ const destinationId = (condition?: boolean) => condition !== false ? { successParam: 'destinationId' } : {}; const newOrigin = () => ({ successParam: 'newOrigin' }); -const createTrueCopyTestCases = () => { +const createNewCopiesTestCases = () => { // for each outcome, if failure !== undefined then we expect to receive // an error; otherwise, we expect to receive a success result const cases = Object.entries(CASES).filter(([key]) => key !== 'HIDDEN'); return [ - ...cases.map(([, val]) => ({ ...val, successParam: 'trueCopy', expectedNewId: uuidv4() })), + ...cases.map(([, val]) => ({ + ...val, + successParam: 'createNewCopies', + expectedNewId: uuidv4(), + })), { ...CASES.HIDDEN, ...fail400() }, ]; }; @@ -82,13 +86,13 @@ export default function ({ getService }: FtrProviderContext) { esArchiver, supertest ); - const createTests = (overwrite: boolean, trueCopy: boolean, spaceId: string) => { + const createTests = (overwrite: boolean, createNewCopies: boolean, spaceId: string) => { const singleRequest = true; - if (trueCopy) { - const cases = createTrueCopyTestCases(); - // The resolveImportErrors API doesn't actually have a flag for "trueCopy" mode; rather, we create test cases as if we are resolving - // errors from a call to the import API that had trueCopy mode enabled. - return createTestDefinitions(cases, false, { trueCopy, spaceId, singleRequest }); + if (createNewCopies) { + const cases = createNewCopiesTestCases(); + // The resolveImportErrors API doesn't actually have a flag for "createNewCopies" mode; rather, we create test cases as if we are resolving + // errors from a call to the import API that had createNewCopies mode enabled. + return createTestDefinitions(cases, false, { createNewCopies, spaceId, singleRequest }); } const testCases = createTestCases(overwrite, spaceId); @@ -101,13 +105,13 @@ export default function ({ getService }: FtrProviderContext) { [false, true], [true, false], ]).spaces.forEach(({ spaceId, modifier }) => { - const [overwrite, trueCopy] = modifier!; + const [overwrite, createNewCopies] = modifier!; const suffix = overwrite ? ' with overwrite enabled' - : trueCopy - ? ' with trueCopy enabled' + : createNewCopies + ? ' with createNewCopies enabled' : ''; - const tests = createTests(overwrite, trueCopy, spaceId); + const tests = createTests(overwrite, createNewCopies, spaceId); addTests(`within the ${spaceId} space${suffix}`, { spaceId, tests }); }); }); From b255efbc126fa6c23e2f90199fb019dd78cc4ecd Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 7 Jul 2020 23:59:34 -0400 Subject: [PATCH 39/55] Clean up copy_to_spaces tests --- .../lib/copy_to_spaces/copy_to_spaces.test.ts | 419 +++++----------- .../resolve_copy_conflicts.test.ts | 450 +++++------------- 2 files changed, 218 insertions(+), 651 deletions(-) diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts index af25d6f56f7ce..365d0494b12c8 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts @@ -3,15 +3,20 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { Readable } from 'stream'; import { SavedObjectsImportResponse, SavedObjectsImportOptions, SavedObjectsExportOptions, SavedObjectsImportSuccess, } from 'src/core/server'; +import { + coreMock, + httpServerMock, + savedObjectsTypeRegistryMock, + savedObjectsClientMock, +} from 'src/core/server/mocks'; import { copySavedObjectsToSpacesFactory } from './copy_to_spaces'; -import { Readable } from 'stream'; -import { coreMock, httpServerMock } from 'src/core/server/mocks'; jest.mock('../../../../../../src/core/server', () => { return { @@ -32,6 +37,8 @@ interface SetupOpts { ) => Promise; } +const EXPORT_LIMIT = 1000; + const expectStreamToContainObjects = async ( stream: Readable, expectedObjects: SetupOpts['objects'] @@ -51,9 +58,20 @@ const expectStreamToContainObjects = async ( }; describe('copySavedObjectsToSpaces', () => { + const mockExportResults = [ + { type: 'dashboard', id: 'my-dashboard', attributes: {} }, + { type: 'visualization', id: 'my-viz', attributes: {} }, + { type: 'index-pattern', id: 'my-index-pattern', attributes: {} }, + ]; + const setup = (setupOpts: SetupOpts) => { const coreStart = coreMock.createStart(); + const savedObjectsClient = savedObjectsClientMock.create(); + const typeRegistry = savedObjectsTypeRegistryMock.create(); + coreStart.savedObjects.getScopedClient.mockReturnValue(savedObjectsClient); + coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); + (exportSavedObjectsToStream as jest.Mock).mockImplementation( async (opts: SavedObjectsExportOptions) => { return ( @@ -91,288 +109,89 @@ describe('copySavedObjectsToSpaces', () => { return { savedObjects: coreStart.savedObjects, + savedObjectsClient, + typeRegistry, }; }; it('uses the Saved Objects Service to perform an export followed by a series of imports', async () => { - const { savedObjects } = setup({ - objects: [ - { - type: 'dashboard', - id: 'my-dashboard', - attributes: {}, - }, - { - type: 'visualization', - id: 'my-viz', - attributes: {}, - }, - { - type: 'index-pattern', - id: 'my-index-pattern', - attributes: {}, - }, - ], + const { savedObjects, savedObjectsClient, typeRegistry } = setup({ + objects: mockExportResults, }); const request = httpServerMock.createKibanaRequest(); const copySavedObjectsToSpaces = copySavedObjectsToSpacesFactory( savedObjects, - () => 1000, + () => EXPORT_LIMIT, request ); - const result = await copySavedObjectsToSpaces('sourceSpace', ['destination1', 'destination2'], { + const namespace = 'sourceSpace'; + const objects = [{ type: 'dashboard', id: 'my-dashboard' }]; + const result = await copySavedObjectsToSpaces(namespace, ['destination1', 'destination2'], { includeReferences: true, overwrite: true, - objects: [ - { - type: 'dashboard', - id: 'my-dashboard', - }, - ], + objects, createNewCopies: false, }); expect(result).toMatchInlineSnapshot(` - Object { - "destination1": Object { - "errors": undefined, - "success": true, - "successCount": 3, - "successResults": Array [ - "Some success(es) occurred!", - ], - }, - "destination2": Object { - "errors": undefined, - "success": true, - "successCount": 3, - "successResults": Array [ - "Some success(es) occurred!", - ], - }, - } - `); - - expect((exportSavedObjectsToStream as jest.Mock).mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - Object { - "excludeExportDetails": true, - "exportSizeLimit": 1000, - "includeReferencesDeep": true, - "namespace": "sourceSpace", - "objects": Array [ - Object { - "id": "my-dashboard", - "type": "dashboard", - }, - ], - "savedObjectsClient": Object { - "addToNamespaces": [MockFunction], - "bulkCreate": [MockFunction], - "bulkGet": [MockFunction], - "bulkUpdate": [MockFunction], - "checkConflicts": [MockFunction], - "create": [MockFunction], - "delete": [MockFunction], - "deleteFromNamespaces": [MockFunction], - "errors": [Function], - "find": [MockFunction], - "get": [MockFunction], - "update": [MockFunction], - }, - }, - ], - ] + Object { + "destination1": Object { + "errors": undefined, + "success": true, + "successCount": 3, + "successResults": Array [ + "Some success(es) occurred!", + ], + }, + "destination2": Object { + "errors": undefined, + "success": true, + "successCount": 3, + "successResults": Array [ + "Some success(es) occurred!", + ], + }, + } `); - expect((importSavedObjectsFromStream as jest.Mock).mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - Object { - "createNewCopies": false, - "namespace": "destination1", - "objectLimit": 1000, - "overwrite": true, - "readStream": Readable { - "_events": Object { - "data": [Function], - "end": [Function], - "error": [Function], - }, - "_eventsCount": 3, - "_maxListeners": undefined, - "_read": [Function], - "_readableState": ReadableState { - "autoDestroy": false, - "awaitDrain": 0, - "buffer": BufferList { - "head": null, - "length": 0, - "tail": null, - }, - "decoder": null, - "defaultEncoding": "utf8", - "destroyed": false, - "emitClose": true, - "emittedReadable": false, - "encoding": null, - "endEmitted": true, - "ended": true, - "flowing": true, - "highWaterMark": 16, - "length": 0, - "needReadable": false, - "objectMode": true, - "paused": false, - "pipes": null, - "pipesCount": 0, - "readableListening": false, - "reading": false, - "readingMore": false, - "resumeScheduled": false, - "sync": false, - }, - "readable": false, - }, - "savedObjectsClient": Object { - "addToNamespaces": [MockFunction], - "bulkCreate": [MockFunction], - "bulkGet": [MockFunction], - "bulkUpdate": [MockFunction], - "checkConflicts": [MockFunction], - "create": [MockFunction], - "delete": [MockFunction], - "deleteFromNamespaces": [MockFunction], - "errors": [Function], - "find": [MockFunction], - "get": [MockFunction], - "update": [MockFunction], - }, - "typeRegistry": Object { - "getAllTypes": [MockFunction], - "getImportableAndExportableTypes": [MockFunction], - "getIndex": [MockFunction], - "getType": [MockFunction], - "getVisibleTypes": [MockFunction], - "isHidden": [MockFunction], - "isImportableAndExportable": [MockFunction], - "isMultiNamespace": [MockFunction], - "isNamespaceAgnostic": [MockFunction], - "isSingleNamespace": [MockFunction], - "registerType": [MockFunction], - }, - }, - ], - Array [ - Object { - "createNewCopies": false, - "namespace": "destination2", - "objectLimit": 1000, - "overwrite": true, - "readStream": Readable { - "_events": Object { - "data": [Function], - "end": [Function], - "error": [Function], - }, - "_eventsCount": 3, - "_maxListeners": undefined, - "_read": [Function], - "_readableState": ReadableState { - "autoDestroy": false, - "awaitDrain": 0, - "buffer": BufferList { - "head": null, - "length": 0, - "tail": null, - }, - "decoder": null, - "defaultEncoding": "utf8", - "destroyed": false, - "emitClose": true, - "emittedReadable": false, - "encoding": null, - "endEmitted": true, - "ended": true, - "flowing": true, - "highWaterMark": 16, - "length": 0, - "needReadable": false, - "objectMode": true, - "paused": false, - "pipes": null, - "pipesCount": 0, - "readableListening": false, - "reading": false, - "readingMore": false, - "resumeScheduled": false, - "sync": false, - }, - "readable": false, - }, - "savedObjectsClient": Object { - "addToNamespaces": [MockFunction], - "bulkCreate": [MockFunction], - "bulkGet": [MockFunction], - "bulkUpdate": [MockFunction], - "checkConflicts": [MockFunction], - "create": [MockFunction], - "delete": [MockFunction], - "deleteFromNamespaces": [MockFunction], - "errors": [Function], - "find": [MockFunction], - "get": [MockFunction], - "update": [MockFunction], - }, - "typeRegistry": Object { - "getAllTypes": [MockFunction], - "getImportableAndExportableTypes": [MockFunction], - "getIndex": [MockFunction], - "getType": [MockFunction], - "getVisibleTypes": [MockFunction], - "isHidden": [MockFunction], - "isImportableAndExportable": [MockFunction], - "isMultiNamespace": [MockFunction], - "isNamespaceAgnostic": [MockFunction], - "isSingleNamespace": [MockFunction], - "registerType": [MockFunction], - }, - }, - ], - ] - `); + expect(exportSavedObjectsToStream).toHaveBeenCalledWith({ + excludeExportDetails: true, + exportSizeLimit: EXPORT_LIMIT, + includeReferencesDeep: true, + namespace, + objects, + savedObjectsClient, + }); + + const importOptions = { + createNewCopies: false, + objectLimit: EXPORT_LIMIT, + overwrite: true, + readStream: expect.any(Readable), + savedObjectsClient, + typeRegistry, + }; + expect(importSavedObjectsFromStream).toHaveBeenNthCalledWith(1, { + ...importOptions, + namespace: 'destination1', + }); + expect(importSavedObjectsFromStream).toHaveBeenNthCalledWith(2, { + ...importOptions, + namespace: 'destination2', + }); }); it(`doesn't stop copy if some spaces fail`, async () => { - const objects = [ - { - type: 'dashboard', - id: 'my-dashboard', - attributes: {}, - }, - { - type: 'visualization', - id: 'my-viz', - attributes: {}, - }, - { - type: 'index-pattern', - id: 'my-index-pattern', - attributes: {}, - }, - ]; - const { savedObjects } = setup({ - objects, + objects: mockExportResults, importSavedObjectsFromStreamImpl: async (opts) => { if (opts.namespace === 'failure-space') { throw new Error(`Some error occurred!`); } - await expectStreamToContainObjects(opts.readStream, objects); + await expectStreamToContainObjects(opts.readStream, mockExportResults); return Promise.resolve({ success: true, successCount: 3, @@ -385,7 +204,7 @@ describe('copySavedObjectsToSpaces', () => { const copySavedObjectsToSpaces = copySavedObjectsToSpacesFactory( savedObjects, - () => 1000, + () => EXPORT_LIMIT, request ); @@ -395,65 +214,44 @@ describe('copySavedObjectsToSpaces', () => { { includeReferences: true, overwrite: true, - objects: [ - { - type: 'dashboard', - id: 'my-dashboard', - }, - ], + objects: [{ type: 'dashboard', id: 'my-dashboard' }], createNewCopies: false, } ); expect(result).toMatchInlineSnapshot(` - Object { - "failure-space": Object { - "errors": Array [ - [Error: Some error occurred!], - ], - "success": false, - "successCount": 0, - }, - "marketing": Object { - "errors": undefined, - "success": true, - "successCount": 3, - "successResults": Array [ - "Some success(es) occurred!", - ], - }, - "non-existent-space": Object { - "errors": undefined, - "success": true, - "successCount": 3, - "successResults": Array [ - "Some success(es) occurred!", - ], - }, - } - `); + Object { + "failure-space": Object { + "errors": Array [ + [Error: Some error occurred!], + ], + "success": false, + "successCount": 0, + }, + "marketing": Object { + "errors": undefined, + "success": true, + "successCount": 3, + "successResults": Array [ + "Some success(es) occurred!", + ], + }, + "non-existent-space": Object { + "errors": undefined, + "success": true, + "successCount": 3, + "successResults": Array [ + "Some success(es) occurred!", + ], + }, + } + `); }); it(`handles stream read errors`, async () => { const { savedObjects } = setup({ - objects: [ - { - type: 'dashboard', - id: 'my-dashboard', - attributes: {}, - }, - { - type: 'visualization', - id: 'my-viz', - attributes: {}, - }, - { - type: 'index-pattern', - id: 'my-index-pattern', - attributes: {}, - }, - ], - exportSavedObjectsToStreamImpl: (opts) => { + objects: mockExportResults, + exportSavedObjectsToStreamImpl: (_opts) => { return Promise.resolve( new Readable({ objectMode: true, @@ -469,7 +267,7 @@ describe('copySavedObjectsToSpaces', () => { const copySavedObjectsToSpaces = copySavedObjectsToSpacesFactory( savedObjects, - () => 1000, + () => EXPORT_LIMIT, request ); @@ -480,12 +278,7 @@ describe('copySavedObjectsToSpaces', () => { { includeReferences: true, overwrite: true, - objects: [ - { - type: 'dashboard', - id: 'my-dashboard', - }, - ], + objects: [{ type: 'dashboard', id: 'my-dashboard' }], createNewCopies: false, } ) diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts index 4373593ddd663..a124096ebb6a3 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts @@ -3,14 +3,19 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { Readable } from 'stream'; import { SavedObjectsImportResponse, SavedObjectsResolveImportErrorsOptions, SavedObjectsExportOptions, SavedObjectsImportSuccess, } from 'src/core/server'; -import { coreMock, httpServerMock } from 'src/core/server/mocks'; -import { Readable } from 'stream'; +import { + coreMock, + httpServerMock, + savedObjectsTypeRegistryMock, + savedObjectsClientMock, +} from 'src/core/server/mocks'; import { resolveCopySavedObjectsToSpacesConflictsFactory } from './resolve_copy_conflicts'; jest.mock('../../../../../../src/core/server', () => { @@ -32,6 +37,8 @@ interface SetupOpts { ) => Promise; } +const EXPORT_LIMIT = 1000; + const expectStreamToContainObjects = async ( stream: Readable, expectedObjects: SetupOpts['objects'] @@ -51,9 +58,20 @@ const expectStreamToContainObjects = async ( }; describe('resolveCopySavedObjectsToSpacesConflicts', () => { + const mockExportResults = [ + { type: 'dashboard', id: 'my-dashboard', attributes: {} }, + { type: 'visualization', id: 'my-viz', attributes: {} }, + { type: 'index-pattern', id: 'my-index-pattern', attributes: {} }, + ]; + const setup = (setupOpts: SetupOpts) => { const coreStart = coreMock.createStart(); + const savedObjectsClient = savedObjectsClientMock.create(); + const typeRegistry = savedObjectsTypeRegistryMock.create(); + coreStart.savedObjects.getScopedClient.mockReturnValue(savedObjectsClient); + coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); + (exportSavedObjectsToStream as jest.Mock).mockImplementation( async (opts: SavedObjectsExportOptions) => { return ( @@ -92,317 +110,94 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { return { savedObjects: coreStart.savedObjects, + savedObjectsClient, + typeRegistry, }; }; it('uses the Saved Objects Service to perform an export followed by a series of conflict resolution calls', async () => { - const { savedObjects } = setup({ - objects: [ - { - type: 'dashboard', - id: 'my-dashboard', - attributes: {}, - }, - { - type: 'visualization', - id: 'my-viz', - attributes: {}, - }, - { - type: 'index-pattern', - id: 'my-index-pattern', - attributes: {}, - }, - ], + const { savedObjects, savedObjectsClient, typeRegistry } = setup({ + objects: mockExportResults, }); const request = httpServerMock.createKibanaRequest(); const resolveCopySavedObjectsToSpacesConflicts = resolveCopySavedObjectsToSpacesConflictsFactory( savedObjects, - () => 1000, + () => EXPORT_LIMIT, request ); - const result = await resolveCopySavedObjectsToSpacesConflicts('sourceSpace', { + const namespace = 'sourceSpace'; + const objects = [{ type: 'dashboard', id: 'my-dashboard' }]; + const retries = { + destination1: [{ type: 'visualization', id: 'my-visualization', overwrite: true }], + destination2: [{ type: 'visualization', id: 'my-visualization', overwrite: false }], + }; + const result = await resolveCopySavedObjectsToSpacesConflicts(namespace, { includeReferences: true, - objects: [ - { - type: 'dashboard', - id: 'my-dashboard', - }, - ], - retries: { - destination1: [ - { - type: 'visualization', - id: 'my-visualization', - overwrite: true, - }, - ], - destination2: [ - { - type: 'visualization', - id: 'my-visualization', - overwrite: false, - }, - ], - }, + objects, + retries, createNewCopies: false, }); expect(result).toMatchInlineSnapshot(` - Object { - "destination1": Object { - "errors": undefined, - "success": true, - "successCount": 3, - "successResults": Array [ - "Some success(es) occurred!", - ], - }, - "destination2": Object { - "errors": undefined, - "success": true, - "successCount": 3, - "successResults": Array [ - "Some success(es) occurred!", - ], - }, - } - `); - - expect((exportSavedObjectsToStream as jest.Mock).mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - Object { - "excludeExportDetails": true, - "exportSizeLimit": 1000, - "includeReferencesDeep": true, - "namespace": "sourceSpace", - "objects": Array [ - Object { - "id": "my-dashboard", - "type": "dashboard", - }, - ], - "savedObjectsClient": Object { - "addToNamespaces": [MockFunction], - "bulkCreate": [MockFunction], - "bulkGet": [MockFunction], - "bulkUpdate": [MockFunction], - "checkConflicts": [MockFunction], - "create": [MockFunction], - "delete": [MockFunction], - "deleteFromNamespaces": [MockFunction], - "errors": [Function], - "find": [MockFunction], - "get": [MockFunction], - "update": [MockFunction], - }, - }, - ], - ] + Object { + "destination1": Object { + "errors": undefined, + "success": true, + "successCount": 3, + "successResults": Array [ + "Some success(es) occurred!", + ], + }, + "destination2": Object { + "errors": undefined, + "success": true, + "successCount": 3, + "successResults": Array [ + "Some success(es) occurred!", + ], + }, + } `); - expect((resolveSavedObjectsImportErrors as jest.Mock).mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - Object { - "createNewCopies": false, - "namespace": "destination1", - "objectLimit": 1000, - "readStream": Readable { - "_events": Object { - "data": [Function], - "end": [Function], - "error": [Function], - }, - "_eventsCount": 3, - "_maxListeners": undefined, - "_read": [Function], - "_readableState": ReadableState { - "autoDestroy": false, - "awaitDrain": 0, - "buffer": BufferList { - "head": null, - "length": 0, - "tail": null, - }, - "decoder": null, - "defaultEncoding": "utf8", - "destroyed": false, - "emitClose": true, - "emittedReadable": false, - "encoding": null, - "endEmitted": true, - "ended": true, - "flowing": true, - "highWaterMark": 16, - "length": 0, - "needReadable": false, - "objectMode": true, - "paused": false, - "pipes": null, - "pipesCount": 0, - "readableListening": false, - "reading": false, - "readingMore": false, - "resumeScheduled": false, - "sync": false, - }, - "readable": false, - }, - "retries": Array [ - Object { - "id": "my-visualization", - "overwrite": true, - "replaceReferences": Array [], - "type": "visualization", - }, - ], - "savedObjectsClient": Object { - "addToNamespaces": [MockFunction], - "bulkCreate": [MockFunction], - "bulkGet": [MockFunction], - "bulkUpdate": [MockFunction], - "checkConflicts": [MockFunction], - "create": [MockFunction], - "delete": [MockFunction], - "deleteFromNamespaces": [MockFunction], - "errors": [Function], - "find": [MockFunction], - "get": [MockFunction], - "update": [MockFunction], - }, - "typeRegistry": Object { - "getAllTypes": [MockFunction], - "getImportableAndExportableTypes": [MockFunction], - "getIndex": [MockFunction], - "getType": [MockFunction], - "getVisibleTypes": [MockFunction], - "isHidden": [MockFunction], - "isImportableAndExportable": [MockFunction], - "isMultiNamespace": [MockFunction], - "isNamespaceAgnostic": [MockFunction], - "isSingleNamespace": [MockFunction], - "registerType": [MockFunction], - }, - }, - ], - Array [ - Object { - "createNewCopies": false, - "namespace": "destination2", - "objectLimit": 1000, - "readStream": Readable { - "_events": Object { - "data": [Function], - "end": [Function], - "error": [Function], - }, - "_eventsCount": 3, - "_maxListeners": undefined, - "_read": [Function], - "_readableState": ReadableState { - "autoDestroy": false, - "awaitDrain": 0, - "buffer": BufferList { - "head": null, - "length": 0, - "tail": null, - }, - "decoder": null, - "defaultEncoding": "utf8", - "destroyed": false, - "emitClose": true, - "emittedReadable": false, - "encoding": null, - "endEmitted": true, - "ended": true, - "flowing": true, - "highWaterMark": 16, - "length": 0, - "needReadable": false, - "objectMode": true, - "paused": false, - "pipes": null, - "pipesCount": 0, - "readableListening": false, - "reading": false, - "readingMore": false, - "resumeScheduled": false, - "sync": false, - }, - "readable": false, - }, - "retries": Array [ - Object { - "id": "my-visualization", - "overwrite": false, - "replaceReferences": Array [], - "type": "visualization", - }, - ], - "savedObjectsClient": Object { - "addToNamespaces": [MockFunction], - "bulkCreate": [MockFunction], - "bulkGet": [MockFunction], - "bulkUpdate": [MockFunction], - "checkConflicts": [MockFunction], - "create": [MockFunction], - "delete": [MockFunction], - "deleteFromNamespaces": [MockFunction], - "errors": [Function], - "find": [MockFunction], - "get": [MockFunction], - "update": [MockFunction], - }, - "typeRegistry": Object { - "getAllTypes": [MockFunction], - "getImportableAndExportableTypes": [MockFunction], - "getIndex": [MockFunction], - "getType": [MockFunction], - "getVisibleTypes": [MockFunction], - "isHidden": [MockFunction], - "isImportableAndExportable": [MockFunction], - "isMultiNamespace": [MockFunction], - "isNamespaceAgnostic": [MockFunction], - "isSingleNamespace": [MockFunction], - "registerType": [MockFunction], - }, - }, - ], - ] - `); + expect(exportSavedObjectsToStream).toHaveBeenCalledWith({ + excludeExportDetails: true, + exportSizeLimit: EXPORT_LIMIT, + includeReferencesDeep: true, + namespace, + objects, + savedObjectsClient, + }); + + const importOptions = { + createNewCopies: false, + objectLimit: EXPORT_LIMIT, + readStream: expect.any(Readable), + savedObjectsClient, + typeRegistry, + }; + expect(resolveSavedObjectsImportErrors).toHaveBeenNthCalledWith(1, { + ...importOptions, + namespace: 'destination1', + retries: [{ ...retries.destination1[0], replaceReferences: [] }], + }); + expect(resolveSavedObjectsImportErrors).toHaveBeenNthCalledWith(2, { + ...importOptions, + namespace: 'destination2', + retries: [{ ...retries.destination2[0], replaceReferences: [] }], + }); }); it(`doesn't stop resolution if some spaces fail`, async () => { - const objects = [ - { - type: 'dashboard', - id: 'my-dashboard', - attributes: {}, - }, - { - type: 'visualization', - id: 'my-viz', - attributes: {}, - }, - { - type: 'index-pattern', - id: 'my-index-pattern', - attributes: {}, - }, - ]; - const { savedObjects } = setup({ - objects, + objects: mockExportResults, resolveSavedObjectsImportErrorsImpl: async (opts) => { if (opts.namespace === 'failure-space') { throw new Error(`Some error occurred!`); } - await expectStreamToContainObjects(opts.readStream, objects); + await expectStreamToContainObjects(opts.readStream, mockExportResults); return Promise.resolve({ success: true, successCount: 3, @@ -415,71 +210,50 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { const resolveCopySavedObjectsToSpacesConflicts = resolveCopySavedObjectsToSpacesConflictsFactory( savedObjects, - () => 1000, + () => EXPORT_LIMIT, request ); const result = await resolveCopySavedObjectsToSpacesConflicts('sourceSpace', { includeReferences: true, - objects: [ - { - type: 'dashboard', - id: 'my-dashboard', - }, - ], + objects: [{ type: 'dashboard', id: 'my-dashboard' }], retries: { - ['failure-space']: [ - { - type: 'visualization', - id: 'my-visualization', - overwrite: true, - }, - ], + ['failure-space']: [{ type: 'visualization', id: 'my-visualization', overwrite: true }], ['non-existent-space']: [ - { - type: 'visualization', - id: 'my-visualization', - overwrite: false, - }, - ], - ['marketing']: [ - { - type: 'visualization', - id: 'my-visualization', - overwrite: true, - }, + { type: 'visualization', id: 'my-visualization', overwrite: false }, ], + marketing: [{ type: 'visualization', id: 'my-visualization', overwrite: true }], }, createNewCopies: false, }); expect(result).toMatchInlineSnapshot(` - Object { - "failure-space": Object { - "errors": Array [ - [Error: Some error occurred!], - ], - "success": false, - "successCount": 0, - }, - "marketing": Object { - "errors": undefined, - "success": true, - "successCount": 3, - "successResults": Array [ - "Some success(es) occurred!", - ], - }, - "non-existent-space": Object { - "errors": undefined, - "success": true, - "successCount": 3, - "successResults": Array [ - "Some success(es) occurred!", - ], - }, - } - `); + Object { + "failure-space": Object { + "errors": Array [ + [Error: Some error occurred!], + ], + "success": false, + "successCount": 0, + }, + "marketing": Object { + "errors": undefined, + "success": true, + "successCount": 3, + "successResults": Array [ + "Some success(es) occurred!", + ], + }, + "non-existent-space": Object { + "errors": undefined, + "success": true, + "successCount": 3, + "successResults": Array [ + "Some success(es) occurred!", + ], + }, + } + `); }); it(`handles stream read errors`, async () => { @@ -501,7 +275,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { const resolveCopySavedObjectsToSpacesConflicts = resolveCopySavedObjectsToSpacesConflictsFactory( savedObjects, - () => 1000, + () => EXPORT_LIMIT, request ); From 45b37c6a10d7052b2b4e59548f18995845fade38 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Wed, 8 Jul 2020 09:58:38 -0400 Subject: [PATCH 40/55] Fix integration tests that broke due to `trueCopy` rename --- .../common/suites/import.ts | 10 +++++----- .../common/suites/resolve_import_errors.ts | 12 ++++++------ .../security_and_spaces/apis/import.ts | 10 +++++----- .../apis/resolve_import_errors.ts | 6 +++--- .../security_only/apis/import.ts | 10 +++++----- .../security_only/apis/resolve_import_errors.ts | 6 +++--- .../spaces_only/apis/import.ts | 10 +++++----- .../spaces_only/apis/resolve_import_errors.ts | 6 +++--- 8 files changed, 35 insertions(+), 35 deletions(-) diff --git a/x-pack/test/saved_object_api_integration/common/suites/import.ts b/x-pack/test/saved_object_api_integration/common/suites/import.ts index 2e63537e8f4d5..9d0e5a361ab68 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/import.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/import.ts @@ -114,7 +114,7 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe // the new ID was randomly generated expect(destinationId).to.match(/^[0-9a-f-]{36}$/); } - } else if (successParam === 'createNewCopies' || successParam === 'newOrigin') { + } else if (successParam === 'createNewCopies' || successParam === 'createNewCopy') { // the new ID was randomly generated expect(destinationId).to.match(/^[0-9a-f-]{36}$/); } else { @@ -124,11 +124,11 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe // This assertion is only needed for the case where `createNewCopies` mode is disabled and ambiguous source conflicts are detected. // When `createNewCopies` mode is permanently enabled, this field will be removed, and this assertion will be redundant and can be // removed too. - const resultNewCopies = object!.createNewCopies as boolean | undefined; - if (successParam === 'newOrigin') { - expect(resultNewCopies).to.be(true); + const createNewCopy = object!.createNewCopy as boolean | undefined; + if (successParam === 'createNewCopy') { + expect(createNewCopy).to.be(true); } else { - expect(resultNewCopies).to.be(undefined); + expect(createNewCopy).to.be(undefined); } if (!singleRequest || overwrite || createNewCopies) { diff --git a/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts index 6c9d2596400ea..d5c8fe32ec262 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts @@ -84,7 +84,7 @@ const createRequest = ( id, overwrite, ...(expectedNewId && { destinationId: expectedNewId }), - ...(successParam === 'newOrigin' && { createNewCopy: true }), + ...(successParam === 'createNewCopy' && { createNewCopy: true }), }, ], }); @@ -134,7 +134,7 @@ export function resolveImportErrorsTestSuiteFactory( // the new ID was randomly generated expect(destinationId).to.match(/^[0-9a-f-]{36}$/); } - } else if (successParam === 'createNewCopies' || successParam === 'newOrigin') { + } else if (successParam === 'createNewCopies' || successParam === 'createNewCopy') { expect(destinationId).to.be(expectedNewId!); } else { expect(destinationId).to.be(undefined); @@ -143,11 +143,11 @@ export function resolveImportErrorsTestSuiteFactory( // This assertion is only needed for the case where `createNewCopies` mode is disabled and ambiguous source conflicts are detected. // When `createNewCopies` mode is permanently enabled, this field will be removed, and this assertion will be redundant and can be // removed too. - const resultNewCopies = object!.createNewCopies as boolean | undefined; - if (successParam === 'newOrigin') { - expect(resultNewCopies).to.be(true); + const createNewCopy = object!.createNewCopy as boolean | undefined; + if (successParam === 'createNewCopy') { + expect(createNewCopy).to.be(true); } else { - expect(resultNewCopies).to.be(undefined); + expect(createNewCopy).to.be(undefined); } const { _source } = await expectResponses.successCreated( diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts index 9d3a763057fed..9f7226e5d44b5 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts @@ -22,7 +22,7 @@ const { const { fail400, fail409 } = testCaseFailures; const destinationId = (condition?: boolean) => condition !== false ? { successParam: 'destinationId' } : {}; -const newOrigin = () => ({ successParam: 'newOrigin' }); +const newCopy = () => ({ successParam: 'createNewCopy' }); const ambiguousConflict = (suffix: string) => ({ failure: 409 as 409, fail409Param: `ambiguous_conflict_${suffix}`, @@ -73,8 +73,8 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { ...fail409(!overwrite && spaceId === SPACE_2_ID), ...destinationId(spaceId !== SPACE_2_ID), }, - { ...CASES.CONFLICT_1A_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID - { ...CASES.CONFLICT_1B_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_1A_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_1B_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict ]; @@ -88,8 +88,8 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { { ...CASES.CONFLICT_1_OBJ, ...fail409(!overwrite) }, // "exact match" conflict CASES.CONFLICT_1A_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match CASES.CONFLICT_1B_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match - { ...CASES.CONFLICT_2C_OBJ, ...newOrigin() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID - { ...CASES.CONFLICT_2D_OBJ, ...newOrigin() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_2C_OBJ, ...newCopy() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_2D_OBJ, ...newCopy() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID ]; return { group1Importable, group1NonImportable, group1All, group2, group3, group4 }; }; diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts index ec8fa2390c852..792fe63e5932d 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts @@ -23,7 +23,7 @@ const { const { fail400, fail409 } = testCaseFailures; const destinationId = (condition?: boolean) => condition !== false ? { successParam: 'destinationId' } : {}; -const newOrigin = () => ({ successParam: 'newOrigin' }); +const newCopy = () => ({ successParam: 'createNewCopy' }); const createNewCopiesTestCases = () => { // for each outcome, if failure !== undefined then we expect to receive @@ -70,8 +70,8 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { ...fail409(!overwrite && spaceId === SPACE_2_ID), ...destinationId(spaceId !== SPACE_2_ID), }, - { ...CASES.CONFLICT_1A_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID - { ...CASES.CONFLICT_1B_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_1A_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_1B_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID // all of the cases below represent imports that had an inexact match conflict or an ambiguous conflict // if we call _resolve_import_errors and don't specify overwrite, each of these will result in a conflict because an object with that // `expectedDestinationId` already exists diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/import.ts b/x-pack/test/saved_object_api_integration/security_only/apis/import.ts index 61e42c4dce422..62c5dfbb664d7 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/import.ts @@ -16,7 +16,7 @@ import { const { fail400, fail409 } = testCaseFailures; const destinationId = (condition?: boolean) => condition !== false ? { successParam: 'destinationId' } : {}; -const newOrigin = () => ({ successParam: 'newOrigin' }); +const newCopy = () => ({ successParam: 'createNewCopy' }); const ambiguousConflict = (suffix: string) => ({ failure: 409 as 409, fail409Param: `ambiguous_conflict_${suffix}`, @@ -52,8 +52,8 @@ const createTestCases = (overwrite: boolean) => { { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite) }, { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...destinationId() }, { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...destinationId() }, - { ...CASES.CONFLICT_1A_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID - { ...CASES.CONFLICT_1B_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_1A_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_1B_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict ]; @@ -67,8 +67,8 @@ const createTestCases = (overwrite: boolean) => { { ...CASES.CONFLICT_1_OBJ, ...fail409(!overwrite) }, // "exact match" conflict CASES.CONFLICT_1A_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match CASES.CONFLICT_1B_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match - { ...CASES.CONFLICT_2C_OBJ, ...newOrigin() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID - { ...CASES.CONFLICT_2D_OBJ, ...newOrigin() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_2C_OBJ, ...newCopy() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_2D_OBJ, ...newCopy() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID ]; return { group1Importable, group1NonImportable, group1All, group2, group3, group4 }; }; diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts index 7c0bb9dadf123..91134dd14bd8a 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts @@ -17,7 +17,7 @@ import { const { fail400, fail409 } = testCaseFailures; const destinationId = (condition?: boolean) => condition !== false ? { successParam: 'destinationId' } : {}; -const newOrigin = () => ({ successParam: 'newOrigin' }); +const newCopy = () => ({ successParam: 'createNewCopy' }); const createNewCopiesTestCases = () => { // for each outcome, if failure !== undefined then we expect to receive @@ -44,8 +44,8 @@ const createTestCases = (overwrite: boolean) => { const group1All = [...group1Importable, ...group1NonImportable]; const group2 = [ { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite) }, - { ...CASES.CONFLICT_1A_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID - { ...CASES.CONFLICT_1B_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_1A_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_1B_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID // all of the cases below represent imports that had an inexact match conflict or an ambiguous conflict // if we call _resolve_import_errors and don't specify overwrite, each of these will result in a conflict because an object with that // `expectedDestinationId` already exists diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts index 1340f86683a1d..a36249528540b 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts @@ -17,7 +17,7 @@ const { const { fail400, fail409 } = testCaseFailures; const destinationId = (condition?: boolean) => condition !== false ? { successParam: 'destinationId' } : {}; -const newOrigin = () => ({ successParam: 'newOrigin' }); +const newCopy = () => ({ successParam: 'createNewCopy' }); const ambiguousConflict = (suffix: string) => ({ failure: 409 as 409, fail409Param: `ambiguous_conflict_${suffix}`, @@ -61,8 +61,8 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { }, { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, { ...CASES.HIDDEN, ...fail400() }, - { ...CASES.CONFLICT_1A_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID - { ...CASES.CONFLICT_1B_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_1A_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_1B_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict CASES.NEW_SINGLE_NAMESPACE_OBJ, @@ -79,8 +79,8 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { { ...CASES.CONFLICT_1_OBJ, ...fail409(!overwrite) }, // "exact match" conflict CASES.CONFLICT_1A_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match CASES.CONFLICT_1B_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match - { ...CASES.CONFLICT_2C_OBJ, ...newOrigin() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID - { ...CASES.CONFLICT_2D_OBJ, ...newOrigin() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_2C_OBJ, ...newCopy() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_2D_OBJ, ...newCopy() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID ]; return { group1, group2, group3 }; }; diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts index c135de588b2de..1431a61b1cbe0 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts @@ -21,7 +21,7 @@ const { const { fail400, fail409 } = testCaseFailures; const destinationId = (condition?: boolean) => condition !== false ? { successParam: 'destinationId' } : {}; -const newOrigin = () => ({ successParam: 'newOrigin' }); +const newCopy = () => ({ successParam: 'createNewCopy' }); const createNewCopiesTestCases = () => { // for each outcome, if failure !== undefined then we expect to receive @@ -65,8 +65,8 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { }, { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, { ...CASES.HIDDEN, ...fail400() }, - { ...CASES.CONFLICT_1A_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID - { ...CASES.CONFLICT_1B_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_1A_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_1B_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID // all of the cases below represent imports that had an inexact match conflict or an ambiguous conflict // if we call _resolve_import_errors and don't specify overwrite, each of these will result in a conflict because an object with that // `expectedDestinationId` already exists From 3538c2baac22ebce78e23dfe5f3189b4dca8049e Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Wed, 8 Jul 2020 09:59:03 -0400 Subject: [PATCH 41/55] Change CTS to omit namespace-agnostic object types before import `lib/get_eligible_types.ts` was actually obsolete, I thought I had removed it earlier; I took this opportunity to repurpose it. --- .../lib/copy_to_spaces/copy_to_spaces.test.ts | 34 ++++++++++++++++--- .../lib/copy_to_spaces/copy_to_spaces.ts | 7 +++- .../copy_to_spaces/lib/get_eligible_types.ts | 16 --------- .../lib/get_ineligible_types.ts | 24 +++++++++++++ .../resolve_copy_conflicts.test.ts | 33 +++++++++++++++--- .../copy_to_spaces/resolve_copy_conflicts.ts | 7 +++- 6 files changed, 95 insertions(+), 26 deletions(-) delete mode 100644 x-pack/plugins/spaces/server/lib/copy_to_spaces/lib/get_eligible_types.ts create mode 100644 x-pack/plugins/spaces/server/lib/copy_to_spaces/lib/get_ineligible_types.ts diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts index 365d0494b12c8..d49dfa2015dc6 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts @@ -62,6 +62,7 @@ describe('copySavedObjectsToSpaces', () => { { type: 'dashboard', id: 'my-dashboard', attributes: {} }, { type: 'visualization', id: 'my-viz', attributes: {} }, { type: 'index-pattern', id: 'my-index-pattern', attributes: {} }, + { type: 'globaltype', id: 'my-globaltype', attributes: {} }, ]; const setup = (setupOpts: SetupOpts) => { @@ -72,6 +73,27 @@ describe('copySavedObjectsToSpaces', () => { coreStart.savedObjects.getScopedClient.mockReturnValue(savedObjectsClient); coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); + typeRegistry.getImportableAndExportableTypes.mockReturnValue([ + // don't need to include all types, just need a positive case (agnostic) and a negative case (non-agnostic) + { + name: 'dashboard', + namespaceType: 'single', + hidden: false, + mappings: { properties: {} }, + }, + { + name: 'globaltype', + namespaceType: 'agnostic', + hidden: false, + mappings: { properties: {} }, + }, + ]); + typeRegistry.isNamespaceAgnostic.mockImplementation((type: string) => + typeRegistry + .getImportableAndExportableTypes() + .some((t) => t.name === type && t.namespaceType === 'agnostic') + ); + (exportSavedObjectsToStream as jest.Mock).mockImplementation( async (opts: SavedObjectsExportOptions) => { return ( @@ -91,10 +113,12 @@ describe('copySavedObjectsToSpaces', () => { (importSavedObjectsFromStream as jest.Mock).mockImplementation( async (opts: SavedObjectsImportOptions) => { const defaultImpl = async () => { - await expectStreamToContainObjects(opts.readStream, setupOpts.objects); + // namespace-agnostic types should be filtered out before import + const filteredObjects = setupOpts.objects.filter(({ type }) => type !== 'globaltype'); + await expectStreamToContainObjects(opts.readStream, filteredObjects); const response: SavedObjectsImportResponse = { success: true, - successCount: setupOpts.objects.length, + successCount: filteredObjects.length, successResults: [ ('Some success(es) occurred!' as unknown) as SavedObjectsImportSuccess, ], @@ -191,10 +215,12 @@ describe('copySavedObjectsToSpaces', () => { if (opts.namespace === 'failure-space') { throw new Error(`Some error occurred!`); } - await expectStreamToContainObjects(opts.readStream, mockExportResults); + // namespace-agnostic types should be filtered out before import + const filteredObjects = mockExportResults.filter(({ type }) => type !== 'globaltype'); + await expectStreamToContainObjects(opts.readStream, filteredObjects); return Promise.resolve({ success: true, - successCount: 3, + successCount: filteredObjects.length, successResults: [('Some success(es) occurred!' as unknown) as SavedObjectsImportSuccess], }); }, diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts index 41fcd42677767..5575052d7bbb8 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts @@ -16,6 +16,7 @@ import { createReadableStreamFromArray } from './lib/readable_stream_from_array' import { createEmptyFailureResponse } from './lib/create_empty_failure_response'; import { readStreamToCompletion } from './lib/read_stream_to_completion'; import { COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS } from './lib/saved_objects_client_opts'; +import { getIneligibleTypes } from './lib/get_ineligible_types'; export function copySavedObjectsToSpacesFactory( savedObjects: CoreStart['savedObjects'], @@ -77,11 +78,15 @@ export function copySavedObjectsToSpacesFactory( const response: CopyResponse = {}; const exportedSavedObjects = await exportRequestedObjects(sourceSpaceId, options); + const ineligibleTypes = getIneligibleTypes(getTypeRegistry()); + const filteredObjects = exportedSavedObjects.filter( + ({ type }) => !ineligibleTypes.includes(type) + ); for (const spaceId of destinationSpaceIds) { response[spaceId] = await importObjectsToSpace( spaceId, - createReadableStreamFromArray(exportedSavedObjects), + createReadableStreamFromArray(filteredObjects), options ); } diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/lib/get_eligible_types.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/lib/get_eligible_types.ts deleted file mode 100644 index e5f2c5b18bd00..0000000000000 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/lib/get_eligible_types.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { SavedObjectTypeRegistry } from 'src/core/server'; - -export function getEligibleTypes( - typeRegistry: Pick -) { - return typeRegistry - .getAllTypes() - .filter((type) => !typeRegistry.isNamespaceAgnostic(type.name)) - .map((type) => type.name); -} diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/lib/get_ineligible_types.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/lib/get_ineligible_types.ts new file mode 100644 index 0000000000000..91d4cb13b98eb --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/lib/get_ineligible_types.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectTypeRegistry } from 'src/core/server'; + +/** + * This function returns any importable/exportable saved object types that are namespace-agnostic. Even if these are eligible for + * import/export, we should not include them in the copy operation because it will result in a conflict that needs to overwrite itself to be + * resolved. + */ +export function getIneligibleTypes( + typeRegistry: Pick< + SavedObjectTypeRegistry, + 'getImportableAndExportableTypes' | 'isNamespaceAgnostic' + > +) { + return typeRegistry + .getImportableAndExportableTypes() + .filter((type) => typeRegistry.isNamespaceAgnostic(type.name)) + .map((type) => type.name); +} diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts index a124096ebb6a3..6a77bf7397cb5 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts @@ -72,6 +72,27 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { coreStart.savedObjects.getScopedClient.mockReturnValue(savedObjectsClient); coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); + typeRegistry.getImportableAndExportableTypes.mockReturnValue([ + // don't need to include all types, just need a positive case (agnostic) and a negative case (non-agnostic) + { + name: 'dashboard', + namespaceType: 'single', + hidden: false, + mappings: { properties: {} }, + }, + { + name: 'globaltype', + namespaceType: 'agnostic', + hidden: false, + mappings: { properties: {} }, + }, + ]); + typeRegistry.isNamespaceAgnostic.mockImplementation((type: string) => + typeRegistry + .getImportableAndExportableTypes() + .some((t) => t.name === type && t.namespaceType === 'agnostic') + ); + (exportSavedObjectsToStream as jest.Mock).mockImplementation( async (opts: SavedObjectsExportOptions) => { return ( @@ -91,11 +112,13 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { (resolveSavedObjectsImportErrors as jest.Mock).mockImplementation( async (opts: SavedObjectsResolveImportErrorsOptions) => { const defaultImpl = async () => { - await expectStreamToContainObjects(opts.readStream, setupOpts.objects); + // namespace-agnostic types should be filtered out before import + const filteredObjects = setupOpts.objects.filter(({ type }) => type !== 'globaltype'); + await expectStreamToContainObjects(opts.readStream, filteredObjects); const response: SavedObjectsImportResponse = { success: true, - successCount: setupOpts.objects.length, + successCount: filteredObjects.length, successResults: [ ('Some success(es) occurred!' as unknown) as SavedObjectsImportSuccess, ], @@ -197,10 +220,12 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { if (opts.namespace === 'failure-space') { throw new Error(`Some error occurred!`); } - await expectStreamToContainObjects(opts.readStream, mockExportResults); + // namespace-agnostic types should be filtered out before import + const filteredObjects = mockExportResults.filter(({ type }) => type !== 'globaltype'); + await expectStreamToContainObjects(opts.readStream, filteredObjects); return Promise.resolve({ success: true, - successCount: 3, + successCount: filteredObjects.length, successResults: [('Some success(es) occurred!' as unknown) as SavedObjectsImportSuccess], }); }, diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts index 700b34c067d8e..d433712bb9412 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts @@ -16,6 +16,7 @@ import { createEmptyFailureResponse } from './lib/create_empty_failure_response' import { readStreamToCompletion } from './lib/read_stream_to_completion'; import { createReadableStreamFromArray } from './lib/readable_stream_from_array'; import { COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS } from './lib/saved_objects_client_opts'; +import { getIneligibleTypes } from './lib/get_ineligible_types'; export function resolveCopySavedObjectsToSpacesConflictsFactory( savedObjects: CoreStart['savedObjects'], @@ -79,6 +80,10 @@ export function resolveCopySavedObjectsToSpacesConflictsFactory( includeReferences: options.includeReferences, objects: options.objects, }); + const ineligibleTypes = getIneligibleTypes(getTypeRegistry()); + const filteredObjects = exportedSavedObjects.filter( + ({ type }) => !ineligibleTypes.includes(type) + ); for (const entry of Object.entries(options.retries)) { const [spaceId, entryRetries] = entry; @@ -87,7 +92,7 @@ export function resolveCopySavedObjectsToSpacesConflictsFactory( response[spaceId] = await resolveConflictsForSpace( spaceId, - createReadableStreamFromArray(exportedSavedObjects), + createReadableStreamFromArray(filteredObjects), retries, options.createNewCopies ); From 2177c4a3db48d0dd18de3790f632d230e986f575 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Wed, 8 Jul 2020 10:32:36 -0400 Subject: [PATCH 42/55] Limit concurrent search requests --- package.json | 1 + .../saved_objects/import/check_origin_conflicts.ts | 13 +++++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index bb28c9e27e9f7..2e05d071199e4 100644 --- a/package.json +++ b/package.json @@ -228,6 +228,7 @@ "node-forge": "^0.9.1", "opn": "^5.5.0", "oppsy": "^2.0.0", + "p-map": "^4.0.0", "pegjs": "0.10.0", "postcss-loader": "^3.0.0", "prop-types": "15.6.0", diff --git a/src/core/server/saved_objects/import/check_origin_conflicts.ts b/src/core/server/saved_objects/import/check_origin_conflicts.ts index db838b949dae8..6c8a8a062d2eb 100644 --- a/src/core/server/saved_objects/import/check_origin_conflicts.ts +++ b/src/core/server/saved_objects/import/check_origin_conflicts.ts @@ -17,6 +17,7 @@ * under the License. */ +import pMap from 'p-map'; import { v4 as uuidv4 } from 'uuid'; import { SavedObject, @@ -54,6 +55,8 @@ interface Right { type Either = Left | Right; const isLeft = (object: Either): object is Left => object.tag === 'left'; +const MAX_CONCURRENT_SEARCHES = 10; + const createQueryTerm = (input: string) => input.replace(/\\/g, '\\\\').replace(/\"/g, '\\"'); const createQuery = (type: string, id: string, rawIdPrefix: string) => `"${createQueryTerm(`${rawIdPrefix}${type}:${id}`)}" | "${createQueryTerm(id)}"`; @@ -141,10 +144,12 @@ export async function checkOriginConflicts( objects: Array>, options: CheckOriginConflictsOptions ) { - // Check each object for possible destination conflicts. - const checkOriginConflictResults = await Promise.all( - objects.map((object) => checkOriginConflict(object, options)) - ); + // Check each object for possible destination conflicts, ensuring we don't too many concurrent searches running. + const mapper = async (object: SavedObject<{ title?: string }>) => + checkOriginConflict(object, options); + const checkOriginConflictResults = await pMap(objects, mapper, { + concurrency: MAX_CONCURRENT_SEARCHES, + }); // Get a map of all inexact matches that share the same destination(s). const ambiguousConflictSourcesMap = checkOriginConflictResults From 3c632dbbc3f2cc35dffcb69c90e4f592bbced81f Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Wed, 8 Jul 2020 15:47:35 -0400 Subject: [PATCH 43/55] Updated dev docs for Import and Copy API routes I chose not to include details for the "ambiguous source" edge case where an object may be returned with a `createNewCopy` flag. The examples are pretty verbose as-is, and this is a case that we don't intend to support forever. --- docs/api/saved-objects/import.asciidoc | 158 ++++++++++++- .../resolve_import_errors.asciidoc | 126 +++++++--- .../copy_saved_objects.asciidoc | 223 +++++++++++++++--- ...olve_copy_saved_objects_conflicts.asciidoc | 90 +++++-- 4 files changed, 509 insertions(+), 88 deletions(-) diff --git a/docs/api/saved-objects/import.asciidoc b/docs/api/saved-objects/import.asciidoc index b3e4c48696a17..16320af32493c 100644 --- a/docs/api/saved-objects/import.asciidoc +++ b/docs/api/saved-objects/import.asciidoc @@ -22,8 +22,17 @@ experimental[] Create sets of {kib} saved objects from a file created by the exp [[saved-objects-api-import-query-params]] ==== Query parameters +`createNewCopies`:: + (Optional, boolean) Creates new copies of saved objects, regenerating each object's ID and resetting its origin in the process. If this + option is used, potential conflict errors will be avoided. ++ +NOTE: This cannot be used with the `overwrite` option. + `overwrite`:: - (Optional, boolean) Overwrites saved objects. + (Optional, boolean) Overwrites saved objects if they already exist. If this option is used, potential conflict errors will be + automatically resolved by overwriting the destination object. ++ +NOTE: This cannot be used with the `createNewCopies` option. [[saved-objects-api-import-request-body]] ==== Request body @@ -37,22 +46,77 @@ The request body must include the multipart/form-data type. ==== Response body `success`:: - Top-level property that indicates if the import was successful. + (boolean) Indicates if the import was completely successful. When set to `false`, some objects may have been copied. For additional + information, refer to the `errors` and `successResults` properties. `successCount`:: - Indicates the number of successfully imported records. + (number) Indicates the number of successfully imported records. `errors`:: (array) Indicates the import was unsuccessful and specifies the objects that failed to import. +`successResults`:: + (array) Indicates the objects that were imported successfully, with any metadata if applicable. ++ +NOTE: No objects are actually created until all resolvable errors have been addressed! This includes conflict errors and missing references +errors. See the <> documentation for more information. + [[saved-objects-api-import-codes]] ==== Response code `200`:: Indicates a successful call. +[[saved-objects-api-import-example]] ==== Examples +[[saved-objects-api-import-example-1]] +===== 1. Successful import (with `createNewCopies` enabled) + +Import an index pattern and dashboard: + +[source,sh] +-------------------------------------------------- +$ curl -X POST "localhost:5601/api/saved_objects/_import?createNewCopies=true" -H "kbn-xsrf: true" --form file=@file.ndjson +-------------------------------------------------- +// KIBANA + +The `file.ndjson` file contains the following: + +[source,sh] +-------------------------------------------------- +{"type":"index-pattern","id":"my-pattern","attributes":{"title":"my-pattern-*"}} +{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"}} +-------------------------------------------------- + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "success": true, + "successCount": 2, + "successResults": [ + { + "id": "my-pattern", + "type": "index-pattern", + "destinationId": "4aba3770-0d04-45e1-9e34-4cf0fd2165ae" + }, + { + "id": "my-dashboard", + "type": "dashboard", + "destinationId": "c31d1eca-9bc0-4a81-b5f9-30c442824c48" + } + ] +} +-------------------------------------------------- + +This result indicates that the import was successful, and both objects were created. Since these objects were created as new copies, each +entry in the `successResults` array includes a `destinationId` attribute. + +[[saved-objects-api-import-example-2]] +===== 2. Successful import (with `createNewCopies` disabled) + Import an index pattern and dashboard: [source,sh] @@ -75,11 +139,26 @@ The API returns the following: -------------------------------------------------- { "success": true, - "successCount": 2 + "successCount": 2, + "successResults": [ + { + "id": "my-pattern", + "type": "index-pattern" + }, + { + "id": "my-dashboard", + "type": "dashboard" + } + ] } -------------------------------------------------- -Import an index pattern and dashboard that includes a conflict on the index pattern: +This result indicates that the import was successful, and both objects were created. + +[[saved-objects-api-import-example-3]] +===== 3. Failed import (with conflict errors) + +Import an index pattern, visualization, canvas, and dashboard, where some objects already exists: [source,sh] -------------------------------------------------- @@ -92,6 +171,8 @@ The `file.ndjson` file contains the following: [source,sh] -------------------------------------------------- {"type":"index-pattern","id":"my-pattern","attributes":{"title":"my-pattern-*"}} +{"type":"visualization","id":"my-vis","attributes":{"title":"Look at my visualization"}} +{"type":"canvas-workpad","id":"my-canvas","attributes":{"name":"Look at my canvas"}} {"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"}} -------------------------------------------------- @@ -109,13 +190,69 @@ The API returns the following: "title": "my-pattern-*", "error": { "type": "conflict" - }, + } + }, + { + "id": "my-visualization", + "type": "my-vis", + "title": "Look at my visualization", + "error": { + "type": "conflict", + "destinationId": "another-vis" + } }, + { + "id": "my-canvas", + "type": "canvas-workpad", + "title": "Look at my canvas", + "error": { + "type": "ambiguous_conflict", + "destinations": [ + { + "id": "another-canvas", + "title": "Look at another canvas", + "updatedAt": "2020-07-08T16:36:32.377Z" + }, + { + "id": "yet-another-canvas", + "title": "Look at yet another canvas", + "updatedAt": "2020-07-05T12:29:54.849Z" + } + ] + } + } ], + "successResults": [ + { + "id": "my-dashboard", + "type": "dashboard" + } + ] } -------------------------------------------------- -Import a visualization and dashboard with an index pattern for the visualization reference that doesn't exist: +This result indicates that the import was not successful because the index pattern, visualization, and dashboard each resulted in a conflict error: + +* An index pattern with the same ID already exists, so this resulted in a conflict error. This can be resolved by overwriting the existing +object, or skipping this object entirely. + +* A visualization with a different ID but the same "origin" already exists, so this resulted in a conflict error as well. The +`destinationId` field describes to the other visualization which caused this conflict. This behavior was added to ensure that new objects +which can be shared between spaces behave in a similar way as legacy non-shareable objects. When a shareable object is exported and then +imported into a new space, it retains its origin so that it conflicts will be encountered as expected. This can be resolved by overwriting +the specified destination object, or skipping this object entirely. + +* Two canvases with different IDs but the same "origin" already exist, so this resulted in an ambiguous conflict error. The `destinations` +array describes to the other canvases which caused this conflict. When a shareable object is exported and then imported into a new space, +and is _then_ shared to another space where an object of the same origin exists, this situation may occur. This can be resolved by picking +one of the destination objects to overwrite, or skipping this object entirely. + +These errors need to be addressed using the <>. + +[[saved-objects-api-import-example-4]] +===== 4. Failed import (with missing reference errors) + +Import a visualization and dashboard with an index pattern for the visualization reference that doesn\'t exist: [source,sh] -------------------------------------------------- @@ -127,7 +264,7 @@ The `file.ndjson` file contains the following: [source,sh] -------------------------------------------------- -{"type":"visualization","id":"my-vis","attributes":{"title":"my-vis"},"references":[{"name":"ref_0","type":"index-pattern","id":"my-pattern-*"}]} +{"type":"visualization","id":"my-vis","attributes":{"title":"Look at my visualization"},"references":[{"name":"ref_0","type":"index-pattern","id":"my-pattern-*"}]} {"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"},"references":[{"name":"ref_0","type":"visualization","id":"my-vis"}]} -------------------------------------------------- @@ -141,7 +278,7 @@ The API returns the following: { "id": "my-vis", "type": "visualization", - "title": "my-vis", + "title": "Look at my visualization", "error": { "type": "missing_references", "references": [ @@ -160,3 +297,6 @@ The API returns the following: } ] -------------------------------------------------- + +This result indicates that the import was not successful because the visualization resulted in a missing references error. This error needs +to be addressed using the <>. diff --git a/docs/api/saved-objects/resolve_import_errors.asciidoc b/docs/api/saved-objects/resolve_import_errors.asciidoc index ec03917390d36..6ec8eb8d6f777 100644 --- a/docs/api/saved-objects/resolve_import_errors.asciidoc +++ b/docs/api/saved-objects/resolve_import_errors.asciidoc @@ -4,7 +4,7 @@ Resolve import errors ++++ -experimental[] Resolve errors from the import API. +experimental[] Resolve errors from the <>. To resolve errors, you can: @@ -27,6 +27,13 @@ To resolve errors, you can: `space_id`:: (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. +[[saved-objects-api-resolve-import-errors-query-params]] +==== Query parameters + +`createNewCopies`:: + (Optional, boolean) Creates new copies of saved objects, regenerating each object's ID and resetting its origin in the process. If this + option was enabled during the initial import, it should also be enabled when resolving import errors. + [[saved-objects-api-resolve-import-errors-request-body]] ==== Request body @@ -36,19 +43,42 @@ The request body must include the multipart/form-data type. The same file given to the import API. `retries`:: - (array) A list of `type`, `id`, `replaceReferences`, and `overwrite` objects to retry. The property `replaceReferences` is a list of `type`, `from`, and `to` used to change the object references. + (Required, array) The retry operations to attempt, which can specify how to resolve different types of errors. ++ +.Properties of `` +[%collapsible%open] +===== + `type`::: + (Required, string) The saved object type. + `id`::: + (Required, string) The saved object ID. + `overwrite`::: + (Optional, boolean) When set to `true`, the source object overwrites the conflicting destination object. When set to `false`, this does + nothing. + `destinationId`::: + (Optional, string) Specifies which destination ID the imported object should have (if different from the current ID). + `replaceReferences`::: + (Optional, array) A list of `type`, `from`, and `to` used to change the object references. +===== [[saved-objects-api-resolve-import-errors-response-body]] ==== Response body `success`:: - Top-level property that indicates if the errors successfully resolved. + (boolean) Indicates if the import was completely successful. When set to `false`, some objects may have been copied. For additional + information, refer to the `errors` and `successResults` properties. `successCount`:: - Indicates the number of successfully resolved records. + (number) Indicates the number of successfully resolved records. `errors`:: - (array) Specifies the objects that failed to resolve. + (Optional, array) Specifies the objects that failed to resolve. + +`successResults`:: + (Optional, array) Indicates the objects that were imported successfully, with any metadata if applicable. ++ +NOTE: No objects are actually created until all resolvable errors have been addressed! This includes conflict errors and missing references +errors. See the examples below for how to resolve these errors. [[saved-objects-api-resolve-import-errors-codes]] ==== Response code @@ -59,11 +89,16 @@ The request body must include the multipart/form-data type. [[saved-objects-api-resolve-import-errors-example]] ==== Examples -Retry a dashboard import: +[[saved-objects-api-resolve-import-errors-example-1]] +===== 1. Resolve conflict errors + +This example builds upon the <>. + +Resolve conflict errors for an index pattern, visualization, and canvas by overwriting the existing saved objects: [source,sh] -------------------------------------------------- -$ curl -X POST "localhost:5601/api/saved_objects/_resolve_import_errors" -H "kbn-xsrf: true" --form file=@file.ndjson --form retries='[{"type":"dashboard","id":"my-dashboard"}]' +$ curl -X POST "localhost:5601/api/saved_objects/_resolve_import_errors" -H "kbn-xsrf: true" --form file=@file.ndjson --form retries='[{"type":"index-pattern","id":"my-pattern","overwrite":true},{"type":"visualization","id":"my-vis","overwrite":true,"destinationId":"another-vis"},{"type":"canvas","id":"my-canvas","overwrite":true,"destinationId":"yet-another-canvas"},{"type":"dashboard","id":"my-dashboard"}]' -------------------------------------------------- // KIBANA @@ -71,6 +106,9 @@ The `file.ndjson` file contains the following: [source,sh] -------------------------------------------------- +{"type":"index-pattern","id":"my-pattern","attributes":{"title":"my-pattern-*"}} +{"type":"visualization","id":"my-vis","attributes":{"title":"Look at my visualization"}} +{"type":"canvas-workpad","id":"my-canvas","attributes":{"name":"Look at my canvas"}} {"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"}} -------------------------------------------------- @@ -80,41 +118,45 @@ The API returns the following: -------------------------------------------------- { "success": true, - "successCount": 1 + "successCount": 4, + "successResults": [ + { + "id": "my-pattern", + "type": "index-pattern" + }, + { + "id": "my-vis", + "type": "visualization", + "destinationId": "another-vis" + }, + { + "id": "my-canvas", + "type": "canvas-workpad", + "destinationId": "yet-another-canvas" + }, + { + "id": "my-dashboard", + "type": "dashboard" + } + ] } -------------------------------------------------- -Resolve errors for a dashboard and overwrite the existing saved object: +This result indicates that the import was successful, and all four objects were created. -[source,sh] --------------------------------------------------- -$ curl -X POST "localhost:5601/api/saved_objects/_resolve_import_errors" -H "kbn-xsrf: true" --form file=@file.ndjson --form retries='[{"type":"dashboard","id":"my-dashboard","overwrite":true}]' --------------------------------------------------- -// KIBANA +TIP: If a prior import attempt resulted in resolvable errors, you must include a retry for each object you want to import, including any +that were returned in the `successResults` array. In this example, we retried importing the dashboard accordingly. -The `file.ndjson` file contains the following: +[[saved-objects-api-resolve-import-errors-example-2]] +===== 2. Resolve missing reference errors -[source,sh] --------------------------------------------------- -{"type":"index-pattern","id":"my-pattern","attributes":{"title":"my-pattern-*"}} -{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"}} --------------------------------------------------- +This example builds upon the <>. -The API returns the following: +Resolve a missing reference error for a visualization by replacing the index pattern with another: [source,sh] -------------------------------------------------- -{ - "success": true, - "successCount": 1 -} --------------------------------------------------- - -Resolve errors for a visualization by replacing the index pattern with another: - -[source,sh] --------------------------------------------------- -$ curl -X POST "localhost:5601/api/saved_objects/_resolve_import_errors" -H "kbn-xsrf: true" --form file=@file.ndjson --form retries='[{"type":"visualization","id":"my-vis","replaceReferences":[{"type":"index-pattern","from":"missing","to":"existing"}]}]' +$ curl -X POST "localhost:5601/api/saved_objects/_resolve_import_errors" -H "kbn-xsrf: true" --form file=@file.ndjson --form retries='[{"type":"visualization","id":"my-vis","replaceReferences":[{"type":"index-pattern","from":"my-pattern-*","to":"existing-pattern"}]},{"type":"dashboard","id":"my-dashboard"}]' -------------------------------------------------- // KIBANA @@ -122,7 +164,8 @@ The `file.ndjson` file contains the following: [source,sh] -------------------------------------------------- -{"type":"visualization","id":"my-vis","attributes":{"title":"Look at my visualization"},"references":[{"name":"ref_0","type":"index-pattern","id":"missing"}]} +{"type":"visualization","id":"my-vis","attributes":{"title":"Look at my visualization"},"references":[{"name":"ref_0","type":"index-pattern","id":"my-pattern-*"}]} +{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"},"references":[{"name":"ref_0","type":"visualization","id":"my-vis"}]} -------------------------------------------------- The API returns the following: @@ -131,6 +174,21 @@ The API returns the following: -------------------------------------------------- { "success": true, - "successCount": 1 + "successCount": 2, + "successResults": [ + { + "id": "my-pattern", + "type": "index-pattern" + }, + { + "id": "my-dashboard", + "type": "dashboard" + } + ] } -------------------------------------------------- + +This result indicates that the import was successful, and both objects were created. + +TIP: If a prior import attempt resulted in resolvable errors, you must include a retry for each object you want to import, including any +that were described in the missing error object's `blocked` array. In this example, we retried importing the dashboard accordingly. diff --git a/docs/api/spaces-management/copy_saved_objects.asciidoc b/docs/api/spaces-management/copy_saved_objects.asciidoc index 4822e7f624302..acd60538e9596 100644 --- a/docs/api/spaces-management/copy_saved_objects.asciidoc +++ b/docs/api/spaces-management/copy_saved_objects.asciidoc @@ -24,7 +24,8 @@ You can request to overwrite any objects that already exist in the target space ==== {api-path-parms-title} `space_id`:: -(Optional, string) The ID of the space that contains the saved objects you want to copy. When `space_id` is unspecified in the URL, the default space is used. + (Optional, string) The ID of the space that contains the saved objects you want to copy. When `space_id` is unspecified in the URL, the + default space is used. [role="child_attributes"] [[spaces-api-copy-saved-objects-request-body]] @@ -47,10 +48,12 @@ You can request to overwrite any objects that already exist in the target space ===== `includeReferences`:: - (Optional, boolean) When set to `true`, all saved objects related to the specified saved objects will also be copied into the target spaces. The default value is `false`. + (Optional, boolean) When set to `true`, all saved objects related to the specified saved objects will also be copied into the target + spaces. The default value is `false`. `overwrite`:: - (Optional, boolean) When set to `true`, all conflicts are automatically overidden. When a saved object with a matching `type` and `id` exists in the target space, that version is replaced with the version from the source space. The default value is `false`. + (Optional, boolean) When set to `true`, all conflicts are automatically overidden. When a saved object with a matching `type` and `id` + exists in the target space, that version is replaced with the version from the source space. The default value is `false`. [role="child_attributes"] [[spaces-api-copy-saved-objects-response-body]] @@ -63,7 +66,8 @@ You can request to overwrite any objects that already exist in the target space [%collapsible%open] ===== `success`::: - (boolean) The copy operation was successful. When set to `false`, some objects may have been copied. For additional information, refer to the `successCount` and `errors` properties. + (boolean) The copy operation was successful. When set to `false`, some objects may have been copied. For additional information, refer + to the `errors` and `successResults` properties. `successCount`::: (number) The number of objects that successfully copied. @@ -84,15 +88,33 @@ You can request to overwrite any objects that already exist in the target space .Properties of `error` [%collapsible%open] ======= - `type`::::: - (string) The type of error. For example, `unsupported_type`, `missing_references`, or `unknown`. Errors marked as `conflict` may be resolved by using the <>. + `type`:::: + (string) The type of error. For example, `conflict`, `ambiguous_conflict`, `missing_references`, `unsupported_type`, or `unknown`. + Errors marked as `conflict` or `ambiguous_conflict` may be resolved by using the <>. + `destinationId`:::: + (Optional, string) The destination ID that was used during the copy attempt. This is only present on `conflict` error types. + `destinations`:::: + (Optional, array) A list of possible object destinations with `id`, `title`, and `updatedAt` fields to describe each one. This is + only present on `ambiguous_conflict` error types. ======= ====== + + `successResults`::: + (Optional, array) Indicates the objects that were copied successfully, with any metadata if applicable. ++ +NOTE: No objects are actually created until all resolvable errors have been addressed! This includes conflict errors. See the examples below +for how to resolve these errors. + ===== [[spaces-api-copy-saved-objects-example]] ==== {api-examples-title} -Copy a dashboard with the `my-dashboard` ID, including all references from the `default` space to the `marketing` and `sales` spaces: +[[spaces-api-copy-saved-objects-example-1]] +===== 1. Successful copy (with `createNewCopies` enabled) + +Copy a dashboard with the `my-dashboard` ID, including all references from the `default` space to the `marketing` space. In this example, +the dashboard has a reference to a visualization, and that has a reference to an index pattern: [source,sh] ---- @@ -102,7 +124,60 @@ $ curl -X POST "localhost:5601/api/spaces/_copy_saved_objects" "type": "dashboard", "id": "my-dashboard" }], - "spaces": ["marketing", "sales"], + "spaces": ["marketing"], + "includeReferences": true, + "createNewcopies": true +} +---- +// KIBANA + +The API returns the following: + +[source,sh] +---- +{ + "marketing": { + "success": true, + "successCount": 3, + "successResults": [ + { + "id": "my-dashboard", + "type": "dashboard", + "destinationId": "1e127098-5b80-417f-b0f1-c60c8395358f" + }, + { + "id": "my-vis", + "type": "visualization", + "destinationId": "a610ed80-1c73-4507-9e13-d3af736c8e04" + }, + { + "id": "my-index-pattern", + "type": "index-pattern", + "destinationId": "bc3c9c70-bf6f-4bec-b4ce-f4189aa9e26b" + } + ] + } +} +---- + +This result indicates that the copy was successful, and all three objects were created. Since these objects were created as new copies, each +entry in the `successResults` array includes a `destinationId` attribute. + +[[spaces-api-copy-saved-objects-example-2]] +===== 2. Successful copy (with `createNewCopies` disabled) + +Copy a dashboard with the `my-dashboard` ID, including all references from the `default` space to the `marketing` space. In this example, +the dashboard has a reference to a visualization, and that has a reference to an index pattern: + +[source,sh] +---- +$ curl -X POST "localhost:5601/api/spaces/_copy_saved_objects" +{ + "objects": [{ + "type": "dashboard", + "id": "my-dashboard" + }], + "spaces": ["marketing"], "includeReferences": true } ---- @@ -115,35 +190,43 @@ The API returns the following: { "marketing": { "success": true, - "successCount": 5 - }, - "sales": { - "success": false, - "successCount": 4, - "errors": [{ - "id": "my-index-pattern", - "type": "index-pattern", - "error": { - "type": "conflict" + "successCount": 3, + "successResults": [ + { + "id": "my-dashboard", + "type": "dashboard" + }, + { + "id": "my-vis", + "type": "visualization" + }, + { + "id": "my-index-pattern", + "type": "index-pattern" } - }] + ] } } ---- -The `marketing` space succeeds, but the `sales` space fails due to a conflict in the index pattern. +This result indicates that the import was successful, and all three objects were created. + +[[spaces-api-copy-saved-objects-example-3]] +===== 3. Failed copy (with conflict errors) -Copy a visualization with the `my-viz` ID from the `marketing` space to the `default` space: +Copy a dashboard with the `my-dashboard` ID, including all references from the `default` space to the `marketing` and `sales` spaces. In +this example, the dashboard has a reference to a visualization and a canvas, and the visualization has a reference to an index pattern: [source,sh] ---- -$ curl -X POST "localhost:5601/s/marketing/api/spaces/_copy_saved_objects" +$ curl -X POST "localhost:5601/api/spaces/_copy_saved_objects" { "objects": [{ - "type": "visualization", - "id": "my-viz" + "type": "dashboard", + "id": "my-dashboard" }], - "spaces": ["default"] + "spaces": ["marketing", "sales"], + "includeReferences": true } ---- // KIBANA @@ -153,9 +236,95 @@ The API returns the following: [source,sh] ---- { - "default": { + "marketing": { "success": true, - "successCount": 1 + "successCount": 4, + "successResults": [ + { + "id": "other-dashboard", + "type": "dashboard" + }, + { + "id": "my-vis", + "type": "visualization" + }, + { + "id": "my-canvas", + "type": "canvas-workpad" + }, + { + "id": "my-index-pattern", + "type": "index-pattern" + } + ] + }, + "sales": { + "success": false, + "successCount": 1, + "errors": [ + { + "id": "my-pattern", + "type": "index-pattern", + "title": "my-pattern-*", + "error": { + "type": "conflict" + } + }, + { + "id": "my-visualization", + "type": "my-vis", + "title": "Look at my visualization", + "error": { + "type": "conflict", + "destinationId": "another-vis" + } + }, + { + "id": "my-canvas", + "type": "canvas-workpad", + "title": "Look at my canvas", + "error": { + "type": "ambiguous_conflict", + "destinations": [ + { + "id": "another-canvas", + "title": "Look at another canvas", + "updatedAt": "2020-07-08T16:36:32.377Z" + }, + { + "id": "yet-another-canvas", + "title": "Look at yet another canvas", + "updatedAt": "2020-07-05T12:29:54.849Z" + } + ] + } + } + ], + "successResults": [ + { + "id": "my-dashboard", + "type": "dashboard" + } + ] } } ---- + +This result indicates that the copy was successful for the `marketing` space, but it was not successful for the `sales` space because the +index pattern, visualization, and dashboard each resulted in a conflict error: + +* An index pattern with the same ID already exists, so this resulted in a conflict error. This can be resolved by overwriting the existing +object, or skipping this object entirely. + +* A visualization with a different ID but the same "origin" already exists, so this resulted in a conflict error as well. The +`destinationId` field describes to the other visualization which caused this conflict. This behavior was added to ensure that new objects +which can be shared between spaces behave in a similar way as legacy non-shareable objects. When a shareable object is copied into a new +space, it retains its origin so that it conflicts will be encountered as expected. This can be resolved by overwriting the specified +destination object, or skipping this object entirely. + +* Two canvases with different IDs but the same "origin" already exist, so this resulted in an ambiguous conflict error. The `destinations` +array describes to the other canvases which caused this conflict. When a shareable object is copied into a new space, and is _then_ shared +to another space where an object of the same origin exists, this situation may occur. This can be resolved by picking one of the destination +objects to overwrite, or skipping this object entirely. + +These errors need to be addressed using the <>. diff --git a/docs/api/spaces-management/resolve_copy_saved_objects_conflicts.asciidoc b/docs/api/spaces-management/resolve_copy_saved_objects_conflicts.asciidoc index 565d12513815b..04575eea85d85 100644 --- a/docs/api/spaces-management/resolve_copy_saved_objects_conflicts.asciidoc +++ b/docs/api/spaces-management/resolve_copy_saved_objects_conflicts.asciidoc @@ -46,7 +46,8 @@ Execute the <>, w (Optional, boolean) When set to `true`, all saved objects related to the specified saved objects are copied into the target spaces. The `includeReferences` must be the same values used during the failed <> operation. The default value is `false`. `retries`:: - (Required, object) The retry operations to attempt. Object keys represent the target space IDs. + (Required, object) The retry operations to attempt, which can specify how to resolve different types of errors. Object keys represent the + target space IDs. + .Properties of `retries` [%collapsible%open] @@ -64,6 +65,8 @@ Execute the <>, w (Required, string) The saved object ID. `overwrite`:::: (Required, boolean) When set to `true`, the saved object from the source space (desigated by the <>) overwrites the conflicting object in the destination space. When set to `false`, this does nothing. + `destinationId`:::: + (Optional, string) Specifies which destination ID the copied object should have (if different from the current ID). ====== ===== @@ -104,15 +107,32 @@ Execute the <>, w [%collapsible%open] ======= `type`:::: - (string) The type of error. For example, `unsupported_type`, `missing_references`, or `unknown`. + (string) The type of error. For example, `conflict`, `ambiguous_conflict`, `missing_references`, `unsupported_type`, or `unknown`. + `destinationId`:::: + (Optional, string) The destination ID that was used during the copy attempt. This is only present on `conflict` errors types. + `destinations`:::: + (Optional, array) A list of possible object destinations with `id`, `title`, and `updatedAt` fields to describe each one. This is + only present on `ambiguous_conflict` error types. ======= ====== + +`successResults`::: + (Optional, array) Indicates the objects that were copied successfully, with any metadata if applicable. ++ +NOTE: No objects are actually created until all resolvable errors have been addressed! This includes conflict errors. See the examples below +for how to resolve these errors. + ===== [[spaces-api-resolve-copy-saved-objects-conflicts-example]] ==== {api-examples-title} -Overwrite an index pattern in the `marketing` space, and a visualization in the `sales` space: +[[spaces-api-resolve-copy-saved-objects-conflicts-example-1]] +===== 1. Resolve conflict errors + +This example builds upon the <>. + +Resolve conflict errors for an index pattern, visualization, and canvas by overwriting the existing saved objects: [source,sh] ---- @@ -124,16 +144,29 @@ $ curl -X POST "localhost:5601/api/spaces/_resolve_copy_saved_objects_errors" }], "includeReferences": true, "retries": { - "marketing": [{ - "type": "index-pattern", - "id": "my-pattern", - "overwrite": true - }], - "sales": [{ - "type": "visualization", - "id": "my-viz", - "overwrite": true - }] + "sales": [ + { + "type": "index-pattern", + "id": "my-pattern", + "overwrite": true + }, + { + "type": "visualization", + "id": "my-vis", + "overwrite": true, + "destinationId": "another-vis" + }, + { + "type": "canvas", + "id": "my-canvas", + "overwrite": true, + "destinationId": "yet-another-canvas" + }, + { + "type": "dashboard", + "id": "my-dashboard" + } + ] } } ---- @@ -144,13 +177,34 @@ The API returns the following: [source,sh] ---- { - "marketing": { - "success": true, - "successCount": 1 - }, "sales": { "success": true, - "successCount": 1 + "successCount": 4, + "successResults": [ + { + "id": "my-pattern", + "type": "index-pattern" + }, + { + "id": "my-vis", + "type": "visualization", + "destinationId": "another-vis" + }, + { + "id": "my-canvas", + "type": "canvas-workpad", + "destinationId": "yet-another-canvas" + }, + { + "id": "my-dashboard", + "type": "dashboard" + } + ] } } ---- + +This result indicates that the copy was successful, and all four objects were created. + +TIP: If a prior copy attempt resulted in resolvable errors, you must include a retry for each object you want to copy, including any that +were returned in the `successResults` array. In this example, we retried copying the dashboard accordingly. From dd25fbc884badf31490bf039416f323845a07a0c Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 13 Jul 2020 05:38:30 -0400 Subject: [PATCH 44/55] Add inline links from Import docs to Spaces docs --- docs/api/saved-objects/import.asciidoc | 8 ++++---- docs/api/saved-objects/resolve_import_errors.asciidoc | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/api/saved-objects/import.asciidoc b/docs/api/saved-objects/import.asciidoc index 16320af32493c..855f36712be04 100644 --- a/docs/api/saved-objects/import.asciidoc +++ b/docs/api/saved-objects/import.asciidoc @@ -17,7 +17,7 @@ experimental[] Create sets of {kib} saved objects from a file created by the exp ==== Path parameters `space_id`:: - (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + (Optional, string) An identifier for the <>. If `space_id` is not provided in the URL, the default space is used. [[saved-objects-api-import-query-params]] ==== Query parameters @@ -238,9 +238,9 @@ object, or skipping this object entirely. * A visualization with a different ID but the same "origin" already exists, so this resulted in a conflict error as well. The `destinationId` field describes to the other visualization which caused this conflict. This behavior was added to ensure that new objects -which can be shared between spaces behave in a similar way as legacy non-shareable objects. When a shareable object is exported and then -imported into a new space, it retains its origin so that it conflicts will be encountered as expected. This can be resolved by overwriting -the specified destination object, or skipping this object entirely. +which can be shared between <> behave in a similar way as legacy non-shareable objects. When a shareable object is +exported and then imported into a new space, it retains its origin so that it conflicts will be encountered as expected. This can be +resolved by overwriting the specified destination object, or skipping this object entirely. * Two canvases with different IDs but the same "origin" already exist, so this resulted in an ambiguous conflict error. The `destinations` array describes to the other canvases which caused this conflict. When a shareable object is exported and then imported into a new space, diff --git a/docs/api/saved-objects/resolve_import_errors.asciidoc b/docs/api/saved-objects/resolve_import_errors.asciidoc index 6ec8eb8d6f777..f5b8835f448b7 100644 --- a/docs/api/saved-objects/resolve_import_errors.asciidoc +++ b/docs/api/saved-objects/resolve_import_errors.asciidoc @@ -25,7 +25,7 @@ To resolve errors, you can: ==== Path parameters `space_id`:: - (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + (Optional, string) An identifier for the <>. If `space_id` is not provided in the URL, the default space is used. [[saved-objects-api-resolve-import-errors-query-params]] ==== Query parameters From ba4778032b34603634eaf024d2048e59dd379e3f Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 13 Jul 2020 18:31:34 -0400 Subject: [PATCH 45/55] Fix jest test that failed due to recent merge --- .../server/saved_objects/migrations/core/index_migrator.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts index 0240196825e76..691ad8dfa84f4 100644 --- a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts @@ -195,6 +195,7 @@ describe('IndexMigrator', () => { migrationVersion: { dynamic: 'true', type: 'object' }, namespace: { type: 'keyword' }, namespaces: { type: 'keyword' }, + originId: { type: 'keyword' }, type: { type: 'keyword' }, updated_at: { type: 'date' }, references: { @@ -243,6 +244,7 @@ describe('IndexMigrator', () => { migrationVersion: '4a1746014a75ade3a714e1db5763276f', namespace: '2f4316de49999235636386fe51dc06c1', namespaces: '2f4316de49999235636386fe51dc06c1', + originId: '2f4316de49999235636386fe51dc06c1', references: '7997cf5a56cc02bdc9c93361bde732b0', type: '2f4316de49999235636386fe51dc06c1', updated_at: '00da57df13e94e9d98437d13ace4bfe0', From 6ebf12a7aff78e452d8d5422624146cd4f42bf79 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 14 Jul 2020 11:29:14 -0400 Subject: [PATCH 46/55] Fix functional test failures after merge --- .../import/check_origin_conflicts.ts | 3 +- .../common/suites/find.ts | 34 +++++++++++++++---- .../common/suites/import.ts | 2 +- .../security_and_spaces/apis/import.ts | 8 ++--- .../security_only/apis/import.ts | 8 ++--- 5 files changed, 35 insertions(+), 20 deletions(-) diff --git a/src/core/server/saved_objects/import/check_origin_conflicts.ts b/src/core/server/saved_objects/import/check_origin_conflicts.ts index 6c8a8a062d2eb..578fb120f59c0 100644 --- a/src/core/server/saved_objects/import/check_origin_conflicts.ts +++ b/src/core/server/saved_objects/import/check_origin_conflicts.ts @@ -24,6 +24,7 @@ import { SavedObjectsClientContract, SavedObjectsImportError, SavedObjectsImportRetry, + SavedObjectsFindOptions, } from '../types'; import { ISavedObjectTypeRegistry } from '..'; @@ -105,7 +106,7 @@ const checkOriginConflict = async ( page: 1, perPage: 10, fields: ['title'], - namespace, + ...(namespace && { namespaces: [namespace] }), }; const findResult = await savedObjectsClient.find<{ title?: string }>(findOptions); const { total, saved_objects: savedObjects } = findResult; diff --git a/x-pack/test/saved_object_api_integration/common/suites/find.ts b/x-pack/test/saved_object_api_integration/common/suites/find.ts index 3f0c988912544..bab4a4d88534a 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/find.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/find.ts @@ -44,12 +44,34 @@ export interface FindTestCase { } // additional sharedtype objects that exist but do not have common test cases defined -const CID = 'conflict_'; -const CONFLICT_1_OBJ = Object.freeze({ type: 'sharedtype', id: `${CID}1` }); -const CONFLICT_2A_OBJ = Object.freeze({ type: 'sharedtype', id: `${CID}2a`, originId: `${CID}2` }); -const CONFLICT_2B_OBJ = Object.freeze({ type: 'sharedtype', id: `${CID}2b`, originId: `${CID}2` }); -const CONFLICT_3_OBJ = Object.freeze({ type: 'sharedtype', id: `${CID}3` }); -const CONFLICT_4A_OBJ = Object.freeze({ type: 'sharedtype', id: `${CID}4a`, originId: `${CID}4` }); +const CONFLICT_1_OBJ = Object.freeze({ + type: 'sharedtype', + id: 'conflict_1', + namespaces: ['default', 'space_1', 'space_2'], +}); +const CONFLICT_2A_OBJ = Object.freeze({ + type: 'sharedtype', + id: 'conflict_2a', + originId: 'conflict_2', + namespaces: ['default', 'space_1', 'space_2'], +}); +const CONFLICT_2B_OBJ = Object.freeze({ + type: 'sharedtype', + id: 'conflict_2b', + originId: 'conflict_2', + namespaces: ['default', 'space_1', 'space_2'], +}); +const CONFLICT_3_OBJ = Object.freeze({ + type: 'sharedtype', + id: 'conflict_3', + namespaces: ['default', 'space_1', 'space_2'], +}); +const CONFLICT_4A_OBJ = Object.freeze({ + type: 'sharedtype', + id: 'conflict_4a', + originId: 'conflict_4', + namespaces: ['default', 'space_1', 'space_2'], +}); const TEST_CASES = [ { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, namespaces: ['default'] }, diff --git a/x-pack/test/saved_object_api_integration/common/suites/import.ts b/x-pack/test/saved_object_api_integration/common/suites/import.ts index 3f3b5e187e919..5036d7b200881 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/import.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/import.ts @@ -83,7 +83,7 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe const testCaseArray = Array.isArray(testCases) ? testCases : [testCases]; if (statusCode === 403) { const types = testCaseArray.map((x) => x.type); - await expectResponses.forbidden('bulk_create')(types)(response); + await expectForbidden(types)(response); } else { // permitted const { success, successCount, successResults, errors } = response.body; diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts index 9f7226e5d44b5..0b531a3dccc1a 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts @@ -117,7 +117,7 @@ export default function ({ getService }: FtrProviderContext) { createNewCopies, spaceId, singleRequest, - responseBodyOverride: expectForbidden('bulk_create')([ + responseBodyOverride: expectForbidden([ 'dashboard', 'globaltype', 'isolatedtype', @@ -145,11 +145,7 @@ export default function ({ getService }: FtrProviderContext) { overwrite, spaceId, singleRequest, - responseBodyOverride: expectForbidden('bulk_create')([ - 'dashboard', - 'globaltype', - 'isolatedtype', - ]), + responseBodyOverride: expectForbidden(['dashboard', 'globaltype', 'isolatedtype']), }), createTestDefinitions(group2, true, { overwrite, spaceId, singleRequest }), createTestDefinitions(group3, true, { overwrite, spaceId, singleRequest }), diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/import.ts b/x-pack/test/saved_object_api_integration/security_only/apis/import.ts index 62c5dfbb664d7..34be3b7408432 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/import.ts @@ -96,7 +96,7 @@ export default function ({ getService }: FtrProviderContext) { createTestDefinitions(all, true, { createNewCopies, singleRequest, - responseBodyOverride: expectForbidden('bulk_create')([ + responseBodyOverride: expectForbidden([ 'dashboard', 'globaltype', 'isolatedtype', @@ -123,11 +123,7 @@ export default function ({ getService }: FtrProviderContext) { createTestDefinitions(group1All, true, { overwrite, singleRequest, - responseBodyOverride: expectForbidden('bulk_create')([ - 'dashboard', - 'globaltype', - 'isolatedtype', - ]), + responseBodyOverride: expectForbidden(['dashboard', 'globaltype', 'isolatedtype']), }), createTestDefinitions(group2, true, { overwrite, singleRequest }), createTestDefinitions(group3, true, { overwrite, singleRequest }), From 48feb3e10cb5a8908eb5b9d7ec97080ec5d06b70 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 14 Jul 2020 13:51:11 -0400 Subject: [PATCH 47/55] Fix type check Oversight on the last commit... --- src/core/server/saved_objects/import/check_origin_conflicts.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/server/saved_objects/import/check_origin_conflicts.ts b/src/core/server/saved_objects/import/check_origin_conflicts.ts index 578fb120f59c0..2d3ca73f5b270 100644 --- a/src/core/server/saved_objects/import/check_origin_conflicts.ts +++ b/src/core/server/saved_objects/import/check_origin_conflicts.ts @@ -24,7 +24,6 @@ import { SavedObjectsClientContract, SavedObjectsImportError, SavedObjectsImportRetry, - SavedObjectsFindOptions, } from '../types'; import { ISavedObjectTypeRegistry } from '..'; From 8ec0e37a8b2e536b1424d04b26caf2c156baa3d4 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 14 Jul 2020 15:46:39 -0400 Subject: [PATCH 48/55] =?UTF-8?q?Fix=20unit=20test...=20(=E2=95=AF=C2=B0?= =?UTF-8?q?=E2=96=A1=C2=B0=EF=BC=89=E2=95=AF=EF=B8=B5=20=E2=94=BB=E2=94=81?= =?UTF-8?q?=E2=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/saved_objects/import/check_origin_conflicts.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/server/saved_objects/import/check_origin_conflicts.test.ts b/src/core/server/saved_objects/import/check_origin_conflicts.test.ts index 92d83e675cf6c..45925687f36fa 100644 --- a/src/core/server/saved_objects/import/check_origin_conflicts.test.ts +++ b/src/core/server/saved_objects/import/check_origin_conflicts.test.ts @@ -132,7 +132,7 @@ describe('#checkOriginConflicts', () => { await checkOriginConflicts(objects, options); expect(find).toHaveBeenCalledTimes(1); - expect(find).toHaveBeenCalledWith(expect.objectContaining({ namespace })); + expect(find).toHaveBeenCalledWith(expect.objectContaining({ namespaces: [namespace] })); }); test('search query escapes quote and backslash characters in `id` and/or `originId`', async () => { From 330355957015ff021bd1fbcb19e670bb1a542e74 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Wed, 15 Jul 2020 13:15:04 -0400 Subject: [PATCH 49/55] Rename `rawSearchFields` to `rootSearchFields` --- ...gin-core-public.savedobjectsfindoptions.md | 2 +- ...savedobjectsfindoptions.rawsearchfields.md | 13 --------- ...avedobjectsfindoptions.rootsearchfields.md | 13 +++++++++ .../core/server/kibana-plugin-core-server.md | 2 +- ...gin-core-server.savedobjectsfindoptions.md | 2 +- ...savedobjectsfindoptions.rawsearchfields.md | 13 --------- ...avedobjectsfindoptions.rootsearchfields.md | 13 +++++++++ ...core-server.savedobjectsrepository.find.md | 4 +-- ...ugin-core-server.savedobjectsrepository.md | 2 +- src/core/public/public.api.md | 2 +- .../saved_objects/saved_objects_client.ts | 2 +- .../import/check_origin_conflicts.test.ts | 2 +- .../import/check_origin_conflicts.ts | 2 +- .../saved_objects/service/lib/repository.ts | 4 +-- .../lib/search_dsl/query_params.test.ts | 28 +++++++++---------- .../service/lib/search_dsl/query_params.ts | 16 +++++------ .../service/lib/search_dsl/search_dsl.test.ts | 6 ++-- .../service/lib/search_dsl/search_dsl.ts | 6 ++-- src/core/server/saved_objects/types.ts | 8 ++++-- src/core/server/server.api.md | 4 +-- 20 files changed, 73 insertions(+), 71 deletions(-) delete mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.rawsearchfields.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.rootsearchfields.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.rawsearchfields.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.rootsearchfields.md diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md index ddfc21df09891..ebd0a99531755 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md @@ -23,7 +23,7 @@ export interface SavedObjectsFindOptions | [page](./kibana-plugin-core-public.savedobjectsfindoptions.page.md) | number | | | [perPage](./kibana-plugin-core-public.savedobjectsfindoptions.perpage.md) | number | | | [preference](./kibana-plugin-core-public.savedobjectsfindoptions.preference.md) | string | An optional ES preference value to be used for the query \* | -| [rawSearchFields](./kibana-plugin-core-public.savedobjectsfindoptions.rawsearchfields.md) | string[] | The fields to perform the parsed query against. Unlike the searchFields argument, these are expected to be raw and will not be modified. If used in conjunction with searchFields, both are concatenated together. | +| [rootSearchFields](./kibana-plugin-core-public.savedobjectsfindoptions.rootsearchfields.md) | string[] | The fields to perform the parsed query against. Unlike the searchFields argument, these are expected to be root fields and will not be modified. If used in conjunction with searchFields, both are concatenated together. | | [search](./kibana-plugin-core-public.savedobjectsfindoptions.search.md) | string | Search documents using the Elasticsearch Simple Query String syntax. See Elasticsearch Simple Query String query argument for more information | | [searchFields](./kibana-plugin-core-public.savedobjectsfindoptions.searchfields.md) | string[] | The fields to perform the parsed query against. See Elasticsearch Simple Query String fields argument for more information | | [sortField](./kibana-plugin-core-public.savedobjectsfindoptions.sortfield.md) | string | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.rawsearchfields.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.rawsearchfields.md deleted file mode 100644 index b5861402a08ce..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.rawsearchfields.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsFindOptions](./kibana-plugin-core-public.savedobjectsfindoptions.md) > [rawSearchFields](./kibana-plugin-core-public.savedobjectsfindoptions.rawsearchfields.md) - -## SavedObjectsFindOptions.rawSearchFields property - -The fields to perform the parsed query against. Unlike the `searchFields` argument, these are expected to be raw and will not be modified. If used in conjunction with `searchFields`, both are concatenated together. - -Signature: - -```typescript -rawSearchFields?: string[]; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.rootsearchfields.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.rootsearchfields.md new file mode 100644 index 0000000000000..faa971509eca2 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.rootsearchfields.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsFindOptions](./kibana-plugin-core-public.savedobjectsfindoptions.md) > [rootSearchFields](./kibana-plugin-core-public.savedobjectsfindoptions.rootsearchfields.md) + +## SavedObjectsFindOptions.rootSearchFields property + +The fields to perform the parsed query against. Unlike the `searchFields` argument, these are expected to be root fields and will not be modified. If used in conjunction with `searchFields`, both are concatenated together. + +Signature: + +```typescript +rootSearchFields?: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index c1f442f66fbd8..3a193708e132f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -122,7 +122,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [OnPreAuthToolkit](./kibana-plugin-core-server.onpreauthtoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. | | [OnPreResponseExtensions](./kibana-plugin-core-server.onpreresponseextensions.md) | Additional data to extend a response. | | [OnPreResponseInfo](./kibana-plugin-core-server.onpreresponseinfo.md) | Response status code. | -| [OnPreResponseToolkit](./kibana-plugin-core-server.onpreresponsetoolkit.md) | A tool set defining an outcome of OnPreRouting interceptor for incoming request. | +| [OnPreResponseToolkit](./kibana-plugin-core-server.onpreresponsetoolkit.md) | A tool set defining an outcome of OnPreResponse interceptor for incoming request. | | [OnPreRoutingToolkit](./kibana-plugin-core-server.onpreroutingtoolkit.md) | A tool set defining an outcome of OnPreRouting interceptor for incoming request. | | [OpsMetrics](./kibana-plugin-core-server.opsmetrics.md) | Regroups metrics gathered by all the collectors. This contains metrics about the os/runtime, the kibana process and the http server. | | [OpsOsMetrics](./kibana-plugin-core-server.opsosmetrics.md) | OS related metrics | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md index 94a6d93f3f70b..15a9d99b3d062 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md @@ -23,7 +23,7 @@ export interface SavedObjectsFindOptions | [page](./kibana-plugin-core-server.savedobjectsfindoptions.page.md) | number | | | [perPage](./kibana-plugin-core-server.savedobjectsfindoptions.perpage.md) | number | | | [preference](./kibana-plugin-core-server.savedobjectsfindoptions.preference.md) | string | An optional ES preference value to be used for the query \* | -| [rawSearchFields](./kibana-plugin-core-server.savedobjectsfindoptions.rawsearchfields.md) | string[] | The fields to perform the parsed query against. Unlike the searchFields argument, these are expected to be raw and will not be modified. If used in conjunction with searchFields, both are concatenated together. | +| [rootSearchFields](./kibana-plugin-core-server.savedobjectsfindoptions.rootsearchfields.md) | string[] | The fields to perform the parsed query against. Unlike the searchFields argument, these are expected to be root fields and will not be modified. If used in conjunction with searchFields, both are concatenated together. | | [search](./kibana-plugin-core-server.savedobjectsfindoptions.search.md) | string | Search documents using the Elasticsearch Simple Query String syntax. See Elasticsearch Simple Query String query argument for more information | | [searchFields](./kibana-plugin-core-server.savedobjectsfindoptions.searchfields.md) | string[] | The fields to perform the parsed query against. See Elasticsearch Simple Query String fields argument for more information | | [sortField](./kibana-plugin-core-server.savedobjectsfindoptions.sortfield.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.rawsearchfields.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.rawsearchfields.md deleted file mode 100644 index 11626ac040595..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.rawsearchfields.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsFindOptions](./kibana-plugin-core-server.savedobjectsfindoptions.md) > [rawSearchFields](./kibana-plugin-core-server.savedobjectsfindoptions.rawsearchfields.md) - -## SavedObjectsFindOptions.rawSearchFields property - -The fields to perform the parsed query against. Unlike the `searchFields` argument, these are expected to be raw and will not be modified. If used in conjunction with `searchFields`, both are concatenated together. - -Signature: - -```typescript -rawSearchFields?: string[]; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.rootsearchfields.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.rootsearchfields.md new file mode 100644 index 0000000000000..204342c45f64e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.rootsearchfields.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsFindOptions](./kibana-plugin-core-server.savedobjectsfindoptions.md) > [rootSearchFields](./kibana-plugin-core-server.savedobjectsfindoptions.rootsearchfields.md) + +## SavedObjectsFindOptions.rootSearchFields property + +The fields to perform the parsed query against. Unlike the `searchFields` argument, these are expected to be root fields and will not be modified. If used in conjunction with `searchFields`, both are concatenated together. + +Signature: + +```typescript +rootSearchFields?: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.find.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.find.md index ebf4320c83bba..1b562263145da 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.find.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.find.md @@ -7,14 +7,14 @@ Signature: ```typescript -find({ search, defaultSearchOperator, searchFields, rawSearchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespaces, type, filter, preference, }: SavedObjectsFindOptions): Promise>; +find({ search, defaultSearchOperator, searchFields, rootSearchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespaces, type, filter, preference, }: SavedObjectsFindOptions): Promise>; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| { search, defaultSearchOperator, searchFields, rawSearchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespaces, type, filter, preference, } | SavedObjectsFindOptions | | +| { search, defaultSearchOperator, searchFields, rootSearchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespaces, type, filter, preference, } | SavedObjectsFindOptions | | Returns: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md index 53fbd46fb1ad2..14d3741425987 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md @@ -24,7 +24,7 @@ export declare class SavedObjectsRepository | [delete(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.delete.md) | | Deletes an object | | [deleteByNamespace(namespace, options)](./kibana-plugin-core-server.savedobjectsrepository.deletebynamespace.md) | | Deletes all objects from the provided namespace. | | [deleteFromNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) | | Removes one or more namespaces from a given multi-namespace saved object. If no namespaces remain, the saved object is deleted entirely. This method and \[addToNamespaces\][SavedObjectsRepository.addToNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to. | -| [find({ search, defaultSearchOperator, searchFields, rawSearchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespaces, type, filter, preference, })](./kibana-plugin-core-server.savedobjectsrepository.find.md) | | | +| [find({ search, defaultSearchOperator, searchFields, rootSearchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespaces, type, filter, preference, })](./kibana-plugin-core-server.savedobjectsrepository.find.md) | | | | [get(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.get.md) | | Gets a single object | | [incrementCounter(type, id, counterFieldName, options)](./kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md) | | Increases a counter field by one. Creates the document if one doesn't exist for the given id. | | [update(type, id, attributes, options)](./kibana-plugin-core-server.savedobjectsrepository.update.md) | | Updates an object | diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 2b19639e9b8e5..43bf640503a36 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -1300,7 +1300,7 @@ export interface SavedObjectsFindOptions { // (undocumented) perPage?: number; preference?: string; - rawSearchFields?: string[]; + rootSearchFields?: string[]; search?: string; searchFields?: string[]; // (undocumented) diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index 2bd3b600ccd84..351020004b0e7 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -33,7 +33,7 @@ import { HttpFetchOptions, HttpSetup } from '../http'; type SavedObjectsFindOptions = Omit< SavedObjectFindOptionsServer, - 'namespace' | 'sortOrder' | 'rawSearchFields' + 'namespace' | 'sortOrder' | 'rootSearchFields' >; type PromiseType> = T extends Promise ? U : never; diff --git a/src/core/server/saved_objects/import/check_origin_conflicts.test.ts b/src/core/server/saved_objects/import/check_origin_conflicts.test.ts index 45925687f36fa..bfd8394b2ffbc 100644 --- a/src/core/server/saved_objects/import/check_origin_conflicts.test.ts +++ b/src/core/server/saved_objects/import/check_origin_conflicts.test.ts @@ -95,7 +95,7 @@ describe('#checkOriginConflicts', () => { const { type, id, originId } = object; const search = `"${rawIdPrefix}${type}:${originId || id}" | "${originId || id}"`; // this template works for our basic test cases const expectedOptions = expect.objectContaining({ type, search }); - // exclude rawSearchFields, page, perPage, and fields attributes from assertion -- these are constant + // exclude rootSearchFields, page, perPage, and fields attributes from assertion -- these are constant // exclude namespace from assertion -- a separate test covers that expect(find).toHaveBeenNthCalledWith(n, expectedOptions); }; diff --git a/src/core/server/saved_objects/import/check_origin_conflicts.ts b/src/core/server/saved_objects/import/check_origin_conflicts.ts index 2d3ca73f5b270..f8f2b5707874f 100644 --- a/src/core/server/saved_objects/import/check_origin_conflicts.ts +++ b/src/core/server/saved_objects/import/check_origin_conflicts.ts @@ -101,7 +101,7 @@ const checkOriginConflict = async ( const findOptions = { type, search, - rawSearchFields: ['_id', 'originId'], + rootSearchFields: ['_id', 'originId'], page: 1, perPage: 10, fields: ['title'], diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 145f77b5a8d37..c332364096fe7 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -668,7 +668,7 @@ export class SavedObjectsRepository { search, defaultSearchOperator = 'OR', searchFields, - rawSearchFields, + rootSearchFields, hasReference, page = 1, perPage = 20, @@ -733,7 +733,7 @@ export class SavedObjectsRepository { search, defaultSearchOperator, searchFields, - rawSearchFields, + rootSearchFields, type: allowedTypes, sortField, sortOrder, 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 465fa0a1fe9a9..85c47029e36d5 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 @@ -309,7 +309,7 @@ describe('#getQueryParams', () => { }); }); - describe('`searchFields` and `rawSearchFields` parameters', () => { + describe('`searchFields` and `rootSearchFields` parameters', () => { const getExpectedFields = (searchFields: string[], typeOrTypes: string | string[]) => { const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes]; return searchFields.map((x) => types.map((y) => `${y}.${x}`)).flat(); @@ -317,10 +317,10 @@ describe('#getQueryParams', () => { const test = ({ searchFields, - rawSearchFields, + rootSearchFields, }: { searchFields?: string[]; - rawSearchFields?: string[]; + rootSearchFields?: string[]; }) => { for (const typeOrTypes of ALL_TYPE_SUBSETS) { const result = getQueryParams({ @@ -329,9 +329,9 @@ describe('#getQueryParams', () => { type: typeOrTypes, search, searchFields, - rawSearchFields, + rootSearchFields, }); - let fields = rawSearchFields || []; + let fields = rootSearchFields || []; if (searchFields) { fields = fields.concat(getExpectedFields(searchFields, typeOrTypes)); } @@ -344,9 +344,9 @@ describe('#getQueryParams', () => { type: undefined, search, searchFields, - rawSearchFields, + rootSearchFields, }); - let fields = rawSearchFields || []; + let fields = rootSearchFields || []; if (searchFields) { fields = fields.concat(getExpectedFields(searchFields, ALL_TYPES)); } @@ -361,20 +361,20 @@ describe('#getQueryParams', () => { type: undefined, search, searchFields: undefined, - rawSearchFields: ['foo', 'bar.baz'], + rootSearchFields: ['foo', 'bar.baz'], }) ).toThrowErrorMatchingInlineSnapshot( - `"rawSearchFields entry \\"bar.baz\\" is invalid: cannot contain \\".\\" character"` + `"rootSearchFields entry \\"bar.baz\\" is invalid: cannot contain \\".\\" character"` ); }); - it('includes lenient flag and all fields when `searchFields` and `rawSearchFields` are not specified', () => { + it('includes lenient flag and all fields when `searchFields` and `rootSearchFields` are not specified', () => { const result = getQueryParams({ mappings, registry, search, searchFields: undefined, - rawSearchFields: undefined, + rootSearchFields: undefined, }); expectResult(result, expect.objectContaining({ lenient: true, fields: ['*'] })); }); @@ -392,15 +392,15 @@ describe('#getQueryParams', () => { }); it('includes specified raw search fields', () => { - test({ rawSearchFields: ['_id'] }); + test({ rootSearchFields: ['_id'] }); }); it('supports multiple raw search fields', () => { - test({ rawSearchFields: ['_id', 'originId'] }); + test({ rootSearchFields: ['_id', 'originId'] }); }); it('supports search fields and raw search fields', () => { - test({ searchFields: ['title'], rawSearchFields: ['_id'] }); + test({ searchFields: ['title'], rootSearchFields: ['_id'] }); }); }); diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts index 0d198efc63d1e..ad1a08187dc32 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts @@ -39,24 +39,24 @@ function getTypes(mappings: IndexMapping, type?: string | string[]) { } /** - * Get the field params based on the types, searchFields, and rawSearchFields + * Get the field params based on the types, searchFields, and rootSearchFields */ function getFieldsForTypes( types: string[], searchFields: string[] = [], - rawSearchFields: string[] = [] + rootSearchFields: string[] = [] ) { - if (!searchFields.length && !rawSearchFields.length) { + if (!searchFields.length && !rootSearchFields.length) { return { lenient: true, fields: ['*'], }; } - let fields = [...rawSearchFields]; + let fields = [...rootSearchFields]; fields.forEach((field) => { if (field.indexOf('.') !== -1) { - throw new Error(`rawSearchFields entry "${field}" is invalid: cannot contain "." character`); + throw new Error(`rootSearchFields entry "${field}" is invalid: cannot contain "." character`); } }); @@ -129,7 +129,7 @@ interface QueryParams { type?: string | string[]; search?: string; searchFields?: string[]; - rawSearchFields?: string[]; + rootSearchFields?: string[]; defaultSearchOperator?: string; hasReference?: HasReferenceQueryParams; kueryNode?: KueryNode; @@ -145,7 +145,7 @@ export function getQueryParams({ type, search, searchFields, - rawSearchFields, + rootSearchFields, defaultSearchOperator, hasReference, kueryNode, @@ -211,7 +211,7 @@ export function getQueryParams({ { simple_query_string: { query: search, - ...getFieldsForTypes(types, searchFields, rawSearchFields), + ...getFieldsForTypes(types, searchFields, rootSearchFields), ...(defaultSearchOperator ? { default_operator: defaultSearchOperator } : {}), }, }, diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts index 38fabb2b228aa..62e629ad33cc8 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts @@ -57,13 +57,13 @@ describe('getSearchDsl', () => { }); describe('passes control', () => { - it('passes (mappings, schema, namespaces, type, search, searchFields, rawSearchFields, hasReference) to getQueryParams', () => { + it('passes (mappings, schema, namespaces, type, search, searchFields, rootSearchFields, hasReference) to getQueryParams', () => { const opts = { namespaces: ['foo-namespace'], type: 'foo', search: 'bar', searchFields: ['baz'], - rawSearchFields: ['qux'], + rootSearchFields: ['qux'], defaultSearchOperator: 'AND', hasReference: { type: 'bar', @@ -80,7 +80,7 @@ describe('getSearchDsl', () => { type: opts.type, search: opts.search, searchFields: opts.searchFields, - rawSearchFields: opts.rawSearchFields, + rootSearchFields: opts.rootSearchFields, defaultSearchOperator: opts.defaultSearchOperator, hasReference: opts.hasReference, }); diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts index 13c5169ec1003..ddf20606800c8 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts @@ -31,7 +31,7 @@ interface GetSearchDslOptions { search?: string; defaultSearchOperator?: string; searchFields?: string[]; - rawSearchFields?: string[]; + rootSearchFields?: string[]; sortField?: string; sortOrder?: string; namespaces?: string[]; @@ -52,7 +52,7 @@ export function getSearchDsl( search, defaultSearchOperator, searchFields, - rawSearchFields, + rootSearchFields, sortField, sortOrder, namespaces, @@ -76,7 +76,7 @@ export function getSearchDsl( type, search, searchFields, - rawSearchFields, + rootSearchFields, defaultSearchOperator, hasReference, kueryNode, diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index cf45344cf33e7..7b3697173ef0d 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -82,9 +82,11 @@ export interface SavedObjectsFindOptions { search?: string; /** The fields to perform the parsed query against. See Elasticsearch Simple Query String `fields` argument for more information */ searchFields?: string[]; - /** The fields to perform the parsed query against. Unlike the `searchFields` argument, these are expected to be raw and will not be - * modified. If used in conjunction with `searchFields`, both are concatenated together. */ - rawSearchFields?: string[]; + /** + * The fields to perform the parsed query against. Unlike the `searchFields` argument, these are expected to be root fields and will not + * be modified. If used in conjunction with `searchFields`, both are concatenated together. + */ + rootSearchFields?: string[]; hasReference?: { type: string; id: string }; defaultSearchOperator?: 'AND' | 'OR'; filter?: string; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 955d7a2b3899b..b8cc325fb4422 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2214,7 +2214,7 @@ export interface SavedObjectsFindOptions { // (undocumented) perPage?: number; preference?: string; - rawSearchFields?: string[]; + rootSearchFields?: string[]; search?: string; searchFields?: string[]; // (undocumented) @@ -2456,7 +2456,7 @@ export class SavedObjectsRepository { deleteByNamespace(namespace: string, options?: SavedObjectsDeleteByNamespaceOptions): Promise; deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<{}>; // (undocumented) - find({ search, defaultSearchOperator, searchFields, rawSearchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespaces, type, filter, preference, }: SavedObjectsFindOptions): Promise>; + find({ search, defaultSearchOperator, searchFields, rootSearchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespaces, type, filter, preference, }: SavedObjectsFindOptions): Promise>; get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; incrementCounter(type: string, id: string, counterFieldName: string, options?: SavedObjectsIncrementCounterOptions): Promise; update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; From 90bb5a9f2f01792e456274417601316f21822ba1 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Wed, 15 Jul 2020 13:59:42 -0400 Subject: [PATCH 50/55] Address most nits from fourth review --- .../import/import_saved_objects.test.ts | 4 +-- .../import/import_saved_objects.ts | 3 +- .../import/regenerate_ids.test.ts | 33 ++++++++++--------- .../saved_objects/import/regenerate_ids.ts | 4 +-- .../import/resolve_import_errors.test.ts | 10 +++--- .../import/resolve_import_errors.ts | 3 +- .../routes/integration_tests/import.test.ts | 6 ++-- .../resolve_import_errors.test.ts | 6 ++-- .../saved_objects/service/lib/repository.ts | 27 ++++++++------- 9 files changed, 50 insertions(+), 46 deletions(-) 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 62a0e4d3639e4..bc9350c0d717d 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 @@ -56,7 +56,7 @@ describe('#importSavedObjectsFromStream', () => { collectedObjects: [], importIdMap: new Map(), }); - getMockFn(regenerateIds).mockReturnValue({ importIdMap: new Map() }); + getMockFn(regenerateIds).mockReturnValue(new Map()); getMockFn(validateReferences).mockResolvedValue({ errors: [], filteredObjects: [] }); getMockFn(checkConflicts).mockResolvedValue({ errors: [], @@ -270,7 +270,7 @@ describe('#importSavedObjectsFromStream', () => { getMockFn(validateReferences).mockResolvedValue({ errors: [errors[1]], filteredObjects }); // this importIdMap is not composed with the one obtained from `collectSavedObjects` const importIdMap = new Map().set(`id1`, { id: `newId1` }); - getMockFn(regenerateIds).mockReturnValue({ importIdMap }); + getMockFn(regenerateIds).mockReturnValue(importIdMap); await importSavedObjectsFromStream(options); const createSavedObjectsOptions = { savedObjectsClient, importIdMap, overwrite, namespace }; 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 d3244471378d3..3982c67d24a28 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.ts @@ -67,8 +67,7 @@ export async function importSavedObjectsFromStream({ let objectsToCreate = validateReferencesResult.filteredObjects; if (createNewCopies) { - const regenerateIdsResult = regenerateIds(collectSavedObjectsResult.collectedObjects); - importIdMap = regenerateIdsResult.importIdMap; + importIdMap = regenerateIds(collectSavedObjectsResult.collectedObjects); } else { // Check single-namespace objects for conflicts in this namespace, and check multi-namespace objects for conflicts across all namespaces const checkConflictsOptions = { diff --git a/src/core/server/saved_objects/import/regenerate_ids.test.ts b/src/core/server/saved_objects/import/regenerate_ids.test.ts index 4a69f45dd52e5..1bbc2693e4f49 100644 --- a/src/core/server/saved_objects/import/regenerate_ids.test.ts +++ b/src/core/server/saved_objects/import/regenerate_ids.test.ts @@ -26,27 +26,28 @@ describe('#regenerateIds', () => { { type: 'foo', id: '1' }, { type: 'bar', id: '2' }, { type: 'baz', id: '3' }, - ] as any) as Array>; + ] as any) as SavedObject[]; test('returns expected values', () => { + mockUuidv4 + .mockReturnValueOnce('uuidv4 #1') + .mockReturnValueOnce('uuidv4 #2') + .mockReturnValueOnce('uuidv4 #3'); expect(regenerateIds(objects)).toMatchInlineSnapshot(` - Object { - "importIdMap": Map { - "foo:1" => Object { - "id": "uuidv4", - "omitOriginId": true, - }, - "bar:2" => Object { - "id": "uuidv4", - "omitOriginId": true, - }, - "baz:3" => Object { - "id": "uuidv4", - "omitOriginId": true, - }, + Map { + "foo:1" => Object { + "id": "uuidv4 #1", + "omitOriginId": true, + }, + "bar:2" => Object { + "id": "uuidv4 #2", + "omitOriginId": true, + }, + "baz:3" => Object { + "id": "uuidv4 #3", + "omitOriginId": true, }, } `); - expect(mockUuidv4).toHaveBeenCalledTimes(3); }); }); diff --git a/src/core/server/saved_objects/import/regenerate_ids.ts b/src/core/server/saved_objects/import/regenerate_ids.ts index 01e305785ef01..647386ed16469 100644 --- a/src/core/server/saved_objects/import/regenerate_ids.ts +++ b/src/core/server/saved_objects/import/regenerate_ids.ts @@ -25,9 +25,9 @@ import { SavedObject } from '../types'; * * @param objects The saved objects to generate new IDs for. */ -export const regenerateIds = (objects: Array>) => { +export const regenerateIds = (objects: SavedObject[]) => { const importIdMap = objects.reduce((acc, object) => { return acc.set(`${object.type}:${object.id}`, { id: uuidv4(), omitOriginId: true }); }, new Map()); - return { importIdMap }; + return importIdMap; }; 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 b617ce3efec8e..eb35dc3e9b31f 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 @@ -65,7 +65,7 @@ describe('#importSavedObjectsFromStream', () => { collectedObjects: [], importIdMap: new Map(), }); - getMockFn(regenerateIds).mockReturnValue({ importIdMap: new Map() }); + getMockFn(regenerateIds).mockReturnValue(new Map()); getMockFn(validateReferences).mockResolvedValue({ errors: [], filteredObjects: [] }); getMockFn(checkConflicts).mockResolvedValue({ errors: [], @@ -336,13 +336,13 @@ describe('#importSavedObjectsFromStream', () => { errors: [errors[1]], filteredObjects: [], // doesn't matter }); - getMockFn(regenerateIds).mockReturnValue({ - importIdMap: new Map([ + getMockFn(regenerateIds).mockReturnValue( + new Map([ ['foo', { id: 'randomId1' }], ['bar', { id: 'randomId2' }], ['baz', { id: 'randomId3' }], - ]), - }); + ]) + ); getMockFn(checkConflicts).mockResolvedValue({ errors: [errors[2]], filteredObjects: [], 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 81b48ef51338f..be7a457fdce75 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.ts @@ -102,8 +102,7 @@ export async function resolveSavedObjectsImportErrors({ if (createNewCopies) { // In case any missing reference errors were resolved, ensure that we regenerate those object IDs as well // This is because a retry to resolve a missing reference error may not necessarily specify a destinationId - const regenerateIdsResult = regenerateIds(objectsToResolve); - importIdMap = regenerateIdsResult.importIdMap; + importIdMap = regenerateIds(objectsToResolve); } // Check single-namespace objects for conflicts in this namespace, and check multi-namespace objects for conflicts across all namespaces diff --git a/src/core/server/saved_objects/routes/integration_tests/import.test.ts b/src/core/server/saved_objects/routes/integration_tests/import.test.ts index 4759e36160698..bf3731f4a7d52 100644 --- a/src/core/server/saved_objects/routes/integration_tests/import.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/import.test.ts @@ -121,7 +121,7 @@ describe(`POST ${URL}`, () => { expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( [expect.objectContaining({ migrationVersion: {} })], - expect.anything() // options + expect.any(Object) // options ); }); @@ -249,7 +249,7 @@ describe(`POST ${URL}`, () => { expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1); expect(savedObjectsClient.bulkGet).toHaveBeenCalledWith( [{ fields: ['id'], id: 'my-pattern', type: 'index-pattern' }], - expect.anything() // options + expect.any(Object) // options ); expect(savedObjectsClient.bulkCreate).not.toHaveBeenCalled(); // no objects were created }); @@ -305,7 +305,7 @@ describe(`POST ${URL}`, () => { originId: undefined, }), ], - expect.anything() // options + expect.any(Object) // options ); }); }); diff --git a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts index d063adbdbd9e9..42f28eb468b52 100644 --- a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts @@ -129,7 +129,7 @@ describe(`POST ${URL}`, () => { expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( [expect.objectContaining({ migrationVersion: {} })], - expect.anything() // options + expect.any(Object) // options ); }); @@ -240,7 +240,7 @@ describe(`POST ${URL}`, () => { expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1); expect(savedObjectsClient.bulkGet).toHaveBeenCalledWith( [{ fields: ['id'], id: 'existing', type: 'index-pattern' }], - expect.anything() // options + expect.any(Object) // options ); }); @@ -299,7 +299,7 @@ describe(`POST ${URL}`, () => { originId: undefined, }), ], - expect.anything() // options + expect.any(Object) // options ); }); }); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index c332364096fe7..cdfa79d6ab972 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -26,7 +26,7 @@ import { getRootPropertiesObjects, IndexMapping } from '../../mappings'; import { getSearchDsl } from './search_dsl'; import { includedFields } from './included_fields'; import { decorateEsError } from './decorate_es_error'; -import { SavedObjectsErrorHelpers } from './errors'; +import { SavedObjectsErrorHelpers, DecoratedError } from './errors'; import { decodeRequestVersion, encodeVersion, encodeHitVersion } from '../../version'; import { KibanaMigrator } from '../../migrations'; import { @@ -296,7 +296,7 @@ export class SavedObjectsRepository { error: { id: object.id, type: object.type, - error: SavedObjectsErrorHelpers.createUnsupportedTypeError(object.type).output.payload, + error: errorContent(SavedObjectsErrorHelpers.createUnsupportedTypeError(object.type)), }, }; } @@ -355,7 +355,7 @@ export class SavedObjectsRepository { id, type, error: { - ...SavedObjectsErrorHelpers.createConflictError(type, id).output.payload, + ...errorContent(SavedObjectsErrorHelpers.createConflictError(type, id)), metadata: { isNotOverwritable: true }, }, }, @@ -462,7 +462,7 @@ export class SavedObjectsRepository { error: { id, type, - error: SavedObjectsErrorHelpers.createUnsupportedTypeError(type).output.payload, + error: errorContent(SavedObjectsErrorHelpers.createUnsupportedTypeError(type)), }, }; } @@ -503,7 +503,7 @@ export class SavedObjectsRepository { id, type, error: { - ...SavedObjectsErrorHelpers.createConflictError(type, id).output.payload, + ...errorContent(SavedObjectsErrorHelpers.createConflictError(type, id)), ...(!this.rawDocExistsInNamespace(doc, namespace) && { metadata: { isNotOverwritable: true }, }), @@ -804,7 +804,7 @@ export class SavedObjectsRepository { error: { id, type, - error: SavedObjectsErrorHelpers.createUnsupportedTypeError(type).output.payload, + error: errorContent(SavedObjectsErrorHelpers.createUnsupportedTypeError(type)), }, }; } @@ -849,7 +849,7 @@ export class SavedObjectsRepository { return ({ id, type, - error: SavedObjectsErrorHelpers.createGenericNotFoundError(type, id).output.payload, + error: errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)), } as any) as SavedObject; } @@ -1178,7 +1178,7 @@ export class SavedObjectsRepository { error: { id, type, - error: SavedObjectsErrorHelpers.createGenericNotFoundError(type, id).output.payload, + error: errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)), }, }; } @@ -1243,7 +1243,7 @@ export class SavedObjectsRepository { error: { id, type, - error: SavedObjectsErrorHelpers.createGenericNotFoundError(type, id).output.payload, + error: errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)), }, }; } @@ -1557,9 +1557,9 @@ export class SavedObjectsRepository { function getBulkOperationError(error: { type: string; reason?: string }, type: string, id: string) { switch (error.type) { case 'version_conflict_engine_exception': - return SavedObjectsErrorHelpers.createConflictError(type, id).output.payload; + return errorContent(SavedObjectsErrorHelpers.createConflictError(type, id)); case 'document_missing_exception': - return SavedObjectsErrorHelpers.createGenericNotFoundError(type, id).output.payload; + return errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)); default: return { message: error.reason || JSON.stringify(error), @@ -1611,4 +1611,9 @@ function getSavedObjectNamespaces( return [getNamespaceString(namespace)]; } +/** + * Extracts the contents of a decorated error to return the attributes for bulk operations. + */ +const errorContent = (error: DecoratedError) => error.output.payload; + const unique = (array: string[]) => [...new Set(array)]; From ced0415e6e0c948b54e6d38d61ff655208c96fb7 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Wed, 15 Jul 2020 14:02:04 -0400 Subject: [PATCH 51/55] Remove deprecations --- ...c.savedobjectsimportretry.createnewcopy.md | 5 +--- ...gin-core-public.savedobjectsimportretry.md | 2 +- ...vedobjectsimportoptions.createnewcopies.md | 5 +--- ...n-core-server.savedobjectsimportoptions.md | 4 ++-- ...ver.savedobjectsimportoptions.overwrite.md | 7 +----- ...r.savedobjectsimportretry.createnewcopy.md | 5 +--- ...gin-core-server.savedobjectsimportretry.md | 2 +- ...olveimporterrorsoptions.createnewcopies.md | 5 +--- ....savedobjectsresolveimporterrorsoptions.md | 2 +- src/core/public/public.api.md | 1 - src/core/server/saved_objects/import/types.ts | 23 ++++--------------- src/core/server/server.api.md | 4 ---- 12 files changed, 14 insertions(+), 51 deletions(-) diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.createnewcopy.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.createnewcopy.md index 69b5111daf691..f60c713973d58 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.createnewcopy.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.createnewcopy.md @@ -4,10 +4,7 @@ ## SavedObjectsImportRetry.createNewCopy property -> Warning: This API is now obsolete. -> -> If `createNewCopy` is specified, the new object has a new (undefined) origin ID. This is only needed for the case where `createNewCopies` mode is disabled and ambiguous source conflicts are detected. When `createNewCopies` mode is permanently enabled, this field will be redundant and can be removed. -> +If `createNewCopy` is specified, the new object has a new (undefined) origin ID. This is only needed for the case where `createNewCopies` mode is disabled and ambiguous source conflicts are detected. Signature: diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.md index 82c92fe0fb8e3..a785438c55fd4 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.md @@ -16,7 +16,7 @@ export interface SavedObjectsImportRetry | Property | Type | Description | | --- | --- | --- | -| [createNewCopy](./kibana-plugin-core-public.savedobjectsimportretry.createnewcopy.md) | boolean | | +| [createNewCopy](./kibana-plugin-core-public.savedobjectsimportretry.createnewcopy.md) | boolean | If createNewCopy is specified, the new object has a new (undefined) origin ID. This is only needed for the case where createNewCopies mode is disabled and ambiguous source conflicts are detected. | | [destinationId](./kibana-plugin-core-public.savedobjectsimportretry.destinationid.md) | string | The object ID that will be created or overwritten. If not specified, the id field will be used. | | [id](./kibana-plugin-core-public.savedobjectsimportretry.id.md) | string | | | [overwrite](./kibana-plugin-core-public.savedobjectsimportretry.overwrite.md) | boolean | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.createnewcopies.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.createnewcopies.md index 86551edde097b..23c6fe0051746 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.createnewcopies.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.createnewcopies.md @@ -4,10 +4,7 @@ ## SavedObjectsImportOptions.createNewCopies property -> Warning: This API is now obsolete. -> -> If true, will create new copies of import objects, each with a random `id` and undefined `originId`. This option will be removed and permanently enabled in a future release. -> +If true, will create new copies of import objects, each with a random `id` and undefined `originId`. Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.md index dddc1889c3dd2..6578b01ffa609 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.md @@ -16,10 +16,10 @@ export interface SavedObjectsImportOptions | Property | Type | Description | | --- | --- | --- | -| [createNewCopies](./kibana-plugin-core-server.savedobjectsimportoptions.createnewcopies.md) | boolean | | +| [createNewCopies](./kibana-plugin-core-server.savedobjectsimportoptions.createnewcopies.md) | boolean | If true, will create new copies of import objects, each with a random id and undefined originId. | | [namespace](./kibana-plugin-core-server.savedobjectsimportoptions.namespace.md) | string | if specified, will import in given namespace, else will import as global object | | [objectLimit](./kibana-plugin-core-server.savedobjectsimportoptions.objectlimit.md) | number | The maximum number of object to import | -| [overwrite](./kibana-plugin-core-server.savedobjectsimportoptions.overwrite.md) | boolean | | +| [overwrite](./kibana-plugin-core-server.savedobjectsimportoptions.overwrite.md) | boolean | If true, will override existing object if present. Note: this has no effect when used with the createNewCopies option. | | [readStream](./kibana-plugin-core-server.savedobjectsimportoptions.readstream.md) | Readable | The stream of [saved objects](./kibana-plugin-core-server.savedobject.md) to import | | [savedObjectsClient](./kibana-plugin-core-server.savedobjectsimportoptions.savedobjectsclient.md) | SavedObjectsClientContract | [client](./kibana-plugin-core-server.savedobjectsclientcontract.md) to use to perform the import operation | | [typeRegistry](./kibana-plugin-core-server.savedobjectsimportoptions.typeregistry.md) | ISavedObjectTypeRegistry | The registry of all known saved object types | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.overwrite.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.overwrite.md index 395071772c80e..1e9192c47679d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.overwrite.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.overwrite.md @@ -4,12 +4,7 @@ ## SavedObjectsImportOptions.overwrite property -> Warning: This API is now obsolete. -> -> If true, will override existing object if present. This option will be removed and permanently disabled in a future release. -> -> Note: this has no effect when used with the `createNewCopies` option. -> +If true, will override existing object if present. Note: this has no effect when used with the `createNewCopies` option. Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.createnewcopy.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.createnewcopy.md index 0b59c98e6eda3..e9cc92c55ded1 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.createnewcopy.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.createnewcopy.md @@ -4,10 +4,7 @@ ## SavedObjectsImportRetry.createNewCopy property -> Warning: This API is now obsolete. -> -> If `createNewCopy` is specified, the new object has a new (undefined) origin ID. This is only needed for the case where `createNewCopies` mode is disabled and ambiguous source conflicts are detected. When `createNewCopies` mode is permanently enabled, this field will be redundant and can be removed. -> +If `createNewCopy` is specified, the new object has a new (undefined) origin ID. This is only needed for the case where `createNewCopies` mode is disabled and ambiguous source conflicts are detected. Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.md index 3759bb4e72ff8..258f18aebffc8 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.md @@ -16,7 +16,7 @@ export interface SavedObjectsImportRetry | Property | Type | Description | | --- | --- | --- | -| [createNewCopy](./kibana-plugin-core-server.savedobjectsimportretry.createnewcopy.md) | boolean | | +| [createNewCopy](./kibana-plugin-core-server.savedobjectsimportretry.createnewcopy.md) | boolean | If createNewCopy is specified, the new object has a new (undefined) origin ID. This is only needed for the case where createNewCopies mode is disabled and ambiguous source conflicts are detected. | | [destinationId](./kibana-plugin-core-server.savedobjectsimportretry.destinationid.md) | string | The object ID that will be created or overwritten. If not specified, the id field will be used. | | [id](./kibana-plugin-core-server.savedobjectsimportretry.id.md) | string | | | [overwrite](./kibana-plugin-core-server.savedobjectsimportretry.overwrite.md) | boolean | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.createnewcopies.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.createnewcopies.md index 511ecaafc27e4..82831eae37d7b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.createnewcopies.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.createnewcopies.md @@ -4,10 +4,7 @@ ## SavedObjectsResolveImportErrorsOptions.createNewCopies property -> Warning: This API is now obsolete. -> -> If true, will create new copies of import objects, each with a random `id` and undefined `originId`. This option will be removed and permanently enabled in a future release. -> +If true, will create new copies of import objects, each with a random `id` and undefined `originId`. Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md index 8de15c9ff407b..f97bf284375d1 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md @@ -16,7 +16,7 @@ export interface SavedObjectsResolveImportErrorsOptions | Property | Type | Description | | --- | --- | --- | -| [createNewCopies](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.createnewcopies.md) | boolean | | +| [createNewCopies](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.createnewcopies.md) | boolean | If true, will create new copies of import objects, each with a random id and undefined originId. | | [namespace](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.namespace.md) | string | if specified, will import in given namespace | | [objectLimit](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.objectlimit.md) | number | The maximum number of object to import | | [readStream](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.readstream.md) | Readable | The stream of [saved objects](./kibana-plugin-core-server.savedobject.md) to resolve errors from | diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 43bf640503a36..035ae50305aba 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -1383,7 +1383,6 @@ export interface SavedObjectsImportResponse { // @public export interface SavedObjectsImportRetry { - // @deprecated (undocumented) createNewCopy?: boolean; destinationId?: string; // (undocumented) diff --git a/src/core/server/saved_objects/import/types.ts b/src/core/server/saved_objects/import/types.ts index 0d153f26f2033..e0abe23a70372 100644 --- a/src/core/server/saved_objects/import/types.ts +++ b/src/core/server/saved_objects/import/types.ts @@ -39,10 +39,8 @@ export interface SavedObjectsImportRetry { to: string; }>; /** - * @deprecated * If `createNewCopy` is specified, the new object has a new (undefined) origin ID. This is only needed for the case where - * `createNewCopies` mode is disabled and ambiguous source conflicts are detected. When `createNewCopies` mode is permanently enabled, - * this field will be redundant and can be removed. + * `createNewCopies` mode is disabled and ambiguous source conflicts are detected. */ createNewCopy?: boolean; } @@ -155,12 +153,7 @@ export interface SavedObjectsImportOptions { readStream: Readable; /** The maximum number of object to import */ objectLimit: number; - /** - * @deprecated - * If true, will override existing object if present. This option will be removed and permanently disabled in a future release. - * - * Note: this has no effect when used with the `createNewCopies` option. - */ + /** If true, will override existing object if present. Note: this has no effect when used with the `createNewCopies` option. */ overwrite: boolean; /** {@link SavedObjectsClientContract | client} to use to perform the import operation */ savedObjectsClient: SavedObjectsClientContract; @@ -168,11 +161,7 @@ export interface SavedObjectsImportOptions { typeRegistry: ISavedObjectTypeRegistry; /** if specified, will import in given namespace, else will import as global object */ namespace?: string; - /** - * @deprecated - * If true, will create new copies of import objects, each with a random `id` and undefined `originId`. This option will be removed and - * permanently enabled in a future release. - */ + /** If true, will create new copies of import objects, each with a random `id` and undefined `originId`. */ createNewCopies: boolean; } @@ -193,11 +182,7 @@ export interface SavedObjectsResolveImportErrorsOptions { retries: SavedObjectsImportRetry[]; /** if specified, will import in given namespace */ namespace?: string; - /** - * @deprecated - * If true, will create new copies of import objects, each with a random `id` and undefined `originId`. This option will be removed and - * permanently enabled in a future release. - */ + /** If true, will create new copies of import objects, each with a random `id` and undefined `originId`. */ createNewCopies: boolean; } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index b8cc325fb4422..eebd33cff8458 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2292,11 +2292,9 @@ export interface SavedObjectsImportMissingReferencesError { // @public export interface SavedObjectsImportOptions { - // @deprecated (undocumented) createNewCopies: boolean; namespace?: string; objectLimit: number; - // @deprecated (undocumented) overwrite: boolean; readStream: Readable; savedObjectsClient: SavedObjectsClientContract; @@ -2317,7 +2315,6 @@ export interface SavedObjectsImportResponse { // @public export interface SavedObjectsImportRetry { - // @deprecated (undocumented) createNewCopy?: boolean; destinationId?: string; // (undocumented) @@ -2470,7 +2467,6 @@ export interface SavedObjectsRepositoryFactory { // @public export interface SavedObjectsResolveImportErrorsOptions { - // @deprecated (undocumented) createNewCopies: boolean; namespace?: string; objectLimit: number; From 357048239aa3cd46263efca159f69697aab3cd4b Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Wed, 15 Jul 2020 15:17:15 -0400 Subject: [PATCH 52/55] Refactor parameters of import modules Now uses consistent 'params' objects. --- .../import/check_conflicts.test.ts | 42 ++--- .../saved_objects/import/check_conflicts.ts | 15 +- .../import/check_origin_conflicts.test.ts | 170 +++++++++--------- .../import/check_origin_conflicts.ts | 31 ++-- .../import/create_saved_objects.test.ts | 53 +++--- .../import/create_saved_objects.ts | 19 +- .../import/import_saved_objects.test.ts | 43 +++-- .../import/import_saved_objects.ts | 31 ++-- .../import/resolve_import_errors.test.ts | 53 +++--- .../import/resolve_import_errors.ts | 25 +-- 10 files changed, 248 insertions(+), 234 deletions(-) diff --git a/src/core/server/saved_objects/import/check_conflicts.test.ts b/src/core/server/saved_objects/import/check_conflicts.test.ts index 8fbc1578be185..a0b34af272fbf 100644 --- a/src/core/server/saved_objects/import/check_conflicts.test.ts +++ b/src/core/server/saved_objects/import/check_conflicts.test.ts @@ -25,7 +25,7 @@ import { SavedObjectsErrorHelpers } from '..'; import { checkConflicts } from './check_conflicts'; type SavedObjectType = SavedObject<{ title?: string }>; -type CheckConflictsOptions = Parameters[1]; +type CheckConflictsParams = Parameters[0]; /** * Function to create a realistic-looking import object given a type and ID @@ -69,22 +69,16 @@ describe('#checkConflicts', () => { let savedObjectsClient: jest.Mocked; let socCheckConflicts: typeof savedObjectsClient['checkConflicts']; - /** - * Creates an options object to be used as an argument for createSavedObjects - * Includes mock savedObjectsClient - */ - const setupOptions = ( - options: { - namespace?: string; - ignoreRegularConflicts?: boolean; - createNewCopies?: boolean; - } = {} - ): CheckConflictsOptions => { - const { namespace, ignoreRegularConflicts, createNewCopies } = options; + const setupParams = (partial: { + objects: SavedObjectType[]; + namespace?: string; + ignoreRegularConflicts?: boolean; + createNewCopies?: boolean; + }): CheckConflictsParams => { savedObjectsClient = savedObjectsClientMock.create(); socCheckConflicts = savedObjectsClient.checkConflicts; socCheckConflicts.mockResolvedValue({ errors: [] }); // by default, mock to empty results - return { savedObjectsClient, namespace, ignoreRegularConflicts, createNewCopies }; + return { ...partial, savedObjectsClient }; }; beforeEach(() => { @@ -94,9 +88,9 @@ describe('#checkConflicts', () => { it('exits early if there are no objects to check', async () => { const namespace = 'foo-namespace'; - const options = setupOptions({ namespace }); + const params = setupParams({ objects: [], namespace }); - const checkConflictsResult = await checkConflicts([], options); + const checkConflictsResult = await checkConflicts(params); expect(socCheckConflicts).not.toHaveBeenCalled(); expect(checkConflictsResult).toEqual({ filteredObjects: [], @@ -107,19 +101,19 @@ describe('#checkConflicts', () => { it('calls checkConflicts with expected inputs', async () => { const namespace = 'foo-namespace'; - const options = setupOptions({ namespace }); + const params = setupParams({ objects, namespace }); - await checkConflicts(objects, options); + await checkConflicts(params); expect(socCheckConflicts).toHaveBeenCalledTimes(1); expect(socCheckConflicts).toHaveBeenCalledWith(objects, { namespace }); }); it('returns expected result', async () => { const namespace = 'foo-namespace'; - const options = setupOptions({ namespace }); + const params = setupParams({ objects, namespace }); socCheckConflicts.mockResolvedValue({ errors: [obj2Error, obj3Error, obj4Error] }); - const checkConflictsResult = await checkConflicts(objects, options); + const checkConflictsResult = await checkConflicts(params); expect(checkConflictsResult).toEqual({ filteredObjects: [obj1, obj3], errors: [ @@ -136,10 +130,10 @@ describe('#checkConflicts', () => { it('does not return errors for regular conflicts when ignoreRegularConflicts=true', async () => { const namespace = 'foo-namespace'; - const options = setupOptions({ namespace, ignoreRegularConflicts: true }); + const params = setupParams({ objects, namespace, ignoreRegularConflicts: true }); socCheckConflicts.mockResolvedValue({ errors: [obj2Error, obj3Error, obj4Error] }); - const checkConflictsResult = await checkConflicts(objects, options); + const checkConflictsResult = await checkConflicts(params); expect(checkConflictsResult).toEqual( expect.objectContaining({ filteredObjects: [obj1, obj2, obj3], @@ -156,10 +150,10 @@ describe('#checkConflicts', () => { it('adds `omitOriginId` field to `importIdMap` entries when createNewCopies=true', async () => { const namespace = 'foo-namespace'; - const options = setupOptions({ namespace, createNewCopies: true }); + const params = setupParams({ objects, namespace, createNewCopies: true }); socCheckConflicts.mockResolvedValue({ errors: [obj2Error, obj3Error, obj4Error] }); - const checkConflictsResult = await checkConflicts(objects, options); + const checkConflictsResult = await checkConflicts(params); expect(checkConflictsResult).toEqual( expect.objectContaining({ importIdMap: new Map([ diff --git a/src/core/server/saved_objects/import/check_conflicts.ts b/src/core/server/saved_objects/import/check_conflicts.ts index 65b078021792a..eec902494ec41 100644 --- a/src/core/server/saved_objects/import/check_conflicts.ts +++ b/src/core/server/saved_objects/import/check_conflicts.ts @@ -25,7 +25,8 @@ import { SavedObjectError, } from '../types'; -interface CheckConflictsOptions { +interface CheckConflictsParams { + objects: Array>; savedObjectsClient: SavedObjectsClientContract; namespace?: string; ignoreRegularConflicts?: boolean; @@ -35,10 +36,13 @@ interface CheckConflictsOptions { const isUnresolvableConflict = (error: SavedObjectError) => error.statusCode === 409 && error.metadata?.isNotOverwritable; -export async function checkConflicts( - objects: Array>, - options: CheckConflictsOptions -) { +export async function checkConflicts({ + objects, + savedObjectsClient, + namespace, + ignoreRegularConflicts, + createNewCopies, +}: CheckConflictsParams) { const filteredObjects: Array> = []; const errors: SavedObjectsImportError[] = []; const importIdMap = new Map(); @@ -48,7 +52,6 @@ export async function checkConflicts( return { filteredObjects, errors, importIdMap }; } - const { savedObjectsClient, namespace, ignoreRegularConflicts, createNewCopies } = options; const checkConflictsResult = await savedObjectsClient.checkConflicts(objects, { namespace }); const errorMap = checkConflictsResult.errors.reduce( (acc, { type, id, error }) => acc.set(`${type}:${id}`, error), diff --git a/src/core/server/saved_objects/import/check_origin_conflicts.test.ts b/src/core/server/saved_objects/import/check_origin_conflicts.test.ts index bfd8394b2ffbc..05bc9ae0af004 100644 --- a/src/core/server/saved_objects/import/check_origin_conflicts.test.ts +++ b/src/core/server/saved_objects/import/check_origin_conflicts.test.ts @@ -31,8 +31,7 @@ import { typeRegistryMock } from '../saved_objects_type_registry.mock'; import { ISavedObjectTypeRegistry } from '..'; type SavedObjectType = SavedObject<{ title?: string }>; -type CheckOriginConflictsOptions = Parameters[1]; -type GetImportIdMapForRetriesOptions = Parameters[1]; +type CheckOriginConflictsParams = Parameters[0]; /** * Function to create a realistic-looking import object given a type, ID, and optional originId @@ -64,20 +63,23 @@ describe('#checkOriginConflicts', () => { saved_objects: objects.map((object) => ({ ...object, score: 0 })), }); - const setupOptions = ( - options: { - namespace?: string; - importIdMap?: Map; - ignoreRegularConflicts?: boolean; - } = {} - ): CheckOriginConflictsOptions => { - const { namespace, importIdMap = new Map(), ignoreRegularConflicts } = options; + const setupParams = (partial: { + objects: SavedObjectType[]; + namespace?: string; + importIdMap?: Map; + ignoreRegularConflicts?: boolean; + }): CheckOriginConflictsParams => { savedObjectsClient = savedObjectsClientMock.create(); find = savedObjectsClient.find; find.mockResolvedValue(getResultMock()); // mock zero hits response by default typeRegistry = typeRegistryMock.create(); typeRegistry.isMultiNamespace.mockImplementation((type) => type === MULTI_NS_TYPE); - return { savedObjectsClient, typeRegistry, namespace, ignoreRegularConflicts, importIdMap }; + return { + importIdMap: new Map(), // empty by default + ...partial, + savedObjectsClient, + typeRegistry, + }; }; const mockFindResult = (...objects: SavedObjectType[]) => { @@ -94,32 +96,32 @@ describe('#checkOriginConflicts', () => { const expectFindArgs = (n: number, object: SavedObject, rawIdPrefix: string) => { const { type, id, originId } = object; const search = `"${rawIdPrefix}${type}:${originId || id}" | "${originId || id}"`; // this template works for our basic test cases - const expectedOptions = expect.objectContaining({ type, search }); + const expectedArgs = expect.objectContaining({ type, search }); // exclude rootSearchFields, page, perPage, and fields attributes from assertion -- these are constant // exclude namespace from assertion -- a separate test covers that - expect(find).toHaveBeenNthCalledWith(n, expectedOptions); + expect(find).toHaveBeenNthCalledWith(n, expectedArgs); }; test('does not execute searches for non-multi-namespace objects', async () => { const objects = [otherObj, otherObjWithOriginId]; - const options = setupOptions(); + const params = setupParams({ objects }); - await checkOriginConflicts(objects, options); + await checkOriginConflicts(params); expect(find).not.toHaveBeenCalled(); }); test('executes searches for multi-namespace objects', async () => { const objects = [multiNsObj, otherObj, multiNsObjWithOriginId, otherObjWithOriginId]; - const options1 = setupOptions(); + const params1 = setupParams({ objects }); - await checkOriginConflicts(objects, options1); + await checkOriginConflicts(params1); expect(find).toHaveBeenCalledTimes(2); expectFindArgs(1, multiNsObj, ''); expectFindArgs(2, multiNsObjWithOriginId, ''); find.mockClear(); - const options2 = setupOptions({ namespace: 'some-namespace' }); - await checkOriginConflicts(objects, options2); + const params2 = setupParams({ objects, namespace: 'some-namespace' }); + await checkOriginConflicts(params2); expect(find).toHaveBeenCalledTimes(2); expectFindArgs(1, multiNsObj, 'some-namespace:'); expectFindArgs(2, multiNsObjWithOriginId, 'some-namespace:'); @@ -128,9 +130,9 @@ describe('#checkOriginConflicts', () => { test('searches within the current `namespace`', async () => { const objects = [multiNsObj]; const namespace = 'some-namespace'; - const options = setupOptions({ namespace }); + const params = setupParams({ objects, namespace }); - await checkOriginConflicts(objects, options); + await checkOriginConflicts(params); expect(find).toHaveBeenCalledTimes(1); expect(find).toHaveBeenCalledWith(expect.objectContaining({ namespaces: [namespace] })); }); @@ -141,9 +143,9 @@ describe('#checkOriginConflicts', () => { createObject(MULTI_NS_TYPE, weirdId), createObject(MULTI_NS_TYPE, 'some-id', weirdId), ]; - const options = setupOptions(); + const params = setupParams({ objects }); - await checkOriginConflicts(objects, options); + await checkOriginConflicts(params); const escapedId = `some\\"weird\\\\id`; const expectedQuery = `"${MULTI_NS_TYPE}:${escapedId}" | "${escapedId}"`; expect(find).toHaveBeenCalledTimes(2); @@ -194,16 +196,14 @@ describe('#checkOriginConflicts', () => { const obj2 = createObject(OTHER_TYPE, 'id-2', 'originId-foo'); // non-multi-namespace types are skipped when searching, so they will never have a match anyway const obj3 = createObject(MULTI_NS_TYPE, 'id-3'); const obj4 = createObject(MULTI_NS_TYPE, 'id-4', 'originId-bar'); - const options = setupOptions(); + const objects = [obj1, obj2, obj3, obj4]; + const params = setupParams({ objects }); // don't need to mock find results for obj3 and obj4, "no match" is the default find result in this test suite - const checkOriginConflictsResult = await checkOriginConflicts( - [obj1, obj2, obj3, obj4], - options - ); + const checkOriginConflictsResult = await checkOriginConflicts(params); const expectedResult = { - filteredObjects: [obj1, obj2, obj3, obj4], + filteredObjects: objects, importIdMap: new Map(), errors: [], }; @@ -217,7 +217,9 @@ describe('#checkOriginConflicts', () => { const obj2 = createObject(MULTI_NS_TYPE, 'id-2', obj1.id); const obj3 = createObject(MULTI_NS_TYPE, 'id-3', 'originId-foo'); const obj4 = createObject(MULTI_NS_TYPE, 'id-4', obj3.originId); - const options = setupOptions({ + const objects = [obj2, obj4]; + const params = setupParams({ + objects, importIdMap: new Map([ [`${obj1.type}:${obj1.id}`, {}], [`${obj2.type}:${obj2.id}`, {}], @@ -228,9 +230,9 @@ describe('#checkOriginConflicts', () => { mockFindResult(obj1); // find for obj2: the result is an inexact match with one destination that is exactly matched by obj1 so it is ignored -- accordingly, obj2 has no match mockFindResult(obj3); // find for obj4: the result is an inexact match with one destination that is exactly matched by obj3 so it is ignored -- accordingly, obj4 has no match - const checkOriginConflictsResult = await checkOriginConflicts([obj2, obj4], options); + const checkOriginConflictsResult = await checkOriginConflicts(params); const expectedResult = { - filteredObjects: [obj2, obj4], + filteredObjects: objects, importIdMap: new Map(), errors: [], }; @@ -243,7 +245,9 @@ describe('#checkOriginConflicts', () => { const obj1 = createObject(MULTI_NS_TYPE, 'id-1'); const obj2 = createObject(MULTI_NS_TYPE, 'id-2', obj1.id); const obj3 = createObject(MULTI_NS_TYPE, 'id-3', obj1.id); - const options = setupOptions({ + const objects = [obj3]; + const params = setupParams({ + objects, importIdMap: new Map([ [`${obj1.type}:${obj1.id}`, {}], [`${obj2.type}:${obj2.id}`, {}], @@ -252,9 +256,9 @@ describe('#checkOriginConflicts', () => { }); mockFindResult(obj1, obj2); // find for obj3: the result is an inexact match with two destinations that are exactly matched by obj1 and obj2 so they are ignored -- accordingly, obj3 has no match - const checkOriginConflictsResult = await checkOriginConflicts([obj3], options); + const checkOriginConflictsResult = await checkOriginConflicts(params); const expectedResult = { - filteredObjects: [obj3], + filteredObjects: objects, importIdMap: new Map(), errors: [], }; @@ -270,17 +274,18 @@ describe('#checkOriginConflicts', () => { const objA = createObject(MULTI_NS_TYPE, 'id-A', obj1.id); const obj2 = createObject(MULTI_NS_TYPE, 'id-2', 'originId-foo'); const objB = createObject(MULTI_NS_TYPE, 'id-B', obj2.originId); + const objects = [obj1, obj2]; const setup = (ignoreRegularConflicts: boolean) => { - const options = setupOptions({ ignoreRegularConflicts }); + const params = setupParams({ objects, ignoreRegularConflicts }); mockFindResult(objA); // find for obj1: the result is an inexact match with one destination mockFindResult(objB); // find for obj2: the result is an inexact match with one destination - return options; + return params; }; test('returns conflict error when ignoreRegularConflicts=false', async () => { - const options = setup(false); - const checkOriginConflictsResult = await checkOriginConflicts([obj1, obj2], options); + const params = setup(false); + const checkOriginConflictsResult = await checkOriginConflicts(params); const expectedResult = { filteredObjects: [], importIdMap: new Map(), @@ -290,10 +295,10 @@ describe('#checkOriginConflicts', () => { }); test('returns object with a `importIdMap` entry when ignoreRegularConflicts=true', async () => { - const options = setup(true); - const checkOriginConflictsResult = await checkOriginConflicts([obj1, obj2], options); + const params = setup(true); + const checkOriginConflictsResult = await checkOriginConflicts(params); const expectedResult = { - filteredObjects: [obj1, obj2], + filteredObjects: objects, importIdMap: new Map([ [`${obj1.type}:${obj1.id}`, { id: objA.id }], [`${obj2.type}:${obj2.id}`, { id: objB.id }], @@ -313,9 +318,11 @@ describe('#checkOriginConflicts', () => { const obj3 = createObject(MULTI_NS_TYPE, 'id-3', 'originId-foo'); const obj4 = createObject(MULTI_NS_TYPE, 'id-4', obj3.originId); const objB = createObject(MULTI_NS_TYPE, 'id-B', obj3.originId); + const objects = [obj2, obj4]; const setup = (ignoreRegularConflicts: boolean) => { - const options = setupOptions({ + const params = setupParams({ + objects, ignoreRegularConflicts, importIdMap: new Map([ [`${obj1.type}:${obj1.id}`, {}], @@ -326,12 +333,12 @@ describe('#checkOriginConflicts', () => { }); mockFindResult(obj1, objA); // find for obj2: the result is an inexact match with two destinations, but the first destination is exactly matched by obj1 so it is ignored -- accordingly, obj2 has an inexact match with one destination (objA) mockFindResult(objB, obj3); // find for obj4: the result is an inexact match with two destinations, but the second destination is exactly matched by obj3 so it is ignored -- accordingly, obj4 has an inexact match with one destination (objB) - return options; + return params; }; test('returns conflict error when ignoreRegularConflicts=false', async () => { - const options = setup(false); - const checkOriginConflictsResult = await checkOriginConflicts([obj2, obj4], options); + const params = setup(false); + const checkOriginConflictsResult = await checkOriginConflicts(params); const expectedResult = { filteredObjects: [], importIdMap: new Map(), @@ -341,10 +348,10 @@ describe('#checkOriginConflicts', () => { }); test('returns object with a `importIdMap` entry when ignoreRegularConflicts=true', async () => { - const options = setup(true); - const checkOriginConflictsResult = await checkOriginConflicts([obj2, obj4], options); + const params = setup(true); + const checkOriginConflictsResult = await checkOriginConflicts(params); const expectedResult = { - filteredObjects: [obj2, obj4], + filteredObjects: objects, importIdMap: new Map([ [`${obj2.type}:${obj2.id}`, { id: objA.id }], [`${obj4.type}:${obj4.id}`, { id: objB.id }], @@ -366,18 +373,16 @@ describe('#checkOriginConflicts', () => { const obj3 = createObject(MULTI_NS_TYPE, 'id-3', 'originId-foo'); const obj4 = createObject(MULTI_NS_TYPE, 'id-4', obj3.originId); const objB = createObject(MULTI_NS_TYPE, 'id-B', obj3.originId); - const options = setupOptions(); + const objects = [obj1, obj2, obj3, obj4]; + const params = setupParams({ objects }); mockFindResult(objA); // find for obj1: the result is an inexact match with one destination mockFindResult(objA); // find for obj2: the result is an inexact match with one destination mockFindResult(objB); // find for obj3: the result is an inexact match with one destination mockFindResult(objB); // find for obj4: the result is an inexact match with one destination - const checkOriginConflictsResult = await checkOriginConflicts( - [obj1, obj2, obj3, obj4], - options - ); + const checkOriginConflictsResult = await checkOriginConflicts(params); const expectedResult = { - filteredObjects: [obj1, obj2, obj3, obj4], + filteredObjects: objects, importIdMap: new Map([ [`${obj1.type}:${obj1.id}`, { id: 'uuidv4', omitOriginId: true }], [`${obj2.type}:${obj2.id}`, { id: 'uuidv4', omitOriginId: true }], @@ -399,11 +404,12 @@ describe('#checkOriginConflicts', () => { const objB = createObject(MULTI_NS_TYPE, 'id-B', obj1.id); const objC = createObject(MULTI_NS_TYPE, 'id-C', obj2.originId); const objD = createObject(MULTI_NS_TYPE, 'id-D', obj2.originId); - const options = setupOptions(); + const objects = [obj1, obj2]; + const params = setupParams({ objects }); mockFindResult(objA, objB); // find for obj1: the result is an inexact match with two destinations mockFindResult(objC, objD); // find for obj2: the result is an inexact match with two destinations - const checkOriginConflictsResult = await checkOriginConflicts([obj1, obj2], options); + const checkOriginConflictsResult = await checkOriginConflicts(params); const expectedResult = { filteredObjects: [], importIdMap: new Map(), @@ -427,18 +433,16 @@ describe('#checkOriginConflicts', () => { const objB = createObject(MULTI_NS_TYPE, 'id-B', obj1.id); const objC = createObject(MULTI_NS_TYPE, 'id-C', obj3.originId); const objD = createObject(MULTI_NS_TYPE, 'id-D', obj3.originId); - const options = setupOptions(); + const objects = [obj1, obj2, obj3, obj4]; + const params = setupParams({ objects }); mockFindResult(objA, objB); // find for obj1: the result is an inexact match with two destinations mockFindResult(objA, objB); // find for obj2: the result is an inexact match with two destinations mockFindResult(objC, objD); // find for obj3: the result is an inexact match with two destinations mockFindResult(objC, objD); // find for obj4: the result is an inexact match with two destinations - const checkOriginConflictsResult = await checkOriginConflicts( - [obj1, obj2, obj3, obj4], - options - ); + const checkOriginConflictsResult = await checkOriginConflicts(params); const expectedResult = { - filteredObjects: [obj1, obj2, obj3, obj4], + filteredObjects: objects, importIdMap: new Map([ [`${obj1.type}:${obj1.id}`, { id: 'uuidv4', omitOriginId: true }], [`${obj2.type}:${obj2.id}`, { id: 'uuidv4', omitOriginId: true }], @@ -474,7 +478,7 @@ describe('#checkOriginConflicts', () => { const importIdMap = new Map([...objects, obj3].map(({ type, id }) => [`${type}:${id}`, {}])); const setup = (ignoreRegularConflicts: boolean) => { - const options = setupOptions({ importIdMap, ignoreRegularConflicts }); + const params = setupParams({ objects, importIdMap, ignoreRegularConflicts }); // obj1 is a non-multi-namespace type, so it is skipped while searching mockFindResult(); // find for obj2: the result is no match mockFindResult(obj3); // find for obj4: the result is an inexact match with one destination that is exactly matched by obj3 so it is ignored -- accordingly, obj4 has no match @@ -482,12 +486,12 @@ describe('#checkOriginConflicts', () => { mockFindResult(objB, objC); // find for obj6: the result is an inexact match with two destinations mockFindResult(objD, objE); // find for obj7: the result is an inexact match with two destinations mockFindResult(objD, objE); // find for obj8: the result is an inexact match with two destinations - return options; + return params; }; test('returns errors for regular conflicts when ignoreRegularConflicts=false', async () => { - const options = setup(false); - const checkOriginConflictsResult = await checkOriginConflicts(objects, options); + const params = setup(false); + const checkOriginConflictsResult = await checkOriginConflicts(params); const expectedResult = { filteredObjects: [obj1, obj2, obj4, obj7, obj8], importIdMap: new Map([ @@ -504,8 +508,8 @@ describe('#checkOriginConflicts', () => { }); test('does not return errors for regular conflicts when ignoreRegularConflicts=true', async () => { - const options = setup(true); - const checkOriginConflictsResult = await checkOriginConflicts(objects, options); + const params = setup(true); + const checkOriginConflictsResult = await checkOriginConflicts(params); const expectedResult = { filteredObjects: [obj1, obj2, obj4, obj5, obj7, obj8], importIdMap: new Map([ @@ -523,30 +527,24 @@ describe('#checkOriginConflicts', () => { }); describe('#getImportIdMapForRetries', () => { - const setupOptions = ( - retries: SavedObjectsImportRetry[], - createNewCopies: boolean = false - ): GetImportIdMapForRetriesOptions => { - return { retries, createNewCopies }; - }; - const createRetry = ( { type, id }: { type: string; id: string }, - options: { destinationId?: string; createNewCopy?: boolean } = {} + params: { destinationId?: string; createNewCopy?: boolean } = {} ): SavedObjectsImportRetry => { - const { destinationId, createNewCopy } = options; + const { destinationId, createNewCopy } = params; return { type, id, overwrite: false, destinationId, replaceReferences: [], createNewCopy }; }; test('throws an error if retry is not found for an object', async () => { const obj1 = createObject(MULTI_NS_TYPE, 'id-1'); const obj2 = createObject(MULTI_NS_TYPE, 'id-2'); + const objects = [obj1, obj2]; const retries = [createRetry(obj1)]; - const options = setupOptions(retries); + const params = { objects, retries, createNewCopies: false }; - expect(() => - getImportIdMapForRetries([obj1, obj2], options) - ).toThrowErrorMatchingInlineSnapshot(`"Retry was expected for \\"multi:id-2\\" but not found"`); + expect(() => getImportIdMapForRetries(params)).toThrowErrorMatchingInlineSnapshot( + `"Retry was expected for \\"multi:id-2\\" but not found"` + ); }); test('returns expected results', async () => { @@ -561,9 +559,9 @@ describe('#getImportIdMapForRetries', () => { createRetry(obj3, { destinationId: 'id-X' }), // this retry will get added to the `importIdMap`! createRetry(obj4, { destinationId: 'id-Y', createNewCopy: true }), // this retry will get added to the `importIdMap`! ]; - const options = setupOptions(retries); + const params = { objects, retries, createNewCopies: false }; - const checkOriginConflictsResult = await getImportIdMapForRetries(objects, options); + const checkOriginConflictsResult = await getImportIdMapForRetries(params); expect(checkOriginConflictsResult).toEqual( new Map([ [`${obj3.type}:${obj3.id}`, { id: 'id-X', omitOriginId: false }], @@ -576,9 +574,9 @@ describe('#getImportIdMapForRetries', () => { const obj = createObject('type-1', 'id-1'); const objects = [obj]; const retries = [createRetry(obj, { destinationId: 'id-X' })]; - const options = setupOptions(retries, true); + const params = { objects, retries, createNewCopies: true }; - const checkOriginConflictsResult = await getImportIdMapForRetries(objects, options); + const checkOriginConflictsResult = await getImportIdMapForRetries(params); expect(checkOriginConflictsResult).toEqual( new Map([[`${obj.type}:${obj.id}`, { id: 'id-X', omitOriginId: true }]]) ); diff --git a/src/core/server/saved_objects/import/check_origin_conflicts.ts b/src/core/server/saved_objects/import/check_origin_conflicts.ts index f8f2b5707874f..5dcaf1f09eb48 100644 --- a/src/core/server/saved_objects/import/check_origin_conflicts.ts +++ b/src/core/server/saved_objects/import/check_origin_conflicts.ts @@ -27,7 +27,8 @@ import { } from '../types'; import { ISavedObjectTypeRegistry } from '..'; -interface CheckOriginConflictsOptions { +interface CheckOriginConflictsParams { + objects: Array>; savedObjectsClient: SavedObjectsClientContract; typeRegistry: ISavedObjectTypeRegistry; namespace?: string; @@ -35,7 +36,12 @@ interface CheckOriginConflictsOptions { importIdMap: Map; } -interface GetImportIdMapForRetriesOptions { +type CheckOriginConflictParams = Omit & { + object: SavedObject<{ title?: string }>; +}; + +interface GetImportIdMapForRetriesParams { + objects: SavedObject[]; retries: SavedObjectsImportRetry[]; createNewCopies: boolean; } @@ -85,10 +91,9 @@ const getAmbiguousConflictSourceKey = ({ object }: InexactMatch) => * `checkConflicts` submodule, which is called before this. */ const checkOriginConflict = async ( - object: SavedObject<{ title?: string }>, - options: CheckOriginConflictsOptions + params: CheckOriginConflictParams ): Promise> => { - const { savedObjectsClient, typeRegistry, namespace, importIdMap } = options; + const { object, savedObjectsClient, typeRegistry, namespace, importIdMap } = params; const importIds = new Set(importIdMap.keys()); const { type, originId } = object; @@ -140,13 +145,10 @@ const checkOriginConflict = async ( * will allow `createSavedObjects` to modify the ID before creating the object (thus ensuring a conflict during). * B. Otherwise, this is an "ambiguous conflict" result; return an error. */ -export async function checkOriginConflicts( - objects: Array>, - options: CheckOriginConflictsOptions -) { +export async function checkOriginConflicts({ objects, ...params }: CheckOriginConflictsParams) { // Check each object for possible destination conflicts, ensuring we don't too many concurrent searches running. const mapper = async (object: SavedObject<{ title?: string }>) => - checkOriginConflict(object, options); + checkOriginConflict({ object, ...params }); const checkOriginConflictResults = await pMap(objects, mapper, { concurrency: MAX_CONCURRENT_SEARCHES, }); @@ -176,7 +178,7 @@ export async function checkOriginConflicts( const { type, id, attributes } = object; if (sources.length === 1 && destinations.length === 1) { // This is a simple "inexact match" result -- a single import object has a single destination conflict. - if (options.ignoreRegularConflicts) { + if (params.ignoreRegularConflicts) { importIdMap.set(`${type}:${id}`, { id: destinations[0].id }); filteredObjects.push(object); } else { @@ -224,11 +226,8 @@ export async function checkOriginConflicts( /** * Assume that all objects exist in the `retries` map (due to filtering at the beginning of `resolveSavedObjectsImportErrors`). */ -export function getImportIdMapForRetries( - objects: Array>, - options: GetImportIdMapForRetriesOptions -) { - const { retries, createNewCopies } = options; +export function getImportIdMapForRetries(params: GetImportIdMapForRetriesParams) { + const { objects, retries, createNewCopies } = params; const retryMap = retries.reduce( (acc, cur) => acc.set(`${cur.type}:${cur.id}`, cur), diff --git a/src/core/server/saved_objects/import/create_saved_objects.test.ts b/src/core/server/saved_objects/import/create_saved_objects.test.ts index 2996bd4c3e9fc..192714390f365 100644 --- a/src/core/server/saved_objects/import/create_saved_objects.test.ts +++ b/src/core/server/saved_objects/import/create_saved_objects.test.ts @@ -23,7 +23,7 @@ import { SavedObjectsClientContract, SavedObject, SavedObjectsImportError } from import { SavedObjectsErrorHelpers } from '..'; import { extractErrors } from './extract_errors'; -type CreateSavedObjectsOptions = Parameters[2]; +type CreateSavedObjectsParams = Parameters[0]; /** * Function to create a realistic-looking import object given a type, ID, and optional originId @@ -77,20 +77,19 @@ describe('#createSavedObjects', () => { * Creates an options object to be used as an argument for createSavedObjects * Includes mock savedObjectsClient */ - const setupOptions = ( - options: { - namespace?: string; - overwrite?: boolean; - } = {} - ): CreateSavedObjectsOptions => { - const { namespace, overwrite } = options; + const setupParams = (partial: { + objects: SavedObject[]; + accumulatedErrors?: SavedObjectsImportError[]; + namespace?: string; + overwrite?: boolean; + }): CreateSavedObjectsParams => { savedObjectsClient = savedObjectsClientMock.create(); bulkCreate = savedObjectsClient.bulkCreate; - return { savedObjectsClient, importIdMap, namespace, overwrite }; + return { accumulatedErrors: [], ...partial, savedObjectsClient, importIdMap }; }; const getExpectedBulkCreateArgsObjects = (objects: SavedObject[], retry?: boolean) => - objects.map(({ type, id, attributes, references, originId }) => ({ + objects.map(({ type, id, attributes, originId }) => ({ type, id: retry ? `new-id-for-${id}` : id, // if this was a retry, we regenerated the id -- this is mocked below attributes, @@ -109,7 +108,7 @@ describe('#createSavedObjects', () => { const expectedOptions = expect.any(Object); expect(bulkCreate).toHaveBeenNthCalledWith(n, expectedObjects, expectedOptions); }, - options: (n: number, options: CreateSavedObjectsOptions) => { + options: (n: number, options: CreateSavedObjectsParams) => { const expectedObjects = expect.any(Array); const expectedOptions = { namespace: options.namespace, overwrite: options.overwrite }; expect(bulkCreate).toHaveBeenNthCalledWith(n, expectedObjects, expectedOptions); @@ -119,7 +118,7 @@ describe('#createSavedObjects', () => { const getResultMock = { success: ( { type, id, attributes, references, originId }: SavedObject, - { namespace }: CreateSavedObjectsOptions + { namespace }: CreateSavedObjectsParams ): SavedObject => ({ type, id, @@ -159,16 +158,16 @@ describe('#createSavedObjects', () => { }; test('exits early if there are no objects to create', async () => { - const options = setupOptions(); + const options = setupParams({ objects: [] }); - const createSavedObjectsResult = await createSavedObjects([], [], options); + const createSavedObjectsResult = await createSavedObjects(options); expect(bulkCreate).not.toHaveBeenCalled(); expect(createSavedObjectsResult).toEqual({ createdObjects: [], errors: [] }); }); const objs = [obj1, obj2, obj3, obj4, obj5, obj6, obj7, obj8, obj9, obj10, obj11, obj12, obj13]; - const setupMockResults = (options: CreateSavedObjectsOptions) => { + const setupMockResults = (options: CreateSavedObjectsParams) => { bulkCreate.mockResolvedValue({ saved_objects: [ getResultMock.success(obj1, options), @@ -205,32 +204,32 @@ describe('#createSavedObjects', () => { test('does not call bulkCreate when resolvable errors are present', async () => { for (const error of resolvableErrors) { - const options = setupOptions(); - await createSavedObjects(objs, [error], options); + const options = setupParams({ objects: objs, accumulatedErrors: [error] }); + await createSavedObjects(options); expect(bulkCreate).not.toHaveBeenCalled(); } }); test('calls bulkCreate when unresolvable errors or no errors are present', async () => { for (const error of unresolvableErrors) { - const options = setupOptions(); + const options = setupParams({ objects: objs, accumulatedErrors: [error] }); setupMockResults(options); - await createSavedObjects(objs, [error], options); + await createSavedObjects(options); expect(bulkCreate).toHaveBeenCalledTimes(1); bulkCreate.mockClear(); } - const options = setupOptions(); + const options = setupParams({ objects: objs }); setupMockResults(options); - await createSavedObjects(objs, [], options); + await createSavedObjects(options); expect(bulkCreate).toHaveBeenCalledTimes(1); }); }); const testBulkCreateObjects = async (namespace?: string) => { - const options = setupOptions({ namespace }); + const options = setupParams({ objects: objs, namespace }); setupMockResults(options); - await createSavedObjects(objs, [], options); + await createSavedObjects(options); expect(bulkCreate).toHaveBeenCalledTimes(1); // these three objects are transformed before being created, because they are included in the `importIdMap` const x3 = { ...obj3, id: importId3, originId: undefined }; // this import object already has an originId, but the entry has omitOriginId=true @@ -241,18 +240,18 @@ describe('#createSavedObjects', () => { }; const testBulkCreateOptions = async (namespace?: string) => { const overwrite = (Symbol() as unknown) as boolean; - const options = setupOptions({ namespace, overwrite }); + const options = setupParams({ objects: objs, namespace, overwrite }); setupMockResults(options); - await createSavedObjects(objs, [], options); + await createSavedObjects(options); expect(bulkCreate).toHaveBeenCalledTimes(1); expectBulkCreateArgs.options(1, options); }; const testReturnValue = async (namespace?: string) => { - const options = setupOptions({ namespace }); + const options = setupParams({ objects: objs, namespace }); setupMockResults(options); - const results = await createSavedObjects(objs, [], options); + const results = await createSavedObjects(options); const resultSavedObjects = (await bulkCreate.mock.results[0].value).saved_objects; const [r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13] = resultSavedObjects; // these three results are transformed before being returned, because the bulkCreate attempt used different IDs for them diff --git a/src/core/server/saved_objects/import/create_saved_objects.ts b/src/core/server/saved_objects/import/create_saved_objects.ts index f5e721d9e435c..3fce2b39c148a 100644 --- a/src/core/server/saved_objects/import/create_saved_objects.ts +++ b/src/core/server/saved_objects/import/create_saved_objects.ts @@ -21,7 +21,9 @@ import { SavedObject, SavedObjectsClientContract, SavedObjectsImportError } from import { extractErrors } from './extract_errors'; import { CreatedObject } from './types'; -interface CreateSavedObjectsOptions { +interface CreateSavedObjectsParams { + objects: Array>; + accumulatedErrors: SavedObjectsImportError[]; savedObjectsClient: SavedObjectsClientContract; importIdMap: Map; namespace?: string; @@ -36,18 +38,19 @@ interface CreateSavedObjectsResult { * This function abstracts the bulk creation of import objects. The main reason for this is that the import ID map should dictate the IDs of * the objects we create, and the create results should be mapped to the original IDs that consumers will be able to understand. */ -export const createSavedObjects = async ( - objects: Array>, - accumulatedErrors: SavedObjectsImportError[], - options: CreateSavedObjectsOptions -): Promise> => { +export const createSavedObjects = async ({ + objects, + accumulatedErrors, + savedObjectsClient, + importIdMap, + namespace, + overwrite, +}: CreateSavedObjectsParams): Promise> => { // exit early if there are no objects to create if (objects.length === 0) { return { createdObjects: [], errors: [] }; } - const { savedObjectsClient, importIdMap, namespace, overwrite } = options; - // generate a map of the raw object IDs const objectIdMap = objects.reduce( (map, object) => map.set(`${object.type}:${object.id}`, object), 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 bc9350c0d717d..8d0ea687e9252 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 @@ -156,12 +156,13 @@ describe('#importSavedObjectsFromStream', () => { getMockFn(validateReferences).mockResolvedValue({ errors: [], filteredObjects }); await importSavedObjectsFromStream(options); - const checkConflictsOptions = { + const checkConflictsParams = { + objects: filteredObjects, savedObjectsClient, namespace, ignoreRegularConflicts: overwrite, }; - expect(checkConflicts).toHaveBeenCalledWith(filteredObjects, checkConflictsOptions); + expect(checkConflicts).toHaveBeenCalledWith(checkConflictsParams); }); test('checks origin conflicts', async () => { @@ -175,17 +176,15 @@ describe('#importSavedObjectsFromStream', () => { }); await importSavedObjectsFromStream(options); - const checkOriginConflictsOptions = { + const checkOriginConflictsParams = { + objects: filteredObjects, savedObjectsClient, typeRegistry, namespace, ignoreRegularConflicts: overwrite, importIdMap, }; - expect(checkOriginConflicts).toHaveBeenCalledWith( - filteredObjects, - checkOriginConflictsOptions - ); + expect(checkOriginConflicts).toHaveBeenCalledWith(checkOriginConflictsParams); }); test('creates saved objects', async () => { @@ -222,12 +221,15 @@ describe('#importSavedObjectsFromStream', () => { ['bar', { id: 'newId1' }], ['baz', { id: 'newId2' }], ]); - const createSavedObjectsOptions = { savedObjectsClient, importIdMap, overwrite, namespace }; - expect(createSavedObjects).toHaveBeenCalledWith( - filteredObjects, - errors, - createSavedObjectsOptions - ); + const createSavedObjectsParams = { + objects: filteredObjects, + accumulatedErrors: errors, + savedObjectsClient, + importIdMap, + overwrite, + namespace, + }; + expect(createSavedObjects).toHaveBeenCalledWith(createSavedObjectsParams); }); }); @@ -273,12 +275,15 @@ describe('#importSavedObjectsFromStream', () => { getMockFn(regenerateIds).mockReturnValue(importIdMap); await importSavedObjectsFromStream(options); - const createSavedObjectsOptions = { savedObjectsClient, importIdMap, overwrite, namespace }; - expect(createSavedObjects).toHaveBeenCalledWith( - filteredObjects, - errors, - createSavedObjectsOptions - ); + const createSavedObjectsParams = { + objects: filteredObjects, + accumulatedErrors: errors, + savedObjectsClient, + importIdMap, + overwrite, + namespace, + }; + expect(createSavedObjects).toHaveBeenCalledWith(createSavedObjectsParams); }); }); }); 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 3982c67d24a28..08a3c80ba947c 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.ts @@ -70,42 +70,41 @@ export async function importSavedObjectsFromStream({ importIdMap = regenerateIds(collectSavedObjectsResult.collectedObjects); } else { // Check single-namespace objects for conflicts in this namespace, and check multi-namespace objects for conflicts across all namespaces - const checkConflictsOptions = { + const checkConflictsParams = { + objects: validateReferencesResult.filteredObjects, savedObjectsClient, namespace, ignoreRegularConflicts: overwrite, }; - const checkConflictsResult = await checkConflicts( - validateReferencesResult.filteredObjects, - checkConflictsOptions - ); + const checkConflictsResult = await checkConflicts(checkConflictsParams); errorAccumulator = [...errorAccumulator, ...checkConflictsResult.errors]; importIdMap = new Map([...importIdMap, ...checkConflictsResult.importIdMap]); // Check multi-namespace object types for origin conflicts in this namespace - const checkOriginConflictsOptions = { + const checkOriginConflictsParams = { + objects: checkConflictsResult.filteredObjects, savedObjectsClient, typeRegistry, namespace, ignoreRegularConflicts: overwrite, importIdMap, }; - const checkOriginConflictsResult = await checkOriginConflicts( - checkConflictsResult.filteredObjects, - checkOriginConflictsOptions - ); + const checkOriginConflictsResult = await checkOriginConflicts(checkOriginConflictsParams); errorAccumulator = [...errorAccumulator, ...checkOriginConflictsResult.errors]; importIdMap = new Map([...importIdMap, ...checkOriginConflictsResult.importIdMap]); objectsToCreate = checkOriginConflictsResult.filteredObjects; } // Create objects in bulk - const createSavedObjectsOptions = { savedObjectsClient, importIdMap, overwrite, namespace }; - const createSavedObjectsResult = await createSavedObjects( - objectsToCreate, - errorAccumulator, - createSavedObjectsOptions - ); + const createSavedObjectsParams = { + objects: objectsToCreate, + accumulatedErrors: errorAccumulator, + savedObjectsClient, + importIdMap, + overwrite, + namespace, + }; + const createSavedObjectsResult = await createSavedObjects(createSavedObjectsParams); errorAccumulator = [...errorAccumulator, ...createSavedObjectsResult.errors]; const successResults = createSavedObjectsResult.createdObjects.map( 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 eb35dc3e9b31f..f5dba716ea348 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 @@ -209,13 +209,14 @@ describe('#importSavedObjectsFromStream', () => { getMockFn(validateReferences).mockResolvedValue({ errors: [], filteredObjects }); await resolveSavedObjectsImportErrors(options); - const checkConflictsOptions = { + const checkConflictsParams = { + objects: filteredObjects, savedObjectsClient, namespace, ignoreRegularConflicts: true, createNewCopies, }; - expect(checkConflicts).toHaveBeenCalledWith(filteredObjects, checkConflictsOptions); + expect(checkConflicts).toHaveBeenCalledWith(checkConflictsParams); }); test('gets import ID map for retries', async () => { @@ -230,8 +231,8 @@ describe('#importSavedObjectsFromStream', () => { }); await resolveSavedObjectsImportErrors(options); - const opts = { retries, createNewCopies }; - expect(getImportIdMapForRetries).toHaveBeenCalledWith(filteredObjects, opts); + const getImportIdMapForRetriesParams = { objects: filteredObjects, retries, createNewCopies }; + expect(getImportIdMapForRetries).toHaveBeenCalledWith(getImportIdMapForRetriesParams); }); test('splits objects to ovewrite from those not to overwrite', async () => { @@ -296,17 +297,21 @@ describe('#importSavedObjectsFromStream', () => { }); await resolveSavedObjectsImportErrors(options); - const createSavedObjectsOptions = { savedObjectsClient, importIdMap, namespace }; - expect(createSavedObjects).toHaveBeenNthCalledWith(1, objectsToOverwrite, errors, { - ...createSavedObjectsOptions, + const partialCreateSavedObjectsParams = { + accumulatedErrors: errors, + savedObjectsClient, + importIdMap, + namespace, + }; + expect(createSavedObjects).toHaveBeenNthCalledWith(1, { + ...partialCreateSavedObjectsParams, + objects: objectsToOverwrite, overwrite: true, }); - expect(createSavedObjects).toHaveBeenNthCalledWith( - 2, - objectsToNotOverwrite, - errors, - createSavedObjectsOptions - ); + expect(createSavedObjects).toHaveBeenNthCalledWith(2, { + ...partialCreateSavedObjectsParams, + objects: objectsToNotOverwrite, + }); }); }); @@ -366,17 +371,21 @@ describe('#importSavedObjectsFromStream', () => { }); await resolveSavedObjectsImportErrors(options); - const createSavedObjectsOptions = { savedObjectsClient, importIdMap, namespace }; - expect(createSavedObjects).toHaveBeenNthCalledWith(1, objectsToOverwrite, errors, { - ...createSavedObjectsOptions, + const partialCreateSavedObjectsParams = { + accumulatedErrors: errors, + savedObjectsClient, + importIdMap, + namespace, + }; + expect(createSavedObjects).toHaveBeenNthCalledWith(1, { + ...partialCreateSavedObjectsParams, + objects: objectsToOverwrite, overwrite: true, }); - expect(createSavedObjects).toHaveBeenNthCalledWith( - 2, - objectsToNotOverwrite, - errors, - createSavedObjectsOptions - ); + expect(createSavedObjects).toHaveBeenNthCalledWith(2, { + ...partialCreateSavedObjectsParams, + objects: objectsToNotOverwrite, + }); }); }); }); 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 be7a457fdce75..7dc45a929fbe4 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.ts @@ -106,35 +106,40 @@ export async function resolveSavedObjectsImportErrors({ } // Check single-namespace objects for conflicts in this namespace, and check multi-namespace objects for conflicts across all namespaces - const checkConflictsOptions = { + const checkConflictsParams = { + objects: validateReferencesResult.filteredObjects, savedObjectsClient, namespace, ignoreRegularConflicts: true, createNewCopies, }; - const checkConflictsResult = await checkConflicts( - validateReferencesResult.filteredObjects, - checkConflictsOptions - ); + const checkConflictsResult = await checkConflicts(checkConflictsParams); errorAccumulator = [...errorAccumulator, ...checkConflictsResult.errors]; importIdMap = new Map([...importIdMap, ...checkConflictsResult.importIdMap]); // Check multi-namespace object types for regular conflicts and ambiguous conflicts - const importIdMapForRetries = getImportIdMapForRetries(checkConflictsResult.filteredObjects, { + const getImportIdMapForRetriesParams = { + objects: checkConflictsResult.filteredObjects, retries, createNewCopies, - }); + }; + const importIdMapForRetries = getImportIdMapForRetries(getImportIdMapForRetriesParams); importIdMap = new Map([...importIdMap, ...importIdMapForRetries]); // Bulk create in two batches, overwrites and non-overwrites let successResults: Array<{ type: string; id: string; destinationId?: string }> = []; const accumulatedErrors = [...errorAccumulator]; const bulkCreateObjects = async (objects: Array>, overwrite?: boolean) => { - const options = { savedObjectsClient, importIdMap, namespace, overwrite }; - const { createdObjects, errors: bulkCreateErrors } = await createSavedObjects( + const createSavedObjectsParams = { objects, accumulatedErrors, - options + savedObjectsClient, + importIdMap, + namespace, + overwrite, + }; + const { createdObjects, errors: bulkCreateErrors } = await createSavedObjects( + createSavedObjectsParams ); errorAccumulator = [...errorAccumulator, ...bulkCreateErrors]; successCount += createdObjects.length; From 6a67e37d11aa7ed5d58fd9a652616f1d0f4b8ecd Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Wed, 15 Jul 2020 15:23:37 -0400 Subject: [PATCH 53/55] `@ts-ignore` -> `@ts-expect-error` --- .../spaces_saved_objects_client.test.ts | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts index 0aee617c14a00..33adcb6ca0e5f 100644 --- a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts +++ b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts @@ -90,7 +90,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; const type = Symbol(); const id = Symbol(); const options = Object.freeze({ foo: 'bar' }); - // @ts-ignore + // @ts-expect-error const actualReturnValue = await client.get(type, id, options); expect(actualReturnValue).toBe(expectedReturnValue); @@ -117,7 +117,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; const objects = [{ type: 'foo' }]; const options = Object.freeze({ foo: 'bar' }); - // @ts-ignore + // @ts-expect-error const actualReturnValue = await client.bulkGet(objects, options); expect(actualReturnValue).toBe(expectedReturnValue); @@ -268,7 +268,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; const { client } = await createSpacesSavedObjectsClient(); await expect( - // @ts-ignore + // @ts-expect-error client.checkConflicts(null, { namespace: 'bar' }) ).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED); }); @@ -280,7 +280,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; const objects = Symbol(); const options = Object.freeze({ foo: 'bar' }); - // @ts-ignore + // @ts-expect-error const actualReturnValue = await client.checkConflicts(objects, options); expect(actualReturnValue).toBe(expectedReturnValue); @@ -308,7 +308,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; const type = Symbol(); const attributes = Symbol(); const options = Object.freeze({ foo: 'bar' }); - // @ts-ignore + // @ts-expect-error const actualReturnValue = await client.create(type, attributes, options); expect(actualReturnValue).toBe(expectedReturnValue); @@ -335,7 +335,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; const objects = [{ type: 'foo' }]; const options = Object.freeze({ foo: 'bar' }); - // @ts-ignore + // @ts-expect-error const actualReturnValue = await client.bulkCreate(objects, options); expect(actualReturnValue).toBe(expectedReturnValue); @@ -351,7 +351,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; const { client } = await createSpacesSavedObjectsClient(); await expect( - // @ts-ignore + // @ts-expect-error client.update(null, null, null, { namespace: 'bar' }) ).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED); }); @@ -365,7 +365,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; const id = Symbol(); const attributes = Symbol(); const options = Object.freeze({ foo: 'bar' }); - // @ts-ignore + // @ts-expect-error const actualReturnValue = await client.update(type, id, attributes, options); expect(actualReturnValue).toBe(expectedReturnValue); @@ -381,7 +381,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; const { client } = await createSpacesSavedObjectsClient(); await expect( - // @ts-ignore + // @ts-expect-error client.bulkUpdate(null, { namespace: 'bar' }) ).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED); }); @@ -415,7 +415,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; const { client } = await createSpacesSavedObjectsClient(); await expect( - // @ts-ignore + // @ts-expect-error client.delete(null, null, { namespace: 'bar' }) ).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED); }); @@ -428,7 +428,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; const type = Symbol(); const id = Symbol(); const options = Object.freeze({ foo: 'bar' }); - // @ts-ignore + // @ts-expect-error const actualReturnValue = await client.delete(type, id, options); expect(actualReturnValue).toBe(expectedReturnValue); @@ -444,7 +444,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; const { client } = await createSpacesSavedObjectsClient(); await expect( - // @ts-ignore + // @ts-expect-error client.addToNamespaces(null, null, null, { namespace: 'bar' }) ).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED); }); @@ -458,7 +458,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; const id = Symbol(); const namespaces = Symbol(); const options = Object.freeze({ foo: 'bar' }); - // @ts-ignore + // @ts-expect-error const actualReturnValue = await client.addToNamespaces(type, id, namespaces, options); expect(actualReturnValue).toBe(expectedReturnValue); @@ -474,7 +474,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; const { client } = await createSpacesSavedObjectsClient(); await expect( - // @ts-ignore + // @ts-expect-error client.deleteFromNamespaces(null, null, null, { namespace: 'bar' }) ).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED); }); @@ -488,7 +488,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; const id = Symbol(); const namespaces = Symbol(); const options = Object.freeze({ foo: 'bar' }); - // @ts-ignore + // @ts-expect-error const actualReturnValue = await client.deleteFromNamespaces(type, id, namespaces, options); expect(actualReturnValue).toBe(expectedReturnValue); From 9d7b755aa8ec84532f1302508a5a79ee6d47ff70 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Thu, 16 Jul 2020 09:36:06 -0400 Subject: [PATCH 54/55] Address fifth round of feedback --- docs/api/saved-objects/import.asciidoc | 16 +++++++------- .../copy_saved_objects.asciidoc | 6 +++--- .../core/public/kibana-plugin-core-public.md | 1 + ...ugin-core-public.savedobjecterror.error.md | 11 ++++++++++ ...ana-plugin-core-public.savedobjecterror.md | 21 +++++++++++++++++++ ...in-core-public.savedobjecterror.message.md | 11 ++++++++++ ...n-core-public.savedobjecterror.metadata.md | 11 ++++++++++ ...core-public.savedobjecterror.statuscode.md | 11 ++++++++++ src/core/public/index.ts | 1 + src/core/public/public.api.md | 16 ++++++++++++-- src/core/public/saved_objects/index.ts | 1 + .../get_sorted_objects_for_export.test.ts | 14 ++++++------- 12 files changed, 100 insertions(+), 20 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjecterror.error.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjecterror.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjecterror.message.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjecterror.metadata.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjecterror.statuscode.md diff --git a/docs/api/saved-objects/import.asciidoc b/docs/api/saved-objects/import.asciidoc index 2a1310b401c00..634b800a60428 100644 --- a/docs/api/saved-objects/import.asciidoc +++ b/docs/api/saved-objects/import.asciidoc @@ -231,15 +231,16 @@ The API returns the following: } -------------------------------------------------- -This result indicates that the import was not successful because the index pattern, visualization, and dashboard each resulted in a conflict error: +This result indicates that the import was not successful because the index pattern, visualization, and dashboard each resulted in a conflict +error: * An index pattern with the same ID already exists, so this resulted in a conflict error. This can be resolved by overwriting the existing object, or skipping this object entirely. * A visualization with a different ID but the same "origin" already exists, so this resulted in a conflict error as well. The -`destinationId` field describes to the other visualization which caused this conflict. This behavior was added to ensure that new objects -which can be shared between <> behave in a similar way as legacy non-shareable objects. When a shareable object is -exported and then imported into a new space, it retains its origin so that it conflicts will be encountered as expected. This can be +`destinationId` field contains the `id` of the other visualization which caused this conflict. This behavior was added to ensure that new +objects which can be shared between <> behave in a similar way as legacy non-shareable objects. When a shareable object +is exported and then imported into a new space, it retains its origin so that its conflicts will be encountered as expected. This can be resolved by overwriting the specified destination object, or skipping this object entirely. * Two canvases with different IDs but the same "origin" already exist, so this resulted in an ambiguous conflict error. The `destinations` @@ -247,7 +248,8 @@ array describes to the other canvases which caused this conflict. When a shareab and is _then_ shared to another space where an object of the same origin exists, this situation may occur. This can be resolved by picking one of the destination objects to overwrite, or skipping this object entirely. -These errors need to be addressed using the <>. +No objects will be imported until this error is resolved using the <>. [[saved-objects-api-import-example-4]] ===== 4. Failed import (with missing reference errors) @@ -298,5 +300,5 @@ The API returns the following: ] -------------------------------------------------- -This result indicates that the import was not successful because the visualization resulted in a missing references error. This error needs -to be addressed using the <>. +This result indicates that the import was not successful because the visualization resulted in a missing references error. No objects will +be imported until this error is resolved using the <>. diff --git a/docs/api/spaces-management/copy_saved_objects.asciidoc b/docs/api/spaces-management/copy_saved_objects.asciidoc index 7e52b676b51fe..323f0f1805575 100644 --- a/docs/api/spaces-management/copy_saved_objects.asciidoc +++ b/docs/api/spaces-management/copy_saved_objects.asciidoc @@ -317,9 +317,9 @@ index pattern, visualization, and dashboard each resulted in a conflict error: object, or skipping this object entirely. * A visualization with a different ID but the same "origin" already exists, so this resulted in a conflict error as well. The -`destinationId` field describes to the other visualization which caused this conflict. This behavior was added to ensure that new objects -which can be shared between spaces behave in a similar way as legacy non-shareable objects. When a shareable object is copied into a new -space, it retains its origin so that it conflicts will be encountered as expected. This can be resolved by overwriting the specified +`destinationId` field contains the `id` of the other visualization which caused this conflict. This behavior was added to ensure that new +objects which can be shared between spaces behave in a similar way as legacy non-shareable objects. When a shareable object is copied into a +new space, it retains its origin so that its conflicts will be encountered as expected. This can be resolved by overwriting the specified destination object, or skipping this object entirely. * Two canvases with different IDs but the same "origin" already exist, so this resulted in an ambiguous conflict error. The `destinations` diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index 3d4f83760ba72..b9533a66fa7b5 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -105,6 +105,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginInitializerContext](./kibana-plugin-core-public.plugininitializercontext.md) | The available core services passed to a PluginInitializer | | [SavedObject](./kibana-plugin-core-public.savedobject.md) | | | [SavedObjectAttributes](./kibana-plugin-core-public.savedobjectattributes.md) | The data for a Saved Object is stored as an object in the attributes property. | +| [SavedObjectError](./kibana-plugin-core-public.savedobjecterror.md) | | | [SavedObjectReference](./kibana-plugin-core-public.savedobjectreference.md) | A reference to another saved object. | | [SavedObjectsBaseOptions](./kibana-plugin-core-public.savedobjectsbaseoptions.md) | | | [SavedObjectsBatchResponse](./kibana-plugin-core-public.savedobjectsbatchresponse.md) | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjecterror.error.md b/docs/development/core/public/kibana-plugin-core-public.savedobjecterror.error.md new file mode 100644 index 0000000000000..87180a520090f --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjecterror.error.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectError](./kibana-plugin-core-public.savedobjecterror.md) > [error](./kibana-plugin-core-public.savedobjecterror.error.md) + +## SavedObjectError.error property + +Signature: + +```typescript +error: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjecterror.md b/docs/development/core/public/kibana-plugin-core-public.savedobjecterror.md new file mode 100644 index 0000000000000..2117cea433b5c --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjecterror.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectError](./kibana-plugin-core-public.savedobjecterror.md) + +## SavedObjectError interface + +Signature: + +```typescript +export interface SavedObjectError +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [error](./kibana-plugin-core-public.savedobjecterror.error.md) | string | | +| [message](./kibana-plugin-core-public.savedobjecterror.message.md) | string | | +| [metadata](./kibana-plugin-core-public.savedobjecterror.metadata.md) | Record<string, unknown> | | +| [statusCode](./kibana-plugin-core-public.savedobjecterror.statuscode.md) | number | | + diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjecterror.message.md b/docs/development/core/public/kibana-plugin-core-public.savedobjecterror.message.md new file mode 100644 index 0000000000000..2a51d4d1a514d --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjecterror.message.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectError](./kibana-plugin-core-public.savedobjecterror.md) > [message](./kibana-plugin-core-public.savedobjecterror.message.md) + +## SavedObjectError.message property + +Signature: + +```typescript +message: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjecterror.metadata.md b/docs/development/core/public/kibana-plugin-core-public.savedobjecterror.metadata.md new file mode 100644 index 0000000000000..a2725f0206655 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjecterror.metadata.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectError](./kibana-plugin-core-public.savedobjecterror.md) > [metadata](./kibana-plugin-core-public.savedobjecterror.metadata.md) + +## SavedObjectError.metadata property + +Signature: + +```typescript +metadata?: Record; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjecterror.statuscode.md b/docs/development/core/public/kibana-plugin-core-public.savedobjecterror.statuscode.md new file mode 100644 index 0000000000000..75a57e98fece2 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjecterror.statuscode.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectError](./kibana-plugin-core-public.savedobjecterror.md) > [statusCode](./kibana-plugin-core-public.savedobjecterror.statuscode.md) + +## SavedObjectError.statusCode property + +Signature: + +```typescript +statusCode: number; +``` diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 65bb12d664b72..77553d76050b2 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -140,6 +140,7 @@ export { SavedObjectAttribute, SavedObjectAttributes, SavedObjectAttributeSingle, + SavedObjectError, SavedObjectReference, SavedObjectsBaseOptions, SavedObjectsFindOptions, diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 035ae50305aba..e8a28c897b014 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -1170,8 +1170,6 @@ export type PublicUiSettingsParams = Omit; // @public (undocumented) export interface SavedObject { attributes: T; - // Warning: (ae-forgotten-export) The symbol "SavedObjectError" needs to be exported by the entry point index.d.ts - // // (undocumented) error?: SavedObjectError; id: string; @@ -1196,6 +1194,20 @@ export interface SavedObjectAttributes { // @public export type SavedObjectAttributeSingle = string | number | boolean | null | undefined | SavedObjectAttributes; +// Warning: (ae-missing-release-tag) "SavedObjectError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface SavedObjectError { + // (undocumented) + error: string; + // (undocumented) + message: string; + // (undocumented) + metadata?: Record; + // (undocumented) + statusCode: number; +} + // @public export interface SavedObjectReference { // (undocumented) diff --git a/src/core/public/saved_objects/index.ts b/src/core/public/saved_objects/index.ts index e5e2d2c326af3..f49a5e0a8a5c4 100644 --- a/src/core/public/saved_objects/index.ts +++ b/src/core/public/saved_objects/index.ts @@ -51,5 +51,6 @@ export { SavedObjectAttribute, SavedObjectAttributes, SavedObjectAttributeSingle, + SavedObjectError, SavedObjectReference, } from '../../types'; 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 aa0bf0bae938d..85b3a281aef7f 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 @@ -684,14 +684,12 @@ describe('getSortedObjectsForExport()', () => { ], }); const response = await readStreamToCompletion(exportStream); - expect(response).toEqual( - expect.arrayContaining([ - expect.not.objectContaining({ id: '1', namespaces: expect.anything() }), - expect.not.objectContaining({ id: '2', namespaces: expect.anything() }), - expect.not.objectContaining({ id: '3', namespaces: expect.anything() }), - expect.objectContaining({ exportedCount: 3 }), - ]) - ); + expect(response).toEqual([ + createSavedObject({ type: 'multi', id: '1' }), + createSavedObject({ type: 'multi', id: '2' }), + createSavedObject({ type: 'other', id: '3' }), + expect.objectContaining({ exportedCount: 3 }), + ]); }); test('includes nested dependencies when passed in', async () => { From 077d52b76baf82928ef79e715cd44a8733830482 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Fri, 31 Jul 2020 07:41:13 -0400 Subject: [PATCH 55/55] Fix type check --- test/api_integration/apis/saved_objects/migrations.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/api_integration/apis/saved_objects/migrations.ts b/test/api_integration/apis/saved_objects/migrations.ts index dc3a41c88652f..9997d9710e212 100644 --- a/test/api_integration/apis/saved_objects/migrations.ts +++ b/test/api_integration/apis/saved_objects/migrations.ts @@ -320,8 +320,7 @@ export default ({ getService }: FtrProviderContext) => { const { body } = await esClient.cat.indices({ index: '.migration-c*', format: 'json' }); // It only created the original and the dest - const indices = body.map((entry) => entry.index).sort(); - assert.deepEqual(indices, ['.migration-c_1', '.migration-c_2']); + expect(_.map(body, 'index').sort()).to.eql(['.migration-c_1', '.migration-c_2']); // The docs in the original index are unchanged expect(await fetchDocs(esClient, `${index}_1`)).to.eql([