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 5a597045f3b07..72011eee0e91b 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 @@ -29,6 +29,7 @@ import { ISavedObjectTypeRegistry } from '..'; import { typeRegistryMock } from '../saved_objects_type_registry.mock'; import { importSavedObjectsFromStream, ImportSavedObjectsOptions } from './import_saved_objects'; import { SavedObjectsImportHook, SavedObjectsImportWarning } from './types'; +import type { ImportStateMap } from './lib'; describe('#importSavedObjectsFromStream', () => { beforeEach(() => { @@ -37,19 +38,19 @@ describe('#importSavedObjectsFromStream', () => { mockCollectSavedObjects.mockResolvedValue({ errors: [], collectedObjects: [], - importIdMap: new Map(), + importStateMap: new Map(), }); mockRegenerateIds.mockReturnValue(new Map()); mockValidateReferences.mockResolvedValue([]); mockCheckConflicts.mockResolvedValue({ errors: [], filteredObjects: [], - importIdMap: new Map(), + importStateMap: new Map(), pendingOverwrites: new Set(), }); mockCheckOriginConflicts.mockResolvedValue({ errors: [], - importIdMap: new Map(), + importStateMap: new Map(), pendingOverwrites: new Set(), }); mockCreateSavedObjects.mockResolvedValue({ errors: [], createdObjects: [] }); @@ -141,7 +142,7 @@ describe('#importSavedObjectsFromStream', () => { mockCollectSavedObjects.mockResolvedValue({ errors: [], collectedObjects, - importIdMap: new Map(), + importStateMap: new Map(), }); await importSavedObjectsFromStream(options); @@ -162,7 +163,7 @@ describe('#importSavedObjectsFromStream', () => { mockCollectSavedObjects.mockResolvedValue({ errors: [], collectedObjects, - importIdMap: new Map(), + importStateMap: new Map(), }); mockCreateSavedObjects.mockResolvedValue({ errors: [], @@ -184,7 +185,7 @@ describe('#importSavedObjectsFromStream', () => { mockCollectSavedObjects.mockResolvedValue({ errors: [], collectedObjects, - importIdMap: new Map(), + importStateMap: new Map(), }); await importSavedObjectsFromStream(options); @@ -197,7 +198,7 @@ describe('#importSavedObjectsFromStream', () => { mockCollectSavedObjects.mockResolvedValue({ errors: [], collectedObjects, - importIdMap: new Map(), + importStateMap: new Map(), }); await importSavedObjectsFromStream(options); @@ -213,11 +214,11 @@ describe('#importSavedObjectsFromStream', () => { test('checks origin conflicts', async () => { const options = setupOptions(); const filteredObjects = [createObject()]; - const importIdMap = new Map(); + const importStateMap = new Map(); mockCheckConflicts.mockResolvedValue({ errors: [], filteredObjects, - importIdMap, + importStateMap, pendingOverwrites: new Set(), }); @@ -228,7 +229,7 @@ describe('#importSavedObjectsFromStream', () => { typeRegistry, namespace, ignoreRegularConflicts: overwrite, - importIdMap, + importStateMap, }; expect(mockCheckOriginConflicts).toHaveBeenCalledWith(checkOriginConflictsParams); }); @@ -241,7 +242,7 @@ describe('#importSavedObjectsFromStream', () => { mockCollectSavedObjects.mockResolvedValue({ errors: [errors[0]], collectedObjects, - importIdMap: new Map([ + importStateMap: new Map([ ['foo', {}], ['bar', {}], ['baz', {}], @@ -251,26 +252,26 @@ describe('#importSavedObjectsFromStream', () => { mockCheckConflicts.mockResolvedValue({ errors: [errors[2]], filteredObjects, - importIdMap: new Map([['bar', { id: 'newId1' }]]), + importStateMap: new Map([['bar', { destinationId: 'newId1' }]]), pendingOverwrites: new Set(), }); mockCheckOriginConflicts.mockResolvedValue({ errors: [errors[3]], - importIdMap: new Map([['baz', { id: 'newId2' }]]), + importStateMap: new Map([['baz', { destinationId: 'newId2' }]]), pendingOverwrites: new Set(), }); await importSavedObjectsFromStream(options); - const importIdMap = new Map([ + const importStateMap = new Map([ ['foo', {}], - ['bar', { id: 'newId1' }], - ['baz', { id: 'newId2' }], + ['bar', { destinationId: 'newId1' }], + ['baz', { destinationId: 'newId2' }], ]); const createSavedObjectsParams = { objects: collectedObjects, accumulatedErrors: errors, savedObjectsClient, - importIdMap, + importStateMap, overwrite, namespace, }; @@ -285,7 +286,7 @@ describe('#importSavedObjectsFromStream', () => { mockCollectSavedObjects.mockResolvedValue({ errors: [], collectedObjects, - importIdMap: new Map(), // doesn't matter + importStateMap: new Map(), // doesn't matter }); await importSavedObjectsFromStream(options); @@ -308,22 +309,22 @@ describe('#importSavedObjectsFromStream', () => { mockCollectSavedObjects.mockResolvedValue({ errors: [errors[0]], collectedObjects, - importIdMap: new Map([ + importStateMap: new Map([ ['foo', {}], ['bar', {}], ]), }); mockValidateReferences.mockResolvedValue([errors[1]]); - // this importIdMap is not composed with the one obtained from `collectSavedObjects` - const importIdMap = new Map().set(`id1`, { id: `newId1` }); - mockRegenerateIds.mockReturnValue(importIdMap); + // this importStateMap is not composed with the one obtained from `collectSavedObjects` + const importStateMap: ImportStateMap = new Map().set(`id1`, { destinationId: `newId1` }); + mockRegenerateIds.mockReturnValue(importStateMap); await importSavedObjectsFromStream(options); const createSavedObjectsParams = { objects: collectedObjects, accumulatedErrors: errors, savedObjectsClient, - importIdMap, + importStateMap, overwrite, namespace, }; @@ -345,7 +346,7 @@ describe('#importSavedObjectsFromStream', () => { mockCollectSavedObjects.mockResolvedValue({ errors: [createError()], collectedObjects: [], - importIdMap: new Map(), // doesn't matter + importStateMap: new Map(), // doesn't matter }); const result = await importSavedObjectsFromStream(options); @@ -363,7 +364,7 @@ describe('#importSavedObjectsFromStream', () => { mockCollectSavedObjects.mockResolvedValue({ errors: [], collectedObjects, - importIdMap: new Map(), + importStateMap: new Map(), }); mockCreateSavedObjects.mockResolvedValue({ errors: [], @@ -411,7 +412,7 @@ describe('#importSavedObjectsFromStream', () => { mockCheckConflicts.mockResolvedValue({ errors: [], filteredObjects: [], - importIdMap: new Map(), + importStateMap: new Map(), pendingOverwrites: new Set([ `${success2.type}:${success2.id}`, // the success2 object was overwritten `${error2.type}:${error2.id}`, // an attempt was made to overwrite the error2 object @@ -487,7 +488,7 @@ describe('#importSavedObjectsFromStream', () => { mockCheckConflicts.mockResolvedValue({ errors: [], filteredObjects: [], - importIdMap: new Map(), + importStateMap: new Map(), pendingOverwrites: new Set(), }); mockCreateSavedObjects.mockResolvedValue({ errors: [], createdObjects: [obj1, obj2] }); @@ -521,18 +522,18 @@ describe('#importSavedObjectsFromStream', () => { mockCollectSavedObjects.mockResolvedValue({ errors: [errors[0]], collectedObjects: [], - importIdMap: new Map(), // doesn't matter + importStateMap: new Map(), // doesn't matter }); mockValidateReferences.mockResolvedValue([errors[1]]); mockCheckConflicts.mockResolvedValue({ errors: [errors[2]], filteredObjects: [], - importIdMap: new Map(), // doesn't matter + importStateMap: new Map(), // doesn't matter pendingOverwrites: new Set(), }); mockCheckOriginConflicts.mockResolvedValue({ errors: [errors[3]], - importIdMap: new Map(), // doesn't matter + importStateMap: new Map(), // doesn't matter pendingOverwrites: new Set(), }); mockCreateSavedObjects.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 4fc8f04a40270..851d36672b58c 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.ts @@ -73,7 +73,7 @@ export async function importSavedObjectsFromStream({ }); 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; + let importStateMap = collectSavedObjectsResult.importStateMap; let pendingOverwrites = new Set(); // Validate references @@ -85,7 +85,7 @@ export async function importSavedObjectsFromStream({ errorAccumulator = [...errorAccumulator, ...validateReferencesResult]; if (createNewCopies) { - importIdMap = regenerateIds(collectSavedObjectsResult.collectedObjects); + importStateMap = regenerateIds(collectSavedObjectsResult.collectedObjects); } else { // Check single-namespace objects for conflicts in this namespace, and check multi-namespace objects for conflicts across all namespaces const checkConflictsParams = { @@ -96,7 +96,7 @@ export async function importSavedObjectsFromStream({ }; const checkConflictsResult = await checkConflicts(checkConflictsParams); errorAccumulator = [...errorAccumulator, ...checkConflictsResult.errors]; - importIdMap = new Map([...importIdMap, ...checkConflictsResult.importIdMap]); + importStateMap = new Map([...importStateMap, ...checkConflictsResult.importStateMap]); pendingOverwrites = checkConflictsResult.pendingOverwrites; // Check multi-namespace object types for origin conflicts in this namespace @@ -106,11 +106,11 @@ export async function importSavedObjectsFromStream({ typeRegistry, namespace, ignoreRegularConflicts: overwrite, - importIdMap, + importStateMap, }; const checkOriginConflictsResult = await checkOriginConflicts(checkOriginConflictsParams); errorAccumulator = [...errorAccumulator, ...checkOriginConflictsResult.errors]; - importIdMap = new Map([...importIdMap, ...checkOriginConflictsResult.importIdMap]); + importStateMap = new Map([...importStateMap, ...checkOriginConflictsResult.importStateMap]); pendingOverwrites = new Set([ ...pendingOverwrites, ...checkOriginConflictsResult.pendingOverwrites, @@ -122,7 +122,7 @@ export async function importSavedObjectsFromStream({ objects: collectSavedObjectsResult.collectedObjects, accumulatedErrors: errorAccumulator, savedObjectsClient, - importIdMap, + importStateMap, overwrite, namespace, }; diff --git a/src/core/server/saved_objects/import/lib/check_conflicts.test.ts b/src/core/server/saved_objects/import/lib/check_conflicts.test.ts index b1adf0f574db6..b2de6f11d5cb8 100644 --- a/src/core/server/saved_objects/import/lib/check_conflicts.test.ts +++ b/src/core/server/saved_objects/import/lib/check_conflicts.test.ts @@ -83,7 +83,7 @@ describe('#checkConflicts', () => { expect(checkConflictsResult).toEqual({ filteredObjects: [], errors: [], - importIdMap: new Map(), + importStateMap: new Map(), pendingOverwrites: new Set(), }); }); @@ -119,7 +119,7 @@ describe('#checkConflicts', () => { error: { ...obj4Error.error, type: 'unknown' }, }, ], - importIdMap: new Map([[`${obj3.type}:${obj3.id}`, { id: 'uuidv4' }]]), + importStateMap: new Map([[`${obj3.type}:${obj3.id}`, { destinationId: 'uuidv4' }]]), pendingOverwrites: new Set(), }); }); @@ -185,12 +185,14 @@ describe('#checkConflicts', () => { error: { ...obj4Error.error, type: 'unknown' }, }, ], - importIdMap: new Map([[`${obj3.type}:${obj3.id}`, { id: 'uuidv4', omitOriginId: true }]]), + importStateMap: new Map([ + [`${obj3.type}:${obj3.id}`, { destinationId: 'uuidv4', omitOriginId: true }], + ]), pendingOverwrites: new Set([`${obj5.type}:${obj5.id}`]), }); }); - it('adds `omitOriginId` field to `importIdMap` entries when createNewCopies=true', async () => { + it('adds `omitOriginId` field to `importStateMap` entries when createNewCopies=true', async () => { const namespace = 'foo-namespace'; const params = setupParams({ objects, namespace, createNewCopies: true }); socCheckConflicts.mockResolvedValue({ errors: [obj2Error, obj3Error, obj4Error] }); @@ -198,7 +200,9 @@ describe('#checkConflicts', () => { const checkConflictsResult = await checkConflicts(params); expect(checkConflictsResult).toEqual( expect.objectContaining({ - importIdMap: new Map([[`${obj3.type}:${obj3.id}`, { id: 'uuidv4', omitOriginId: true }]]), + importStateMap: new Map([ + [`${obj3.type}:${obj3.id}`, { destinationId: 'uuidv4', omitOriginId: true }], + ]), }) ); }); diff --git a/src/core/server/saved_objects/import/lib/check_conflicts.ts b/src/core/server/saved_objects/import/lib/check_conflicts.ts index d5e37f21fc84a..c15c4302491b4 100644 --- a/src/core/server/saved_objects/import/lib/check_conflicts.ts +++ b/src/core/server/saved_objects/import/lib/check_conflicts.ts @@ -14,6 +14,7 @@ import { SavedObjectError, SavedObjectsImportRetry, } from '../../types'; +import type { ImportStateMap } from './types'; interface CheckConflictsParams { objects: Array>; @@ -37,12 +38,12 @@ export async function checkConflicts({ }: CheckConflictsParams) { const filteredObjects: Array> = []; const errors: SavedObjectsImportFailure[] = []; - const importIdMap = new Map(); + const importStateMap: ImportStateMap = new Map(); const pendingOverwrites = new Set(); // exit early if there are no objects to check if (objects.length === 0) { - return { filteredObjects, errors, importIdMap, pendingOverwrites }; + return { filteredObjects, errors, importStateMap, pendingOverwrites }; } const retryMap = retries.reduce( @@ -76,7 +77,7 @@ export async function checkConflicts({ // This code path should not be triggered for a retry, but in case the consumer is using the import APIs incorrectly and attempting to // retry an object with a destinationId that would result in an unresolvable conflict, we regenerate the ID here as a fail-safe. const omitOriginId = createNewCopies || createNewCopy; - importIdMap.set(`${type}:${id}`, { id: uuidv4(), omitOriginId }); + importStateMap.set(`${type}:${id}`, { destinationId: uuidv4(), omitOriginId }); filteredObjects.push(object); } else if (errorObj && errorObj.statusCode !== 409) { errors.push({ type, id, title, meta: { title }, error: { ...errorObj, type: 'unknown' } }); @@ -90,5 +91,5 @@ export async function checkConflicts({ } } }); - return { filteredObjects, errors, importIdMap, pendingOverwrites }; + return { filteredObjects, errors, importStateMap, pendingOverwrites }; } diff --git a/src/core/server/saved_objects/import/lib/check_origin_conflicts.test.ts b/src/core/server/saved_objects/import/lib/check_origin_conflicts.test.ts index 8ac31c7c83c25..3d25b66edf5fe 100644 --- a/src/core/server/saved_objects/import/lib/check_origin_conflicts.test.ts +++ b/src/core/server/saved_objects/import/lib/check_origin_conflicts.test.ts @@ -16,6 +16,7 @@ import { checkOriginConflicts } from './check_origin_conflicts'; import { savedObjectsClientMock } from '../../../mocks'; import { typeRegistryMock } from '../../saved_objects_type_registry.mock'; import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; +import type { ImportStateMap } from './types'; jest.mock('uuid', () => ({ v4: () => 'uuidv4', @@ -59,7 +60,7 @@ describe('#checkOriginConflicts', () => { const setupParams = (partial: { objects: SavedObjectType[]; namespace?: string; - importIdMap?: Map; + importStateMap?: ImportStateMap; ignoreRegularConflicts?: boolean; }): CheckOriginConflictsParams => { savedObjectsClient = savedObjectsClientMock.create(); @@ -68,7 +69,7 @@ describe('#checkOriginConflicts', () => { typeRegistry = typeRegistryMock.create(); typeRegistry.isMultiNamespace.mockImplementation((type) => type === MULTI_NS_TYPE); return { - importIdMap: new Map(), // empty by default + importStateMap: new Map(), // empty by default ...partial, savedObjectsClient, typeRegistry, @@ -181,7 +182,7 @@ describe('#checkOriginConflicts', () => { }, }); - describe('object result without a `importIdMap` entry (no match or exact match)', () => { + describe('object result without a `importStateMap` 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 @@ -196,7 +197,7 @@ describe('#checkOriginConflicts', () => { const checkOriginConflictsResult = await checkOriginConflicts(params); const expectedResult = { - importIdMap: new Map(), + importStateMap: new Map(), errors: [], pendingOverwrites: new Set(), }; @@ -213,7 +214,7 @@ describe('#checkOriginConflicts', () => { const objects = [obj2, obj4]; const params = setupParams({ objects, - importIdMap: new Map([ + importStateMap: new Map([ [`${obj1.type}:${obj1.id}`, {}], [`${obj2.type}:${obj2.id}`, {}], [`${obj3.type}:${obj3.id}`, {}], @@ -225,7 +226,7 @@ describe('#checkOriginConflicts', () => { const checkOriginConflictsResult = await checkOriginConflicts(params); const expectedResult = { - importIdMap: new Map(), + importStateMap: new Map(), errors: [], pendingOverwrites: new Set(), }; @@ -241,7 +242,7 @@ describe('#checkOriginConflicts', () => { const objects = [obj3]; const params = setupParams({ objects, - importIdMap: new Map([ + importStateMap: new Map([ [`${obj1.type}:${obj1.id}`, {}], [`${obj2.type}:${obj2.id}`, {}], [`${obj3.type}:${obj3.id}`, {}], @@ -251,7 +252,7 @@ describe('#checkOriginConflicts', () => { const checkOriginConflictsResult = await checkOriginConflicts(params); const expectedResult = { - importIdMap: new Map(), + importStateMap: new Map(), errors: [], pendingOverwrites: new Set(), }; @@ -259,7 +260,7 @@ describe('#checkOriginConflicts', () => { }); }); - describe('object result with a `importIdMap` entry (partial match with a single destination)', () => { + describe('object result with a `importStateMap` entry (partial match with a single destination)', () => { describe('when an inexact match is detected (1 hit)', () => { // objA and objB exist in this space // try to import obj1 and obj2 @@ -280,20 +281,20 @@ describe('#checkOriginConflicts', () => { const params = setup(false); const checkOriginConflictsResult = await checkOriginConflicts(params); const expectedResult = { - importIdMap: new Map(), + importStateMap: new Map(), errors: [createConflictError(obj1, objA.id), createConflictError(obj2, objB.id)], pendingOverwrites: new Set(), }; expect(checkOriginConflictsResult).toEqual(expectedResult); }); - test('returns object with a `importIdMap` entry when ignoreRegularConflicts=true', async () => { + test('returns object with a `importStateMap` entry when ignoreRegularConflicts=true', async () => { const params = setup(true); const checkOriginConflictsResult = await checkOriginConflicts(params); const expectedResult = { - importIdMap: new Map([ - [`${obj1.type}:${obj1.id}`, { id: objA.id }], - [`${obj2.type}:${obj2.id}`, { id: objB.id }], + importStateMap: new Map([ + [`${obj1.type}:${obj1.id}`, { destinationId: objA.id }], + [`${obj2.type}:${obj2.id}`, { destinationId: objB.id }], ]), errors: [], pendingOverwrites: new Set([`${obj1.type}:${obj1.id}`, `${obj2.type}:${obj2.id}`]), @@ -317,7 +318,7 @@ describe('#checkOriginConflicts', () => { const params = setupParams({ objects, ignoreRegularConflicts, - importIdMap: new Map([ + importStateMap: new Map([ [`${obj1.type}:${obj1.id}`, {}], [`${obj2.type}:${obj2.id}`, {}], [`${obj3.type}:${obj3.id}`, {}], @@ -333,20 +334,20 @@ describe('#checkOriginConflicts', () => { const params = setup(false); const checkOriginConflictsResult = await checkOriginConflicts(params); const expectedResult = { - importIdMap: new Map(), + importStateMap: new Map(), errors: [createConflictError(obj2, objA.id), createConflictError(obj4, objB.id)], pendingOverwrites: new Set(), }; expect(checkOriginConflictsResult).toEqual(expectedResult); }); - test('returns object with a `importIdMap` entry when ignoreRegularConflicts=true', async () => { + test('returns object with a `importStateMap` entry when ignoreRegularConflicts=true', async () => { const params = setup(true); const checkOriginConflictsResult = await checkOriginConflicts(params); const expectedResult = { - importIdMap: new Map([ - [`${obj2.type}:${obj2.id}`, { id: objA.id }], - [`${obj4.type}:${obj4.id}`, { id: objB.id }], + importStateMap: new Map([ + [`${obj2.type}:${obj2.id}`, { destinationId: objA.id }], + [`${obj4.type}:${obj4.id}`, { destinationId: objB.id }], ]), errors: [], pendingOverwrites: new Set([`${obj2.type}:${obj2.id}`, `${obj4.type}:${obj4.id}`]), @@ -357,7 +358,7 @@ describe('#checkOriginConflicts', () => { }); describe('ambiguous conflicts', () => { - test('returns object with a `importIdMap` entry when multiple inexact matches are detected that target the same single destination', async () => { + test('returns object with a `importStateMap` 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'); @@ -375,11 +376,11 @@ describe('#checkOriginConflicts', () => { const checkOriginConflictsResult = await checkOriginConflicts(params); const expectedResult = { - 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 }], + importStateMap: new Map([ + [`${obj1.type}:${obj1.id}`, { destinationId: 'uuidv4', omitOriginId: true }], + [`${obj2.type}:${obj2.id}`, { destinationId: 'uuidv4', omitOriginId: true }], + [`${obj3.type}:${obj3.id}`, { destinationId: 'uuidv4', omitOriginId: true }], + [`${obj4.type}:${obj4.id}`, { destinationId: 'uuidv4', omitOriginId: true }], ]), errors: [], pendingOverwrites: new Set(), @@ -403,7 +404,7 @@ describe('#checkOriginConflicts', () => { const checkOriginConflictsResult = await checkOriginConflicts(params); const expectedResult = { - importIdMap: new Map(), + importStateMap: new Map(), errors: [ createAmbiguousConflictError(obj1, [objB, objA]), // Assert that these have been sorted by updatedAt in descending order createAmbiguousConflictError(obj2, [objC, objD]), // Assert that these have been sorted by ID in ascending order (since their updatedAt values are the same) @@ -413,7 +414,7 @@ describe('#checkOriginConflicts', () => { expect(checkOriginConflictsResult).toEqual(expectedResult); }); - test('returns object with a `importIdMap` entry when multiple inexact matches are detected that target the same multiple destinations', async () => { + test('returns object with a `importStateMap` 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'); @@ -433,11 +434,11 @@ describe('#checkOriginConflicts', () => { const checkOriginConflictsResult = await checkOriginConflicts(params); const expectedResult = { - 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 }], + importStateMap: new Map([ + [`${obj1.type}:${obj1.id}`, { destinationId: 'uuidv4', omitOriginId: true }], + [`${obj2.type}:${obj2.id}`, { destinationId: 'uuidv4', omitOriginId: true }], + [`${obj3.type}:${obj3.id}`, { destinationId: 'uuidv4', omitOriginId: true }], + [`${obj4.type}:${obj4.id}`, { destinationId: 'uuidv4', omitOriginId: true }], ]), errors: [], pendingOverwrites: new Set(), @@ -465,10 +466,12 @@ describe('#checkOriginConflicts', () => { const objE = createObject(MULTI_NS_TYPE, 'id-E', obj7.id); const objects = [obj1, obj2, obj4, obj5, obj6, obj7, obj8]; - const importIdMap = new Map([...objects, obj3].map(({ type, id }) => [`${type}:${id}`, {}])); + const importStateMap = new Map( + [...objects, obj3].map(({ type, id }) => [`${type}:${id}`, {}]) + ); const setup = (ignoreRegularConflicts: boolean) => { - const params = setupParams({ objects, importIdMap, ignoreRegularConflicts }); + const params = setupParams({ objects, importStateMap, 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 @@ -483,9 +486,9 @@ describe('#checkOriginConflicts', () => { const params = setup(false); const checkOriginConflictsResult = await checkOriginConflicts(params); const expectedResult = { - importIdMap: new Map([ - [`${obj7.type}:${obj7.id}`, { id: 'uuidv4', omitOriginId: true }], - [`${obj8.type}:${obj8.id}`, { id: 'uuidv4', omitOriginId: true }], + importStateMap: new Map([ + [`${obj7.type}:${obj7.id}`, { destinationId: 'uuidv4', omitOriginId: true }], + [`${obj8.type}:${obj8.id}`, { destinationId: 'uuidv4', omitOriginId: true }], ]), errors: [ createConflictError(obj5, objA.id), @@ -500,10 +503,10 @@ describe('#checkOriginConflicts', () => { const params = setup(true); const checkOriginConflictsResult = await checkOriginConflicts(params); const expectedResult = { - 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 }], + importStateMap: new Map([ + [`${obj5.type}:${obj5.id}`, { destinationId: objA.id }], + [`${obj7.type}:${obj7.id}`, { destinationId: 'uuidv4', omitOriginId: true }], + [`${obj8.type}:${obj8.id}`, { destinationId: 'uuidv4', omitOriginId: true }], ]), errors: [createAmbiguousConflictError(obj6, [objB, objC])], pendingOverwrites: new Set([`${obj5.type}:${obj5.id}`]), diff --git a/src/core/server/saved_objects/import/lib/check_origin_conflicts.ts b/src/core/server/saved_objects/import/lib/check_origin_conflicts.ts index efb6d3af06a55..fe4058ae868dd 100644 --- a/src/core/server/saved_objects/import/lib/check_origin_conflicts.ts +++ b/src/core/server/saved_objects/import/lib/check_origin_conflicts.ts @@ -10,6 +10,7 @@ import pMap from 'p-map'; import { v4 as uuidv4 } from 'uuid'; import { SavedObject, SavedObjectsClientContract, SavedObjectsImportFailure } from '../../types'; import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; +import type { ImportStateMap } from './types'; interface CheckOriginConflictsParams { objects: Array>; @@ -17,7 +18,7 @@ interface CheckOriginConflictsParams { typeRegistry: ISavedObjectTypeRegistry; namespace?: string; ignoreRegularConflicts?: boolean; - importIdMap: Map; + importStateMap: ImportStateMap; } type CheckOriginConflictParams = Omit & { @@ -76,8 +77,8 @@ const getAmbiguousConflictSourceKey = ({ object }: InexactMatch) => const checkOriginConflict = async ( params: CheckOriginConflictParams ): Promise> => { - const { object, savedObjectsClient, typeRegistry, namespace, importIdMap } = params; - const importIds = new Set(importIdMap.keys()); + const { object, savedObjectsClient, typeRegistry, namespace, importStateMap } = params; + const importIds = new Set(importStateMap.keys()); const { type, originId } = object; if (!typeRegistry.isMultiNamespace(type)) { @@ -126,7 +127,7 @@ const checkOriginConflict = async ( * 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 + * A. If there is a single source and destination match, add the destination to the importStateMap 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. */ @@ -148,7 +149,7 @@ export async function checkOriginConflicts({ objects, ...params }: CheckOriginCo }, new Map>>()); const errors: SavedObjectsImportFailure[] = []; - const importIdMap = new Map(); + const importStateMap: ImportStateMap = new Map(); const pendingOverwrites = new Set(); checkOriginConflictResults.forEach((result) => { if (!isLeft(result)) { @@ -163,7 +164,7 @@ export async function checkOriginConflicts({ objects, ...params }: CheckOriginCo if (sources.length === 1 && destinations.length === 1) { // This is a simple "inexact match" result -- a single import object has a single destination conflict. if (params.ignoreRegularConflicts) { - importIdMap.set(`${type}:${id}`, { id: destinations[0].id }); + importStateMap.set(`${type}:${id}`, { destinationId: destinations[0].id }); pendingOverwrites.add(`${type}:${id}`); } else { const { title } = attributes; @@ -187,7 +188,7 @@ export async function checkOriginConflicts({ objects, ...params }: CheckOriginCo 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., the same outcome as if `createNewCopies` was enabled for the entire import operation). - importIdMap.set(`${type}:${id}`, { id: uuidv4(), omitOriginId: true }); + importStateMap.set(`${type}:${id}`, { destinationId: uuidv4(), omitOriginId: true }); return; } const { title } = attributes; @@ -203,5 +204,5 @@ export async function checkOriginConflicts({ objects, ...params }: CheckOriginCo }); }); - return { errors, importIdMap, pendingOverwrites }; + return { errors, importStateMap, pendingOverwrites }; } diff --git a/src/core/server/saved_objects/import/lib/collect_saved_objects.test.ts b/src/core/server/saved_objects/import/lib/collect_saved_objects.test.ts index c6307070d9231..7247ca9371833 100644 --- a/src/core/server/saved_objects/import/lib/collect_saved_objects.test.ts +++ b/src/core/server/saved_objects/import/lib/collect_saved_objects.test.ts @@ -120,7 +120,7 @@ describe('collectSavedObjects()', () => { const readStream = createReadStream(); const result = await collectSavedObjects({ readStream, supportedTypes: [], objectLimit }); - expect(result).toEqual({ collectedObjects: [], errors: [], importIdMap: new Map() }); + expect(result).toEqual({ collectedObjects: [], errors: [], importStateMap: new Map() }); }); test('collects objects from stream', async () => { @@ -129,8 +129,8 @@ describe('collectSavedObjects()', () => { const result = await collectSavedObjects({ readStream, supportedTypes, objectLimit }); const collectedObjects = [{ ...obj1, migrationVersion: {} }]; - const importIdMap = new Map([[`${obj1.type}:${obj1.id}`, {}]]); - expect(result).toEqual({ collectedObjects, errors: [], importIdMap }); + const importStateMap = new Map([[`${obj1.type}:${obj1.id}`, {}]]); + expect(result).toEqual({ collectedObjects, errors: [], importStateMap }); }); test('unsupported types return as import errors', async () => { @@ -141,7 +141,7 @@ describe('collectSavedObjects()', () => { const error = { type: 'unsupported_type' }; const { title } = obj1.attributes; const errors = [{ error, type: obj1.type, id: obj1.id, title, meta: { title } }]; - expect(result).toEqual({ collectedObjects: [], errors, importIdMap: new Map() }); + expect(result).toEqual({ collectedObjects: [], errors, importStateMap: new Map() }); }); test('returns mixed results', async () => { @@ -150,11 +150,11 @@ describe('collectSavedObjects()', () => { const result = await collectSavedObjects({ readStream, supportedTypes, objectLimit }); const collectedObjects = [{ ...obj2, migrationVersion: {} }]; - const importIdMap = new Map([[`${obj2.type}:${obj2.id}`, {}]]); + const importStateMap = new Map([[`${obj2.type}:${obj2.id}`, {}]]); const error = { type: 'unsupported_type' }; const { title } = obj1.attributes; const errors = [{ error, type: obj1.type, id: obj1.id, title, meta: { title } }]; - expect(result).toEqual({ collectedObjects, errors, importIdMap }); + expect(result).toEqual({ collectedObjects, errors, importStateMap }); }); describe('with optional filter', () => { @@ -172,7 +172,7 @@ describe('collectSavedObjects()', () => { const error = { type: 'unsupported_type' }; const { title } = obj1.attributes; const errors = [{ error, type: obj1.type, id: obj1.id, title, meta: { title } }]; - expect(result).toEqual({ collectedObjects: [], errors, importIdMap: new Map() }); + expect(result).toEqual({ collectedObjects: [], errors, importStateMap: new Map() }); }); test('does not filter out objects when result === true', async () => { @@ -187,11 +187,11 @@ describe('collectSavedObjects()', () => { }); const collectedObjects = [{ ...obj2, migrationVersion: {} }]; - const importIdMap = new Map([[`${obj2.type}:${obj2.id}`, {}]]); + const importStateMap = new Map([[`${obj2.type}:${obj2.id}`, {}]]); const error = { type: 'unsupported_type' }; const { title } = obj1.attributes; const errors = [{ error, type: obj1.type, id: obj1.id, title, meta: { title } }]; - expect(result).toEqual({ collectedObjects, errors, importIdMap }); + expect(result).toEqual({ collectedObjects, errors, importStateMap }); }); }); }); diff --git a/src/core/server/saved_objects/import/lib/collect_saved_objects.ts b/src/core/server/saved_objects/import/lib/collect_saved_objects.ts index 58c7a759cf0bb..4e2888f46f1c0 100644 --- a/src/core/server/saved_objects/import/lib/collect_saved_objects.ts +++ b/src/core/server/saved_objects/import/lib/collect_saved_objects.ts @@ -19,6 +19,7 @@ import { SavedObjectsImportFailure } from '../types'; import { SavedObjectsImportError } from '../errors'; import { getNonUniqueEntries } from './get_non_unique_entries'; import { createLimitStream } from './create_limit_stream'; +import type { ImportStateMap } from './types'; interface CollectSavedObjectsOptions { readStream: Readable; @@ -35,7 +36,7 @@ export async function collectSavedObjects({ }: CollectSavedObjectsOptions) { const errors: SavedObjectsImportFailure[] = []; const entries: Array<{ type: string; id: string }> = []; - const importIdMap = new Map(); + const importStateMap: ImportStateMap = new Map(); const collectedObjects: Array> = await createPromiseFromStreams([ readStream, createLimitStream(objectLimit), @@ -58,7 +59,7 @@ export async function collectSavedObjects({ }), createFilterStream((obj) => (filter ? filter(obj) : true)), createMapStream((obj: SavedObject) => { - importIdMap.set(`${obj.type}:${obj.id}`, {}); + importStateMap.set(`${obj.type}:${obj.id}`, {}); // Ensure migrations execute on every saved object return Object.assign({ migrationVersion: {} }, obj); }), @@ -74,6 +75,6 @@ export async function collectSavedObjects({ return { errors, collectedObjects, - importIdMap, + importStateMap, }; } diff --git a/src/core/server/saved_objects/import/lib/create_saved_objects.test.ts b/src/core/server/saved_objects/import/lib/create_saved_objects.test.ts index 38372e8fad6fd..7f8b67406773e 100644 --- a/src/core/server/saved_objects/import/lib/create_saved_objects.test.ts +++ b/src/core/server/saved_objects/import/lib/create_saved_objects.test.ts @@ -23,8 +23,8 @@ const createObject = (type: string, id: string, originId?: string): SavedObject attributes: {}, 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 + { name: 'name-2', type: MULTI_NS_TYPE, id: 'id-1' }, // object that is present, but does not have an importStateMap entry + { name: 'name-3', type: MULTI_NS_TYPE, id: 'id-3' }, // object that is present and has an importStateMap entry ], ...(originId && { originId }), }); @@ -52,10 +52,10 @@ const obj13 = createObject(OTHER_TYPE, 'id-13'); // -> conflict const importId3 = 'id-foo'; const importId4 = 'id-bar'; const importId8 = 'id-baz'; -const importIdMap = new Map([ - [`${obj3.type}:${obj3.id}`, { id: importId3, omitOriginId: true }], - [`${obj4.type}:${obj4.id}`, { id: importId4 }], - [`${obj8.type}:${obj8.id}`, { id: importId8 }], +const importStateMap = new Map([ + [`${obj3.type}:${obj3.id}`, { destinationId: importId3, omitOriginId: true }], + [`${obj4.type}:${obj4.id}`, { destinationId: importId4 }], + [`${obj8.type}:${obj8.id}`, { destinationId: importId8 }], ]); describe('#createSavedObjects', () => { @@ -74,7 +74,7 @@ describe('#createSavedObjects', () => { }): CreateSavedObjectsParams => { savedObjectsClient = savedObjectsClientMock.create(); bulkCreate = savedObjectsClient.bulkCreate; - return { accumulatedErrors: [], ...partial, savedObjectsClient, importIdMap }; + return { accumulatedErrors: [], ...partial, savedObjectsClient, importStateMap }; }; const getExpectedBulkCreateArgsObjects = (objects: SavedObject[], retry?: boolean) => @@ -84,8 +84,8 @@ describe('#createSavedObjects', () => { attributes, 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 + { name: 'name-2', type: MULTI_NS_TYPE, id: 'id-1' }, // object that is present, but does not have an importStateMap entry + { name: 'name-3', type: MULTI_NS_TYPE, id: 'id-foo' }, // object that is present and has an importStateMap 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 }), @@ -245,7 +245,7 @@ describe('#createSavedObjects', () => { await createSavedObjects(options); expect(bulkCreate).toHaveBeenCalledTimes(1); - // these three objects are transformed before being created, because they are included in the `importIdMap` + // these three objects are transformed before being created, because they are included in the `importStateMap` 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 diff --git a/src/core/server/saved_objects/import/lib/create_saved_objects.ts b/src/core/server/saved_objects/import/lib/create_saved_objects.ts index 30a3b4df96125..bf58b2bb4b00e 100644 --- a/src/core/server/saved_objects/import/lib/create_saved_objects.ts +++ b/src/core/server/saved_objects/import/lib/create_saved_objects.ts @@ -9,12 +9,13 @@ import { SavedObject, SavedObjectsClientContract, SavedObjectsImportFailure } from '../../types'; import { extractErrors } from './extract_errors'; import { CreatedObject } from '../types'; +import type { ImportStateMap } from './types'; export interface CreateSavedObjectsParams { objects: Array>; accumulatedErrors: SavedObjectsImportFailure[]; savedObjectsClient: SavedObjectsClientContract; - importIdMap: Map; + importStateMap: ImportStateMap; namespace?: string; overwrite?: boolean; } @@ -31,7 +32,7 @@ export const createSavedObjects = async ({ objects, accumulatedErrors, savedObjectsClient, - importIdMap, + importStateMap, namespace, overwrite, }: CreateSavedObjectsParams): Promise> => { @@ -58,19 +59,24 @@ export const createSavedObjects = async ({ // 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 }; + const importStateValue = importStateMap.get(`${type}:${id}`); + if (importStateValue?.destinationId) { + return { ...reference, id: importStateValue.destinationId }; } 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 }) }; + const importStateValue = importStateMap.get(`${object.type}:${object.id}`); + if (importStateValue?.destinationId) { + objectIdMap.set(`${object.type}:${importStateValue.destinationId}`, object); + const originId = importStateValue.omitOriginId ? undefined : object.originId ?? object.id; + return { + ...object, + id: importStateValue.destinationId, + originId, + ...(references && { references }), + }; } return { ...object, ...(references && { references }) }; }); diff --git a/src/core/server/saved_objects/import/lib/get_import_id_map_for_retries.test.ts b/src/core/server/saved_objects/import/lib/get_import_state_map_for_retries.test.ts similarity index 73% rename from src/core/server/saved_objects/import/lib/get_import_id_map_for_retries.test.ts rename to src/core/server/saved_objects/import/lib/get_import_state_map_for_retries.test.ts index 1a6c3fa43ea5c..af5aca10ba289 100644 --- a/src/core/server/saved_objects/import/lib/get_import_id_map_for_retries.test.ts +++ b/src/core/server/saved_objects/import/lib/get_import_state_map_for_retries.test.ts @@ -8,9 +8,9 @@ import type { SavedObject } from '../../types'; import type { SavedObjectsImportRetry } from '../types'; -import { getImportIdMapForRetries } from './get_import_id_map_for_retries'; +import { getImportStateMapForRetries } from './get_import_state_map_for_retries'; -describe('#getImportIdMapForRetries', () => { +describe('#getImportStateMapForRetries', () => { const createRetry = ( { type, id }: { type: string; id: string }, params: { destinationId?: string; createNewCopy?: boolean } = {} @@ -26,7 +26,7 @@ describe('#getImportIdMapForRetries', () => { const retries = [createRetry(obj1)]; const params = { objects, retries, createNewCopies: false }; - expect(() => getImportIdMapForRetries(params)).toThrowErrorMatchingInlineSnapshot( + expect(() => getImportStateMapForRetries(params)).toThrowErrorMatchingInlineSnapshot( `"Retry was expected for \\"type-2:id-2\\" but not found"` ); }); @@ -40,29 +40,29 @@ describe('#getImportIdMapForRetries', () => { 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', createNewCopy: true }), // this retry will get added to the `importIdMap`! + createRetry(obj3, { destinationId: 'id-X' }), // this retry will get added to the `importStateMap`! + createRetry(obj4, { destinationId: 'id-Y', createNewCopy: true }), // this retry will get added to the `importStateMap`! ]; const params = { objects, retries, createNewCopies: false }; - const result = await getImportIdMapForRetries(params); + const result = await getImportStateMapForRetries(params); expect(result).toEqual( new Map([ - [`${obj3.type}:${obj3.id}`, { id: 'id-X', omitOriginId: false }], - [`${obj4.type}:${obj4.id}`, { id: 'id-Y', omitOriginId: true }], + [`${obj3.type}:${obj3.id}`, { destinationId: 'id-X', omitOriginId: false }], + [`${obj4.type}:${obj4.id}`, { destinationId: 'id-Y', omitOriginId: true }], ]) ); }); - test('omits origin ID in `importIdMap` entries when createNewCopies=true', async () => { + test('omits origin ID in `importStateMap` entries when createNewCopies=true', async () => { const obj1 = { type: 'type-1', id: 'id-1' }; const objects = [obj1] as SavedObject[]; const retries = [createRetry(obj1, { destinationId: 'id-X' })]; const params = { objects, retries, createNewCopies: true }; - const result = await getImportIdMapForRetries(params); + const result = await getImportStateMapForRetries(params); expect(result).toEqual( - new Map([[`${obj1.type}:${obj1.id}`, { id: 'id-X', omitOriginId: true }]]) + new Map([[`${obj1.type}:${obj1.id}`, { destinationId: 'id-X', omitOriginId: true }]]) ); }); }); diff --git a/src/core/server/saved_objects/import/lib/get_import_id_map_for_retries.ts b/src/core/server/saved_objects/import/lib/get_import_state_map_for_retries.ts similarity index 78% rename from src/core/server/saved_objects/import/lib/get_import_id_map_for_retries.ts rename to src/core/server/saved_objects/import/lib/get_import_state_map_for_retries.ts index 888d3032842e0..3066ae72738a4 100644 --- a/src/core/server/saved_objects/import/lib/get_import_id_map_for_retries.ts +++ b/src/core/server/saved_objects/import/lib/get_import_state_map_for_retries.ts @@ -7,8 +7,9 @@ */ import { SavedObject, SavedObjectsImportRetry } from '../../types'; +import type { ImportStateMap } from './types'; -interface GetImportIdMapForRetriesParams { +interface GetImportStateMapForRetriesParams { objects: SavedObject[]; retries: SavedObjectsImportRetry[]; createNewCopies: boolean; @@ -17,14 +18,14 @@ interface GetImportIdMapForRetriesParams { /** * Assume that all objects exist in the `retries` map (due to filtering at the beginning of `resolveSavedObjectsImportErrors`). */ -export function getImportIdMapForRetries(params: GetImportIdMapForRetriesParams) { +export function getImportStateMapForRetries(params: GetImportStateMapForRetriesParams) { const { objects, retries, createNewCopies } = params; const retryMap = retries.reduce( (acc, cur) => acc.set(`${cur.type}:${cur.id}`, cur), new Map() ); - const importIdMap = new Map(); + const importStateMap: ImportStateMap = new Map(); objects.forEach(({ type, id }) => { const retry = retryMap.get(`${type}:${id}`); @@ -34,9 +35,9 @@ export function getImportIdMapForRetries(params: GetImportIdMapForRetriesParams) const { destinationId } = retry; const omitOriginId = createNewCopies || Boolean(retry.createNewCopy); if (destinationId && destinationId !== id) { - importIdMap.set(`${type}:${id}`, { id: destinationId, omitOriginId }); + importStateMap.set(`${type}:${id}`, { destinationId, omitOriginId }); } }); - return importIdMap; + return importStateMap; } diff --git a/src/core/server/saved_objects/import/lib/index.ts b/src/core/server/saved_objects/import/lib/index.ts index ea8fa376793d7..2f0e0aa9505dc 100644 --- a/src/core/server/saved_objects/import/lib/index.ts +++ b/src/core/server/saved_objects/import/lib/index.ts @@ -13,10 +13,11 @@ export { createLimitStream } from './create_limit_stream'; export { createObjectsFilter } from './create_objects_filter'; export { createSavedObjects } from './create_saved_objects'; export { extractErrors } from './extract_errors'; -export { getImportIdMapForRetries } from './get_import_id_map_for_retries'; +export { getImportStateMapForRetries } from './get_import_state_map_for_retries'; export { getNonUniqueEntries } from './get_non_unique_entries'; export { regenerateIds } from './regenerate_ids'; export { splitOverwrites } from './split_overwrites'; export { getNonExistingReferenceAsKeys, validateReferences } from './validate_references'; export { validateRetries } from './validate_retries'; export { executeImportHooks } from './execute_import_hooks'; +export type { ImportStateMap, ImportStateValue } from './types'; diff --git a/src/core/server/saved_objects/import/lib/regenerate_ids.test.ts b/src/core/server/saved_objects/import/lib/regenerate_ids.test.ts index 1cc14d9cfa428..d22b9431367d4 100644 --- a/src/core/server/saved_objects/import/lib/regenerate_ids.test.ts +++ b/src/core/server/saved_objects/import/lib/regenerate_ids.test.ts @@ -27,9 +27,9 @@ describe('#regenerateIds', () => { test('returns expected values', () => { expect(regenerateIds(objects)).toEqual( new Map([ - ['foo:1', { id: 'uuidv4 #1', omitOriginId: true }], - ['bar:2', { id: 'uuidv4 #2', omitOriginId: true }], - ['baz:3', { id: 'uuidv4 #3', omitOriginId: true }], + ['foo:1', { destinationId: 'uuidv4 #1', omitOriginId: true }], + ['bar:2', { destinationId: 'uuidv4 #2', omitOriginId: true }], + ['baz:3', { destinationId: 'uuidv4 #3', omitOriginId: true }], ]) ); }); diff --git a/src/core/server/saved_objects/import/lib/regenerate_ids.ts b/src/core/server/saved_objects/import/lib/regenerate_ids.ts index 01ce8bd93c01a..174658555aaf1 100644 --- a/src/core/server/saved_objects/import/lib/regenerate_ids.ts +++ b/src/core/server/saved_objects/import/lib/regenerate_ids.ts @@ -8,15 +8,17 @@ import { v4 as uuidv4 } from 'uuid'; import { SavedObject } from '../../types'; +import type { ImportStateMap } from './types'; /** - * Takes an array of saved objects and returns an importIdMap of randomly-generated new IDs. + * Takes an array of saved objects and returns an importStateMap of randomly-generated new IDs. * * @param objects The saved objects to generate new IDs for. */ 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; + const importStateMap: ImportStateMap = new Map(); + for (const { type, id } of objects) { + importStateMap.set(`${type}:${id}`, { destinationId: uuidv4(), omitOriginId: true }); + } + return importStateMap; }; diff --git a/src/core/server/saved_objects/import/lib/types.ts b/src/core/server/saved_objects/import/lib/types.ts new file mode 100644 index 0000000000000..74cdf94fa1091 --- /dev/null +++ b/src/core/server/saved_objects/import/lib/types.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** + * This map contains entries for objects that are included in the import operation. The entry key is the object's `type:id`, and the entry + * value contains optional attributes which change how that object is created. The initial map that is created by the collectSavedObjects + * module contains one entry with an empty value for each object that is being imported. + * + * This map is meant to function as a sort of accumulator; each module that is called during the import process can emit new entries that + * will override those from the initial map. + */ +export type ImportStateMap = Map; + +/** + * The value of an import state entry, which contains optional attributes that change how the object is created. + */ +export interface ImportStateValue { + /** + * This attribute indicates that the object should have this ID instead of what was specified in the import file. + */ + destinationId?: string; + /** + * This attribute indicates that the object's originId should be cleared. + */ + omitOriginId?: boolean; +} diff --git a/src/core/server/saved_objects/import/resolve_import_errors.test.mock.ts b/src/core/server/saved_objects/import/resolve_import_errors.test.mock.ts index 25f1c0fafb8df..8723fadcef024 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.test.mock.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.test.mock.ts @@ -12,7 +12,7 @@ import type { collectSavedObjects } from './lib/collect_saved_objects'; import type { regenerateIds } from './lib/regenerate_ids'; import type { validateReferences } from './lib/validate_references'; import type { checkConflicts } from './lib/check_conflicts'; -import type { getImportIdMapForRetries } from './lib/get_import_id_map_for_retries'; +import type { getImportStateMapForRetries } from './lib/get_import_state_map_for_retries'; import type { splitOverwrites } from './lib/split_overwrites'; import type { createSavedObjects } from './lib/create_saved_objects'; import type { executeImportHooks } from './lib/execute_import_hooks'; @@ -47,11 +47,11 @@ jest.mock('./lib/check_conflicts', () => ({ checkConflicts: mockCheckConflicts, })); -export const mockGetImportIdMapForRetries = jest.fn() as jest.MockedFunction< - typeof getImportIdMapForRetries +export const mockGetImportStateMapForRetries = jest.fn() as jest.MockedFunction< + typeof getImportStateMapForRetries >; -jest.mock('./lib/get_import_id_map_for_retries', () => ({ - getImportIdMapForRetries: mockGetImportIdMapForRetries, +jest.mock('./lib/get_import_state_map_for_retries', () => ({ + getImportStateMapForRetries: mockGetImportStateMapForRetries, })); export const mockSplitOverwrites = jest.fn() as jest.MockedFunction; 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 9fc5a2c55ac90..ce6276af1fccb 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 @@ -13,7 +13,7 @@ import { mockRegenerateIds, mockValidateReferences, mockCheckConflicts, - mockGetImportIdMapForRetries, + mockGetImportStateMapForRetries, mockSplitOverwrites, mockCreateSavedObjects, mockExecuteImportHooks, @@ -46,17 +46,17 @@ describe('#importSavedObjectsFromStream', () => { mockCollectSavedObjects.mockResolvedValue({ errors: [], collectedObjects: [], - importIdMap: new Map(), + importStateMap: new Map(), }); mockRegenerateIds.mockReturnValue(new Map()); mockValidateReferences.mockResolvedValue([]); mockCheckConflicts.mockResolvedValue({ errors: [], filteredObjects: [], - importIdMap: new Map(), + importStateMap: new Map(), pendingOverwrites: new Set(), // not used by resolveImportErrors, but is a required return type }); - mockGetImportIdMapForRetries.mockReturnValue(new Map()); + mockGetImportStateMapForRetries.mockReturnValue(new Map()); mockSplitOverwrites.mockReturnValue({ objectsToOverwrite: [], objectsToNotOverwrite: [], @@ -139,7 +139,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 - * `getImportIdMapForRetries`, passed to `createSavedObjects`, and returned from that. However, for each of the tests below, we skip the + * `getImportStateMapForRetries`, 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', () => { @@ -180,7 +180,7 @@ describe('#importSavedObjectsFromStream', () => { mockCollectSavedObjects.mockResolvedValue({ errors: [], collectedObjects, - importIdMap: new Map(), // doesn't matter + importStateMap: new Map(), // doesn't matter }); await resolveSavedObjectsImportErrors(options); @@ -201,7 +201,7 @@ describe('#importSavedObjectsFromStream', () => { mockCollectSavedObjects.mockResolvedValue({ errors: [], collectedObjects, - importIdMap: new Map(), + importStateMap: new Map(), }); mockCreateSavedObjects.mockResolvedValueOnce({ errors: [], @@ -228,7 +228,7 @@ describe('#importSavedObjectsFromStream', () => { mockCollectSavedObjects.mockResolvedValue({ errors: [], collectedObjects: [object], - importIdMap: new Map(), // doesn't matter + importStateMap: new Map(), // doesn't matter }); await resolveSavedObjectsImportErrors(options); @@ -252,7 +252,7 @@ describe('#importSavedObjectsFromStream', () => { mockCollectSavedObjects.mockResolvedValue({ errors: [], collectedObjects, - importIdMap: new Map(), // doesn't matter + importStateMap: new Map(), // doesn't matter }); await resolveSavedObjectsImportErrors(options); @@ -274,13 +274,19 @@ describe('#importSavedObjectsFromStream', () => { mockCheckConflicts.mockResolvedValue({ errors: [], filteredObjects, - importIdMap: new Map(), + importStateMap: new Map(), pendingOverwrites: new Set(), // not used by resolveImportErrors, but is a required return type }); await resolveSavedObjectsImportErrors(options); - const getImportIdMapForRetriesParams = { objects: filteredObjects, retries, createNewCopies }; - expect(mockGetImportIdMapForRetries).toHaveBeenCalledWith(getImportIdMapForRetriesParams); + const getImportStateMapForRetriesParams = { + objects: filteredObjects, + retries, + createNewCopies, + }; + expect(mockGetImportStateMapForRetries).toHaveBeenCalledWith( + getImportStateMapForRetriesParams + ); }); test('splits objects to overwrite from those not to overwrite', async () => { @@ -290,7 +296,7 @@ describe('#importSavedObjectsFromStream', () => { mockCollectSavedObjects.mockResolvedValue({ errors: [], collectedObjects, - importIdMap: new Map(), // doesn't matter + importStateMap: new Map(), // doesn't matter }); await resolveSavedObjectsImportErrors(options); @@ -304,7 +310,7 @@ describe('#importSavedObjectsFromStream', () => { mockCollectSavedObjects.mockResolvedValue({ errors: [], collectedObjects, - importIdMap: new Map(), // doesn't matter + importStateMap: new Map(), // doesn't matter }); await resolveSavedObjectsImportErrors(options); @@ -317,24 +323,24 @@ describe('#importSavedObjectsFromStream', () => { mockCollectSavedObjects.mockResolvedValue({ errors: [errors[0]], collectedObjects: [], // doesn't matter - importIdMap: new Map(), // doesn't matter + importStateMap: new Map(), // doesn't matter }); mockValidateReferences.mockResolvedValue([errors[1]]); mockCheckConflicts.mockResolvedValue({ errors: [errors[2]], filteredObjects: [], - importIdMap: new Map([['foo', { id: 'someId' }]]), + importStateMap: new Map([['foo', { destinationId: 'someId' }]]), pendingOverwrites: new Set(), // not used by resolveImportErrors, but is a required return type }); - mockGetImportIdMapForRetries.mockReturnValue( + mockGetImportStateMapForRetries.mockReturnValue( new Map([ - ['foo', { id: 'newId' }], - ['bar', { id: 'anotherNewId' }], + ['foo', { destinationId: 'newId' }], + ['bar', { destinationId: 'anotherNewId' }], ]) ); - const importIdMap = new Map([ - ['foo', { id: 'someId' }], - ['bar', { id: 'anotherNewId' }], + const importStateMap = new Map([ + ['foo', { destinationId: 'someId' }], + ['bar', { destinationId: 'anotherNewId' }], ]); const objectsToOverwrite = [createObject()]; const objectsToNotOverwrite = [createObject()]; @@ -348,7 +354,7 @@ describe('#importSavedObjectsFromStream', () => { const partialCreateSavedObjectsParams = { accumulatedErrors: errors, savedObjectsClient, - importIdMap, + importStateMap, namespace, }; expect(mockCreateSavedObjects).toHaveBeenNthCalledWith(1, { @@ -370,7 +376,7 @@ describe('#importSavedObjectsFromStream', () => { mockCollectSavedObjects.mockResolvedValue({ errors: [], collectedObjects, - importIdMap: new Map(), // doesn't matter + importStateMap: new Map(), // doesn't matter }); await resolveSavedObjectsImportErrors(options); @@ -383,32 +389,32 @@ describe('#importSavedObjectsFromStream', () => { mockCollectSavedObjects.mockResolvedValue({ errors: [errors[0]], collectedObjects: [], // doesn't matter - importIdMap: new Map(), // doesn't matter + importStateMap: new Map(), // doesn't matter }); mockValidateReferences.mockResolvedValue([errors[1]]); mockRegenerateIds.mockReturnValue( new Map([ - ['foo', { id: 'randomId1' }], - ['bar', { id: 'randomId2' }], - ['baz', { id: 'randomId3' }], + ['foo', { destinationId: 'randomId1' }], + ['bar', { destinationId: 'randomId2' }], + ['baz', { destinationId: 'randomId3' }], ]) ); mockCheckConflicts.mockResolvedValue({ errors: [errors[2]], filteredObjects: [], - importIdMap: new Map([['bar', { id: 'someId' }]]), + importStateMap: new Map([['bar', { destinationId: 'someId' }]]), pendingOverwrites: new Set(), // not used by resolveImportErrors, but is a required return type }); - mockGetImportIdMapForRetries.mockReturnValue( + mockGetImportStateMapForRetries.mockReturnValue( new Map([ - ['bar', { id: 'newId' }], - ['baz', { id: 'anotherNewId' }], + ['bar', { destinationId: 'newId' }], + ['baz', { destinationId: 'anotherNewId' }], ]) ); - const importIdMap = new Map([ - ['foo', { id: 'randomId1' }], - ['bar', { id: 'someId' }], - ['baz', { id: 'anotherNewId' }], + const importStateMap = new Map([ + ['foo', { destinationId: 'randomId1' }], + ['bar', { destinationId: 'someId' }], + ['baz', { destinationId: 'anotherNewId' }], ]); const objectsToOverwrite = [createObject()]; const objectsToNotOverwrite = [createObject()]; @@ -422,7 +428,7 @@ describe('#importSavedObjectsFromStream', () => { const partialCreateSavedObjectsParams = { accumulatedErrors: errors, savedObjectsClient, - importIdMap, + importStateMap, namespace, }; expect(mockCreateSavedObjects).toHaveBeenNthCalledWith(1, { @@ -451,7 +457,7 @@ describe('#importSavedObjectsFromStream', () => { mockCollectSavedObjects.mockResolvedValue({ errors: [createError()], collectedObjects: [], - importIdMap: new Map(), // doesn't matter + importStateMap: new Map(), // doesn't matter }); const result = await resolveSavedObjectsImportErrors(options); @@ -469,7 +475,7 @@ describe('#importSavedObjectsFromStream', () => { mockCollectSavedObjects.mockResolvedValue({ errors: [], collectedObjects, - importIdMap: new Map(), + importStateMap: new Map(), }); mockCreateSavedObjects.mockResolvedValueOnce({ errors: [], @@ -558,7 +564,7 @@ describe('#importSavedObjectsFromStream', () => { mockCheckConflicts.mockResolvedValue({ errors: [], filteredObjects: [], - importIdMap: new Map(), + importStateMap: new Map(), pendingOverwrites: new Set(), }); mockCreateSavedObjects @@ -596,7 +602,7 @@ describe('#importSavedObjectsFromStream', () => { mockCollectSavedObjects.mockResolvedValue({ errors: [errors[0]], collectedObjects: [], - importIdMap: new Map(), // doesn't matter + importStateMap: new Map(), // doesn't matter }); mockValidateReferences.mockResolvedValue([errors[1]]); mockCreateSavedObjects.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 25382965e845b..f6ed9de3825e6 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.ts @@ -23,9 +23,10 @@ import { validateReferences, validateRetries, createSavedObjects, - getImportIdMapForRetries, + getImportStateMapForRetries, checkConflicts, executeImportHooks, + ImportStateMap, } from './lib'; /** @@ -71,7 +72,7 @@ export async function resolveSavedObjectsImportErrors({ let successCount = 0; let errorAccumulator: SavedObjectsImportFailure[] = []; - let importIdMap: Map = new Map(); + let importStateMap: ImportStateMap = new Map(); const supportedTypes = typeRegistry.getImportableAndExportableTypes().map((type) => type.name); const filter = createObjectsFilter(retries); @@ -122,7 +123,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 - importIdMap = regenerateIds(objectsToResolve); + importStateMap = regenerateIds(objectsToResolve); } // Check single-namespace objects for conflicts in this namespace, and check multi-namespace objects for conflicts across all namespaces @@ -137,16 +138,16 @@ export async function resolveSavedObjectsImportErrors({ errorAccumulator = [...errorAccumulator, ...checkConflictsResult.errors]; // Check multi-namespace object types for regular conflicts and ambiguous conflicts - const getImportIdMapForRetriesParams = { + const getImportStateMapForRetriesParams = { objects: checkConflictsResult.filteredObjects, retries, createNewCopies, }; - const importIdMapForRetries = getImportIdMapForRetries(getImportIdMapForRetriesParams); - importIdMap = new Map([ - ...importIdMap, - ...importIdMapForRetries, - ...checkConflictsResult.importIdMap, // this importIdMap takes precedence over the others + const importStateMapForRetries = getImportStateMapForRetries(getImportStateMapForRetriesParams); + importStateMap = new Map([ + ...importStateMap, + ...importStateMapForRetries, + ...checkConflictsResult.importStateMap, // this importStateMap takes precedence over the others ]); // Bulk create in two batches, overwrites and non-overwrites @@ -161,7 +162,7 @@ export async function resolveSavedObjectsImportErrors({ objects, accumulatedErrors, savedObjectsClient, - importIdMap, + importStateMap, namespace, overwrite, };