diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.getconvertedobjectid.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.getconvertedobjectid.md
new file mode 100644
index 0000000000000..c6a429d345ed1
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.getconvertedobjectid.md
@@ -0,0 +1,28 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUtils](./kibana-plugin-core-server.savedobjectsutils.md) > [getConvertedObjectId](./kibana-plugin-core-server.savedobjectsutils.getconvertedobjectid.md)
+
+## SavedObjectsUtils.getConvertedObjectId() method
+
+Uses a single-namespace object's "legacy ID" to determine what its new ID will be after it is converted to a multi-namespace type.
+
+Signature:
+
+```typescript
+static getConvertedObjectId(namespace: string | undefined, type: string, id: string): string;
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| namespace | string | undefined
| |
+| type | string
| |
+| id | string
| |
+
+Returns:
+
+`string`
+
+{string} The ID of the saved object after it is converted.
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.md
index 0148621e757b7..ab6382aca6a52 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.md
@@ -24,5 +24,6 @@ export declare class SavedObjectsUtils
| Method | Modifiers | Description |
| --- | --- | --- |
| [generateId()](./kibana-plugin-core-server.savedobjectsutils.generateid.md) | static
| Generates a random ID for a saved objects. |
+| [getConvertedObjectId(namespace, type, id)](./kibana-plugin-core-server.savedobjectsutils.getconvertedobjectid.md) | static
| Uses a single-namespace object's "legacy ID" to determine what its new ID will be after it is converted to a multi-namespace type. |
| [isRandomId(id)](./kibana-plugin-core-server.savedobjectsutils.israndomid.md) | static
| Validates that a saved object ID has been randomly generated. |
diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.test.mock.ts b/src/core/server/saved_objects/migrations/core/document_migrator.test.mock.ts
new file mode 100644
index 0000000000000..ee0b18af5ac0d
--- /dev/null
+++ b/src/core/server/saved_objects/migrations/core/document_migrator.test.mock.ts
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+const mockGetConvertedObjectId = jest.fn().mockReturnValue('uuidv5');
+jest.mock('../../service/lib/utils', () => {
+ const actual = jest.requireActual('../../service/lib/utils');
+ return {
+ ...actual,
+ SavedObjectsUtils: {
+ ...actual.SavedObjectsUtils,
+ getConvertedObjectId: mockGetConvertedObjectId,
+ },
+ };
+});
+
+export { mockGetConvertedObjectId };
diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts
index 87b8ee0809064..64c1c4ce2fa9f 100644
--- a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts
+++ b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import { mockUuidv5 } from './__mocks__';
+import { mockGetConvertedObjectId } from './document_migrator.test.mock';
import { set } from '@elastic/safer-lodash-set';
import _ from 'lodash';
import { SavedObjectUnsanitizedDoc } from '../../serialization';
@@ -37,7 +37,7 @@ const createRegistry = (...types: Array>) => {
};
beforeEach(() => {
- mockUuidv5.mockClear();
+ mockGetConvertedObjectId.mockClear();
});
describe('DocumentMigrator', () => {
@@ -866,7 +866,7 @@ describe('DocumentMigrator', () => {
namespace: 'foo-namespace',
};
const actual = migrator.migrate(obj);
- expect(mockUuidv5).not.toHaveBeenCalled();
+ expect(mockGetConvertedObjectId).not.toHaveBeenCalled();
expect(actual).toEqual({
id: 'cowardly',
type: 'dog',
@@ -898,7 +898,7 @@ describe('DocumentMigrator', () => {
it('in the default space', () => {
const actual = migrator.migrateAndConvert(obj);
- expect(mockUuidv5).not.toHaveBeenCalled();
+ expect(mockGetConvertedObjectId).not.toHaveBeenCalled();
expect(actual).toEqual([
{
id: 'bad',
@@ -912,8 +912,8 @@ describe('DocumentMigrator', () => {
it('in a non-default space', () => {
const actual = migrator.migrateAndConvert({ ...obj, namespace: 'foo-namespace' });
- expect(mockUuidv5).toHaveBeenCalledTimes(1);
- expect(mockUuidv5).toHaveBeenCalledWith('foo-namespace:toy:favorite', 'DNSUUID');
+ expect(mockGetConvertedObjectId).toHaveBeenCalledTimes(1);
+ expect(mockGetConvertedObjectId).toHaveBeenCalledWith('foo-namespace', 'toy', 'favorite');
expect(actual).toEqual([
{
id: 'bad',
@@ -946,7 +946,7 @@ describe('DocumentMigrator', () => {
it('in the default space', () => {
const actual = migrator.migrateAndConvert(obj);
- expect(mockUuidv5).not.toHaveBeenCalled();
+ expect(mockGetConvertedObjectId).not.toHaveBeenCalled();
expect(actual).toEqual([
{
id: 'loud',
@@ -961,8 +961,8 @@ describe('DocumentMigrator', () => {
it('in a non-default space', () => {
const actual = migrator.migrateAndConvert({ ...obj, namespace: 'foo-namespace' });
- expect(mockUuidv5).toHaveBeenCalledTimes(1);
- expect(mockUuidv5).toHaveBeenCalledWith('foo-namespace:dog:loud', 'DNSUUID');
+ expect(mockGetConvertedObjectId).toHaveBeenCalledTimes(1);
+ expect(mockGetConvertedObjectId).toHaveBeenCalledWith('foo-namespace', 'dog', 'loud');
expect(actual).toEqual([
{
id: 'uuidv5',
@@ -1008,7 +1008,7 @@ describe('DocumentMigrator', () => {
it('in the default space', () => {
const actual = migrator.migrateAndConvert(obj);
- expect(mockUuidv5).not.toHaveBeenCalled();
+ expect(mockGetConvertedObjectId).not.toHaveBeenCalled();
expect(actual).toEqual([
{
id: 'cute',
@@ -1024,9 +1024,19 @@ describe('DocumentMigrator', () => {
it('in a non-default space', () => {
const actual = migrator.migrateAndConvert({ ...obj, namespace: 'foo-namespace' });
- expect(mockUuidv5).toHaveBeenCalledTimes(2);
- expect(mockUuidv5).toHaveBeenNthCalledWith(1, 'foo-namespace:toy:favorite', 'DNSUUID');
- expect(mockUuidv5).toHaveBeenNthCalledWith(2, 'foo-namespace:dog:cute', 'DNSUUID');
+ expect(mockGetConvertedObjectId).toHaveBeenCalledTimes(2);
+ expect(mockGetConvertedObjectId).toHaveBeenNthCalledWith(
+ 1,
+ 'foo-namespace',
+ 'toy',
+ 'favorite'
+ );
+ expect(mockGetConvertedObjectId).toHaveBeenNthCalledWith(
+ 2,
+ 'foo-namespace',
+ 'dog',
+ 'cute'
+ );
expect(actual).toEqual([
{
id: 'uuidv5',
@@ -1080,7 +1090,7 @@ describe('DocumentMigrator', () => {
it('in the default space', () => {
const actual = migrator.migrateAndConvert(obj);
- expect(mockUuidv5).not.toHaveBeenCalled();
+ expect(mockGetConvertedObjectId).not.toHaveBeenCalled();
expect(actual).toEqual([
{
id: 'sleepy',
@@ -1095,8 +1105,8 @@ describe('DocumentMigrator', () => {
it('in a non-default space', () => {
const actual = migrator.migrateAndConvert({ ...obj, namespace: 'foo-namespace' });
- expect(mockUuidv5).toHaveBeenCalledTimes(1);
- expect(mockUuidv5).toHaveBeenCalledWith('foo-namespace:toy:favorite', 'DNSUUID');
+ expect(mockGetConvertedObjectId).toHaveBeenCalledTimes(1);
+ expect(mockGetConvertedObjectId).toHaveBeenCalledWith('foo-namespace', 'toy', 'favorite');
expect(actual).toEqual([
{
id: 'sleepy',
@@ -1134,7 +1144,7 @@ describe('DocumentMigrator', () => {
it('in the default space', () => {
const actual = migrator.migrateAndConvert(obj);
- expect(mockUuidv5).not.toHaveBeenCalled();
+ expect(mockGetConvertedObjectId).not.toHaveBeenCalled();
expect(actual).toEqual([
{
id: 'hungry',
@@ -1149,8 +1159,8 @@ describe('DocumentMigrator', () => {
it('in a non-default space', () => {
const actual = migrator.migrateAndConvert({ ...obj, namespace: 'foo-namespace' });
- expect(mockUuidv5).toHaveBeenCalledTimes(1);
- expect(mockUuidv5).toHaveBeenCalledWith('foo-namespace:dog:hungry', 'DNSUUID');
+ expect(mockGetConvertedObjectId).toHaveBeenCalledTimes(1);
+ expect(mockGetConvertedObjectId).toHaveBeenCalledWith('foo-namespace', 'dog', 'hungry');
expect(actual).toEqual([
{
id: 'uuidv5',
@@ -1204,7 +1214,7 @@ describe('DocumentMigrator', () => {
it('in the default space', () => {
const actual = migrator.migrateAndConvert(obj);
- expect(mockUuidv5).not.toHaveBeenCalled();
+ expect(mockGetConvertedObjectId).not.toHaveBeenCalled();
expect(actual).toEqual([
{
id: 'pretty',
@@ -1220,9 +1230,19 @@ describe('DocumentMigrator', () => {
it('in a non-default space', () => {
const actual = migrator.migrateAndConvert({ ...obj, namespace: 'foo-namespace' });
- expect(mockUuidv5).toHaveBeenCalledTimes(2);
- expect(mockUuidv5).toHaveBeenNthCalledWith(1, 'foo-namespace:toy:favorite', 'DNSUUID');
- expect(mockUuidv5).toHaveBeenNthCalledWith(2, 'foo-namespace:dog:pretty', 'DNSUUID');
+ expect(mockGetConvertedObjectId).toHaveBeenCalledTimes(2);
+ expect(mockGetConvertedObjectId).toHaveBeenNthCalledWith(
+ 1,
+ 'foo-namespace',
+ 'toy',
+ 'favorite'
+ );
+ expect(mockGetConvertedObjectId).toHaveBeenNthCalledWith(
+ 2,
+ 'foo-namespace',
+ 'dog',
+ 'pretty'
+ );
expect(actual).toEqual([
{
id: 'uuidv5',
diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts
index 23f5d075d72e3..040aa81cdab3a 100644
--- a/src/core/server/saved_objects/migrations/core/document_migrator.ts
+++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts
@@ -65,7 +65,7 @@ import { MigrationLogger } from './migration_logger';
import { TransformSavedObjectDocumentError } from '.';
import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry';
import { SavedObjectMigrationFn, SavedObjectMigrationMap } from '../types';
-import { DEFAULT_NAMESPACE_STRING } from '../../service/lib/utils';
+import { DEFAULT_NAMESPACE_STRING, SavedObjectsUtils } from '../../service/lib/utils';
import { LegacyUrlAlias, LEGACY_URL_ALIAS_TYPE } from '../../object_types';
const DEFAULT_MINIMUM_CONVERT_VERSION = '8.0.0';
@@ -556,7 +556,7 @@ function convertNamespaceType(doc: SavedObjectUnsanitizedDoc) {
}
const { id: originId, type } = otherAttrs;
- const id = deterministicallyRegenerateObjectId(namespace, type, originId!);
+ const id = SavedObjectsUtils.getConvertedObjectId(namespace, type, originId!);
if (namespace !== undefined) {
const legacyUrlAlias: SavedObjectUnsanitizedDoc = {
id: `${namespace}:${type}:${originId}`,
@@ -616,7 +616,9 @@ function getReferenceTransforms(typeRegistry: ISavedObjectTypeRegistry): Transfo
references: references.map(({ type, id, ...attrs }) => ({
...attrs,
type,
- id: types.has(type) ? deterministicallyRegenerateObjectId(namespace, type, id) : id,
+ id: types.has(type)
+ ? SavedObjectsUtils.getConvertedObjectId(namespace, type, id)
+ : id,
})),
},
additionalDocs: [],
diff --git a/src/core/server/saved_objects/migrations/core/__mocks__/index.ts b/src/core/server/saved_objects/service/lib/utils.test.mock.ts
similarity index 80%
rename from src/core/server/saved_objects/migrations/core/__mocks__/index.ts
rename to src/core/server/saved_objects/service/lib/utils.test.mock.ts
index d290bc57e2669..9e2b5c66bc91a 100644
--- a/src/core/server/saved_objects/migrations/core/__mocks__/index.ts
+++ b/src/core/server/saved_objects/service/lib/utils.test.mock.ts
@@ -6,8 +6,11 @@
* Side Public License, v 1.
*/
+const mockUuidv1 = jest.fn().mockReturnValue('uuidv1');
+jest.mock('uuid/v1', () => mockUuidv1);
+
const mockUuidv5 = jest.fn().mockReturnValue('uuidv5');
Object.defineProperty(mockUuidv5, 'DNS', { value: 'DNSUUID', writable: false });
jest.mock('uuid/v5', () => mockUuidv5);
-export { mockUuidv5 };
+export { mockUuidv1, mockUuidv5 };
diff --git a/src/core/server/saved_objects/service/lib/utils.test.ts b/src/core/server/saved_objects/service/lib/utils.test.ts
index 7101eafc72a32..0f502d8bfb9f1 100644
--- a/src/core/server/saved_objects/service/lib/utils.test.ts
+++ b/src/core/server/saved_objects/service/lib/utils.test.ts
@@ -6,14 +6,11 @@
* Side Public License, v 1.
*/
-import uuid from 'uuid';
+import { mockUuidv1, mockUuidv5 } from './utils.test.mock';
+
import { SavedObjectsFindOptions } from '../../types';
import { SavedObjectsUtils } from './utils';
-jest.mock('uuid', () => ({
- v1: jest.fn().mockReturnValue('mock-uuid'),
-}));
-
describe('SavedObjectsUtils', () => {
const {
namespaceIdToString,
@@ -21,6 +18,7 @@ describe('SavedObjectsUtils', () => {
createEmptyFindResponse,
generateId,
isRandomId,
+ getConvertedObjectId,
} = SavedObjectsUtils;
describe('#namespaceIdToString', () => {
@@ -80,8 +78,8 @@ describe('SavedObjectsUtils', () => {
describe('#generateId', () => {
it('returns a valid uuid', () => {
- expect(generateId()).toBe('mock-uuid');
- expect(uuid.v1).toHaveBeenCalled();
+ expect(generateId()).toBe('uuidv1'); // default return value for mockUuidv1
+ expect(mockUuidv1).toHaveBeenCalled();
});
});
@@ -93,4 +91,20 @@ describe('SavedObjectsUtils', () => {
expect(isRandomId(undefined)).toBe(false);
});
});
+
+ describe('#getConvertedObjectId', () => {
+ it('does not change the object ID if namespace is undefined or "default"', () => {
+ for (const namespace of [undefined, 'default']) {
+ const result = getConvertedObjectId(namespace, 'type', 'oldId');
+ expect(result).toBe('oldId');
+ expect(mockUuidv5).not.toHaveBeenCalled();
+ }
+ });
+
+ it('changes the object ID if namespace is defined and not "default"', () => {
+ const result = getConvertedObjectId('namespace', 'type', 'oldId');
+ expect(result).toBe('uuidv5'); // default return value for mockUuidv5
+ expect(mockUuidv5).toHaveBeenCalledWith('namespace:type:oldId', 'DNSUUID');
+ });
+ });
});
diff --git a/src/core/server/saved_objects/service/lib/utils.ts b/src/core/server/saved_objects/service/lib/utils.ts
index 494ac6ce9fad5..6942b3b376232 100644
--- a/src/core/server/saved_objects/service/lib/utils.ts
+++ b/src/core/server/saved_objects/service/lib/utils.ts
@@ -6,7 +6,8 @@
* Side Public License, v 1.
*/
-import uuid from 'uuid';
+import uuidv1 from 'uuid/v1';
+import uuidv5 from 'uuid/v5';
import { SavedObjectsFindOptions } from '../../types';
import { SavedObjectsFindResponse } from '..';
@@ -65,7 +66,7 @@ export class SavedObjectsUtils {
* Generates a random ID for a saved objects.
*/
public static generateId() {
- return uuid.v1();
+ return uuidv1();
}
/**
@@ -77,4 +78,19 @@ export class SavedObjectsUtils {
public static isRandomId(id: string | undefined) {
return typeof id === 'string' && UUID_REGEX.test(id);
}
+
+ /**
+ * Uses a single-namespace object's "legacy ID" to determine what its new ID will be after it is converted to a multi-namespace type.
+ *
+ * @param {string} namespace The namespace of the saved object before it is converted.
+ * @param {string} type The type of the saved object before it is converted.
+ * @param {string} id The ID of the saved object before it is converted.
+ * @returns {string} The ID of the saved object after it is converted.
+ */
+ public static getConvertedObjectId(namespace: string | undefined, type: string, id: string) {
+ if (SavedObjectsUtils.namespaceIdToString(namespace) === DEFAULT_NAMESPACE_STRING) {
+ return id; // Objects that exist in the Default space do not get new IDs when they are converted.
+ }
+ return uuidv5(`${namespace}:${type}:${id}`, uuidv5.DNS); // The uuidv5 namespace constant (uuidv5.DNS) is arbitrary.
+ }
}
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index fbc51acfdc8ef..7f2ce38a5bdd4 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -3163,6 +3163,7 @@ export interface SavedObjectsUpdateResponse extends Omit({ page, perPage, }: SavedObjectsFindOptions) => SavedObjectsFindResponse;
static generateId(): string;
+ static getConvertedObjectId(namespace: string | undefined, type: string, id: string): string;
static isRandomId(id: string | undefined): boolean;
static namespaceIdToString: (namespace?: string | undefined) => string;
static namespaceStringToId: (namespace: string) => string | undefined;