Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SavedObjectsUtils.getConvertedObjectId function #107767

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsUtils](./kibana-plugin-core-server.savedobjectsutils.md) &gt; [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.

<b>Signature:</b>

```typescript
static getConvertedObjectId(namespace: string | undefined, type: string, id: string): string;
```

## Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| namespace | <code>string &#124; undefined</code> | |
| type | <code>string</code> | |
| id | <code>string</code> | |

<b>Returns:</b>

Comment on lines +11 to +24
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder why the doc generator didn't pick up the parameter descriptions or the return description 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple ideas:

  • The script doesn't regen the docs if the signature didn't change, so if you add the comments on the params after generating this the first time it may not be populated. You can add the --docs CLI arg to force this
  • I wonder if the {string} type descriptors are a problem? I usually don't use these since that info is already in the TS signature itself. Not sure how our doc generator handles these.
  • It may not matter much, we're about to remove this tooling.

`string`

{<!-- -->string<!-- -->} The ID of the saved object after it is converted.

Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ export declare class SavedObjectsUtils
| Method | Modifiers | Description |
| --- | --- | --- |
| [generateId()](./kibana-plugin-core-server.savedobjectsutils.generateid.md) | <code>static</code> | Generates a random ID for a saved objects. |
| [getConvertedObjectId(namespace, type, id)](./kibana-plugin-core-server.savedobjectsutils.getconvertedobjectid.md) | <code>static</code> | 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) | <code>static</code> | Validates that a saved object ID has been randomly generated. |

Original file line number Diff line number Diff line change
@@ -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 };
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -37,7 +37,7 @@ const createRegistry = (...types: Array<Partial<SavedObjectsType>>) => {
};

beforeEach(() => {
mockUuidv5.mockClear();
mockGetConvertedObjectId.mockClear();
});

describe('DocumentMigrator', () => {
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<LegacyUrlAlias> = {
id: `${namespace}:${type}:${originId}`,
Expand Down Expand Up @@ -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: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
28 changes: 21 additions & 7 deletions src/core/server/saved_objects/service/lib/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,19 @@
* 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,
namespaceStringToId,
createEmptyFindResponse,
generateId,
isRandomId,
getConvertedObjectId,
} = SavedObjectsUtils;

describe('#namespaceIdToString', () => {
Expand Down Expand Up @@ -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();
});
});

Expand All @@ -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');
});
});
});
20 changes: 18 additions & 2 deletions src/core/server/saved_objects/service/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 '..';

Expand Down Expand Up @@ -65,7 +66,7 @@ export class SavedObjectsUtils {
* Generates a random ID for a saved objects.
*/
public static generateId() {
return uuid.v1();
return uuidv1();
}

/**
Expand All @@ -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.
}
}
1 change: 1 addition & 0 deletions src/core/server/server.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -3163,6 +3163,7 @@ export interface SavedObjectsUpdateResponse<T = unknown> extends Omit<SavedObjec
export class SavedObjectsUtils {
static createEmptyFindResponse: <T, A>({ page, perPage, }: SavedObjectsFindOptions) => SavedObjectsFindResponse<T, A>;
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;
Expand Down