persistable state migrations (#103680) (#104764)

This commit is contained in:
Peter Pisljar 2021-07-08 00:53:05 +02:00 committed by GitHub
parent cba02df513
commit 026a7b4956
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 470 additions and 103 deletions

View file

@ -1,11 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-embeddable-server](./kibana-plugin-plugins-embeddable-server.md) &gt; [EmbeddableSetup](./kibana-plugin-plugins-embeddable-server.embeddablesetup.md) &gt; [getMigrationVersions](./kibana-plugin-plugins-embeddable-server.embeddablesetup.getmigrationversions.md)
[Home](./index.md) &gt; [kibana-plugin-plugins-embeddable-server](./kibana-plugin-plugins-embeddable-server.md) &gt; [EmbeddableSetup](./kibana-plugin-plugins-embeddable-server.embeddablesetup.md) &gt; [getAllMigrations](./kibana-plugin-plugins-embeddable-server.embeddablesetup.getallmigrations.md)
## EmbeddableSetup.getMigrationVersions property
## EmbeddableSetup.getAllMigrations property
<b>Signature:</b>
```typescript
getMigrationVersions: () => string[];
getAllMigrations: () => MigrateFunctionsObject;
```

View file

@ -14,7 +14,7 @@ export interface EmbeddableSetup extends PersistableStateService<EmbeddableState
| Property | Type | Description |
| --- | --- | --- |
| [getMigrationVersions](./kibana-plugin-plugins-embeddable-server.embeddablesetup.getmigrationversions.md) | <code>() =&gt; string[]</code> | |
| [getAllMigrations](./kibana-plugin-plugins-embeddable-server.embeddablesetup.getallmigrations.md) | <code>() =&gt; MigrateFunctionsObject</code> | |
| [registerEmbeddableFactory](./kibana-plugin-plugins-embeddable-server.embeddablesetup.registerembeddablefactory.md) | <code>(factory: EmbeddableRegistryDefinition) =&gt; void</code> | |
| [registerEnhancement](./kibana-plugin-plugins-embeddable-server.embeddablesetup.registerenhancement.md) | <code>(enhancement: EnhancementRegistryDefinition) =&gt; void</code> | |

View file

@ -17,6 +17,8 @@ export { TODO_EMBEDDABLE, TodoEmbeddableFactory } from './todo';
export { BOOK_EMBEDDABLE } from './book';
export { SIMPLE_EMBEDDABLE } from './migrations';
import { EmbeddableExamplesPlugin } from './plugin';
export {

View file

@ -0,0 +1,10 @@
/*
* 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.
*/
export * from './migrations_embeddable';
export * from './migrations_embeddable_factory';

View file

@ -0,0 +1,30 @@
/*
* 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.
*/
import { MigrateFunction } from '../../../../src/plugins/kibana_utils/common/persistable_state';
import { SimpleEmbeddableInput } from './migrations_embeddable_factory';
import { EmbeddableInput } from '../../../../src/plugins/embeddable/common';
// before 7.3.0 this embeddable received a very simple input with a variable named `number`
// eslint-disable-next-line @typescript-eslint/naming-convention
type SimpleEmbeddableInput_pre7_3_0 = EmbeddableInput & {
number: number;
};
type SimpleEmbeddable730MigrateFn = MigrateFunction<
SimpleEmbeddableInput_pre7_3_0,
SimpleEmbeddableInput
>;
// when migrating old state we'll need to set a default title, or we should make title optional in the new state
const defaultTitle = 'no title';
export const migration730: SimpleEmbeddable730MigrateFn = (state) => {
const newState: SimpleEmbeddableInput = { ...state, title: defaultTitle, value: state.number };
return newState;
};

View file

@ -0,0 +1,45 @@
/*
* 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.
*/
import { SIMPLE_EMBEDDABLE, SimpleEmbeddableInput } from '.';
import { Embeddable, IContainer } from '../../../../src/plugins/embeddable/public';
export class SimpleEmbeddable extends Embeddable<SimpleEmbeddableInput> {
// The type of this embeddable. This will be used to find the appropriate factory
// to instantiate this kind of embeddable.
public readonly type = SIMPLE_EMBEDDABLE;
constructor(initialInput: SimpleEmbeddableInput, parent?: IContainer) {
super(
// Input state is irrelevant to this embeddable, just pass it along.
initialInput,
// Initial output state - this embeddable does not do anything with output, so just
// pass along an empty object.
{},
// Optional parent component, this embeddable can optionally be rendered inside a container.
parent
);
}
/**
* Render yourself at the dom node using whatever framework you like, angular, react, or just plain
* vanilla js.
* @param node
*/
public render(node: HTMLElement) {
const input = this.getInput();
// eslint-disable-next-line no-unsanitized/property
node.innerHTML = `<div data-test-subj="simpleEmbeddable">${input.title} ${input.value}</div>`;
}
/**
* This is mostly relevant for time based embeddables which need to update data
* even if EmbeddableInput has not changed at all.
*/
public reload() {}
}

View file

@ -0,0 +1,55 @@
/*
* 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.
*/
import { i18n } from '@kbn/i18n';
import {
IContainer,
EmbeddableInput,
EmbeddableFactoryDefinition,
EmbeddableFactory,
} from '../../../../src/plugins/embeddable/public';
import { SimpleEmbeddable } from './migrations_embeddable';
import { migration730 } from './migration.7.3.0';
export const SIMPLE_EMBEDDABLE = 'SIMPLE_EMBEDDABLE';
// in 7.3.0 we added `title` to the input and renamed the `number` variable to `value`
export type SimpleEmbeddableInput = EmbeddableInput & {
title: string;
value: number;
};
export type SimpleEmbeddableFactory = EmbeddableFactory;
export class SimpleEmbeddableFactoryDefinition
implements EmbeddableFactoryDefinition<SimpleEmbeddableInput> {
public readonly type = SIMPLE_EMBEDDABLE;
// we need to provide migration function every time we change the interface of our state
public readonly migrations = {
'7.3.0': migration730,
};
/**
* In our simple example, we let everyone have permissions to edit this. Most
* embeddables should check the UI Capabilities service to be sure of
* the right permissions.
*/
public async isEditable() {
return true;
}
public async create(initialInput: SimpleEmbeddableInput, parent?: IContainer) {
return new SimpleEmbeddable(initialInput, parent);
}
public getDisplayName() {
return i18n.translate('embeddableExamples.migrations.displayName', {
defaultMessage: 'hello world',
});
}
}

View file

@ -49,6 +49,11 @@ import {
import { UiActionsStart } from '../../../src/plugins/ui_actions/public';
import { createAddBookToLibraryAction } from './book/add_book_to_library_action';
import { createUnlinkBookFromLibraryAction } from './book/unlink_book_from_library_action';
import {
SIMPLE_EMBEDDABLE,
SimpleEmbeddableFactory,
SimpleEmbeddableFactoryDefinition,
} from './migrations';
export interface EmbeddableExamplesSetupDependencies {
embeddable: EmbeddableSetup;
@ -68,6 +73,7 @@ interface ExampleEmbeddableFactories {
getTodoEmbeddableFactory: () => TodoEmbeddableFactory;
getTodoRefEmbeddableFactory: () => TodoRefEmbeddableFactory;
getBookEmbeddableFactory: () => BookEmbeddableFactory;
getMigrationsEmbeddableFactory: () => SimpleEmbeddableFactory;
}
export interface EmbeddableExamplesStart {
@ -94,6 +100,11 @@ export class EmbeddableExamplesPlugin
new HelloWorldEmbeddableFactoryDefinition()
);
this.exampleEmbeddableFactories.getMigrationsEmbeddableFactory = deps.embeddable.registerEmbeddableFactory(
SIMPLE_EMBEDDABLE,
new SimpleEmbeddableFactoryDefinition()
);
this.exampleEmbeddableFactories.getMultiTaskTodoEmbeddableFactory = deps.embeddable.registerEmbeddableFactory(
MULTI_TASK_TODO_EMBEDDABLE,
new MultiTaskTodoEmbeddableFactoryDefinition()

View file

@ -9,11 +9,19 @@
import { Plugin, CoreSetup, CoreStart } from 'kibana/server';
import { todoSavedObject } from './todo_saved_object';
import { bookSavedObject } from './book_saved_object';
import { searchableListSavedObject } from './searchable_list_saved_object';
import { EmbeddableSetup } from '../../../src/plugins/embeddable/server';
export class EmbeddableExamplesPlugin implements Plugin {
public setup(core: CoreSetup) {
export interface EmbeddableExamplesSetupDependencies {
embeddable: EmbeddableSetup;
}
export class EmbeddableExamplesPlugin
implements Plugin<void, void, EmbeddableExamplesSetupDependencies> {
public setup(core: CoreSetup, { embeddable }: EmbeddableExamplesSetupDependencies) {
core.savedObjects.registerType(todoSavedObject);
core.savedObjects.registerType(bookSavedObject);
core.savedObjects.registerType(searchableListSavedObject(embeddable));
}
public start(core: CoreStart) {}

View file

@ -0,0 +1,43 @@
/*
* 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.
*/
import { mapValues } from 'lodash';
import { SavedObjectsType, SavedObjectUnsanitizedDoc } from 'kibana/server';
import { EmbeddableSetup } from '../../../src/plugins/embeddable/server';
export const searchableListSavedObject = (embeddable: EmbeddableSetup) => {
return {
name: 'searchableList',
hidden: false,
namespaceType: 'single',
management: {
icon: 'visualizeApp',
defaultSearchField: 'title',
importableAndExportable: true,
getTitle(obj: any) {
return obj.attributes.title;
},
},
mappings: {
properties: {
title: { type: 'text' },
version: { type: 'integer' },
},
},
migrations: () => {
// we assume all the migration will be done by embeddables service and that saved object holds no extra state besides that of searchable list embeddable input\
// if saved object would hold additional information we would need to merge the response from embeddables.getAllMigrations with our custom migrations.
return mapValues(embeddable.getAllMigrations(), (migrate) => {
return (state: SavedObjectUnsanitizedDoc) => ({
...state,
attributes: migrate(state.attributes),
});
});
},
} as SavedObjectsType;
};

View file

@ -39,7 +39,7 @@ const injectImplementation = (
};
embeddableSetupMock.extract.mockImplementation(extractImplementation);
embeddableSetupMock.inject.mockImplementation(injectImplementation);
embeddableSetupMock.getMigrationVersions.mockImplementation(() => []);
embeddableSetupMock.getAllMigrations.mockImplementation(() => ({}));
const migrations = createDashboardSavedObjectTypeMigrations({
embeddable: embeddableSetupMock,
@ -586,28 +586,14 @@ describe('dashboard', () => {
type: 'dashboard',
};
it('should add all embeddable migrations for versions above 7.12.0 to dashboard saved object migrations', () => {
const newEmbeddableSetupMock = createEmbeddableSetupMock();
newEmbeddableSetupMock.getMigrationVersions.mockImplementation(() => [
'7.10.100',
'7.13.0',
'8.0.0',
]);
const migrationsList = createDashboardSavedObjectTypeMigrations({
embeddable: newEmbeddableSetupMock,
});
expect(Object.keys(migrationsList).indexOf('8.0.0')).not.toBe(-1);
expect(Object.keys(migrationsList).indexOf('7.13.0')).not.toBe(-1);
expect(Object.keys(migrationsList).indexOf('7.10.100')).toBe(-1);
});
it('runs migrations on by value panels only', () => {
const newEmbeddableSetupMock = createEmbeddableSetupMock();
newEmbeddableSetupMock.getMigrationVersions.mockImplementation(() => ['7.13.0']);
newEmbeddableSetupMock.migrate.mockImplementation((state: SerializableState) => {
state.superCoolKey = 'ONLY 4 BY VALUE EMBEDDABLES THANK YOU VERY MUCH';
return state;
});
newEmbeddableSetupMock.getAllMigrations.mockImplementation(() => ({
'7.13.0': (state: SerializableState) => {
state.superCoolKey = 'ONLY 4 BY VALUE EMBEDDABLES THANK YOU VERY MUCH';
return state;
},
}));
const migrationsList = createDashboardSavedObjectTypeMigrations({
embeddable: newEmbeddableSetupMock,
});

View file

@ -6,8 +6,7 @@
* Side Public License, v 1.
*/
import semver from 'semver';
import { get, flow, identity } from 'lodash';
import { get, flow, mapValues } from 'lodash';
import {
SavedObjectAttributes,
SavedObjectMigrationFn,
@ -26,7 +25,12 @@ import {
} from '../../common/embeddable/embeddable_saved_object_converters';
import { SavedObjectEmbeddableInput } from '../../../embeddable/common';
import { INDEX_PATTERN_SAVED_OBJECT_TYPE } from '../../../data/common';
import { SerializableValue } from '../../../kibana_utils/common';
import {
mergeMigrationFunctionMaps,
MigrateFunction,
MigrateFunctionsObject,
SerializableValue,
} from '../../../kibana_utils/common';
import { replaceIndexPatternReference } from './replace_index_pattern_reference';
function migrateIndexPattern(doc: DashboardDoc700To720) {
@ -156,7 +160,7 @@ type ValueOrReferenceInput = SavedObjectEmbeddableInput & {
// Runs the embeddable migrations on each panel
const migrateByValuePanels = (
deps: DashboardSavedObjectTypeMigrationsDeps,
migrate: MigrateFunction,
version: string
): SavedObjectMigrationFn => (doc: any) => {
const { attributes } = doc;
@ -179,13 +183,10 @@ const migrateByValuePanels = (
// saved vis is used to store by value input for Visualize. This should eventually be renamed to `attributes` to align with Lens and Maps
if (originalPanelState.explicitInput.attributes || originalPanelState.explicitInput.savedVis) {
// If this panel is by value, migrate the state using embeddable migrations
const migratedInput = deps.embeddable.migrate(
{
...originalPanelState.explicitInput,
type: originalPanelState.type,
},
version
);
const migratedInput = migrate({
...originalPanelState.explicitInput,
type: originalPanelState.type,
});
// Convert the embeddable state back into the panel shape
newPanels.push(
convertPanelStateToSavedDashboardPanel(
@ -216,16 +217,12 @@ export interface DashboardSavedObjectTypeMigrationsDeps {
export const createDashboardSavedObjectTypeMigrations = (
deps: DashboardSavedObjectTypeMigrationsDeps
): SavedObjectMigrationMap => {
const embeddableMigrations = Object.fromEntries(
deps.embeddable
.getMigrationVersions()
.filter((version) => semver.gt(version, '7.12.0'))
.map((version): [string, SavedObjectMigrationFn] => {
return [version, migrateByValuePanels(deps, version)];
})
);
const embeddableMigrations = mapValues<MigrateFunctionsObject, SavedObjectMigrationFn>(
deps.embeddable.getAllMigrations(),
migrateByValuePanels
) as MigrateFunctionsObject;
return {
const dashboardMigrations = {
/**
* We need to have this migration twice, once with a version prior to 7.0.0 once with a version
* after it. The reason for that is, that this migration has been introduced once 7.0.0 was already
@ -242,14 +239,14 @@ export const createDashboardSavedObjectTypeMigrations = (
'7.9.3': flow(migrateMatchAllQuery),
'7.11.0': flow(createExtractPanelReferencesMigration(deps)),
...embeddableMigrations,
/**
* Any dashboard saved object migrations that come after this point will have to be wary of
* potentially overwriting embeddable migrations. An example of how to mitigate this follows:
*/
// '7.x': flow(yourNewMigrationFunction, embeddableMigrations['7.x'] ?? identity),
'7.14.0': flow(replaceIndexPatternReference, embeddableMigrations['7.14.0'] ?? identity),
'7.14.0': flow(replaceIndexPatternReference),
};
return mergeMigrationFunctionMaps(dashboardMigrations, embeddableMigrations);
};

View file

@ -0,0 +1,34 @@
/*
* 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.
*/
import { getAllMigrations } from './get_all_migrations';
describe('embeddable getAllMigratons', () => {
const factories = [{ migrations: { '7.11.0': (state: any) => state } }];
const enhacements = [{ migrations: { '7.12.0': (state: any) => state } }];
const migrateFn = jest.fn();
test('returns base migrations', () => {
expect(getAllMigrations([], [], migrateFn)).toEqual({});
});
test('returns embeddable factory migrations', () => {
expect(getAllMigrations(factories as any, [], migrateFn)).toHaveProperty(['7.11.0']);
});
test('returns enhancement migrations', () => {
const migrations = getAllMigrations([], enhacements as any, migrateFn);
expect(migrations).toHaveProperty(['7.12.0']);
});
test('returns all migrations', () => {
const migrations = getAllMigrations(factories as any, enhacements as any, migrateFn);
expect(migrations).toHaveProperty(['7.11.0']);
expect(migrations).toHaveProperty(['7.12.0']);
});
});

View file

@ -0,0 +1,44 @@
/*
* 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.
*/
import { baseEmbeddableMigrations } from './migrate_base_input';
import {
MigrateFunctionsObject,
PersistableState,
PersistableStateMigrateFn,
} from '../../../kibana_utils/common/persistable_state';
export const getAllMigrations = (
factories: unknown[],
enhancements: unknown[],
migrateFn: PersistableStateMigrateFn
) => {
const uniqueVersions = new Set<string>();
for (const baseMigrationVersion of Object.keys(baseEmbeddableMigrations)) {
uniqueVersions.add(baseMigrationVersion);
}
for (const factory of factories) {
Object.keys((factory as PersistableState).migrations).forEach((version) =>
uniqueVersions.add(version)
);
}
for (const enhancement of enhancements) {
Object.keys((enhancement as PersistableState).migrations).forEach((version) =>
uniqueVersions.add(version)
);
}
const migrations: MigrateFunctionsObject = {};
uniqueVersions.forEach((version) => {
migrations[version] = (state) => ({
...migrateFn(state, version),
});
});
return migrations;
};

View file

@ -10,8 +10,10 @@ import { CommonEmbeddableStartContract } from '../types';
import { baseEmbeddableMigrations } from './migrate_base_input';
import { SerializableState } from '../../../kibana_utils/common/persistable_state';
export type MigrateFunction = (state: SerializableState, version: string) => SerializableState;
export const getMigrateFunction = (embeddables: CommonEmbeddableStartContract) => {
return (state: SerializableState, version: string) => {
const migrateFn: MigrateFunction = (state: SerializableState, version: string) => {
const enhancements = (state.enhancements as SerializableState) || {};
const factory = embeddables.getEmbeddableFactory(state.type as string);
@ -19,10 +21,16 @@ export const getMigrateFunction = (embeddables: CommonEmbeddableStartContract) =
? baseEmbeddableMigrations[version](state)
: state;
if (factory && factory.migrations[version]) {
if (factory?.migrations[version]) {
updatedInput = factory.migrations[version](updatedInput);
}
if (factory?.isContainerType) {
updatedInput.panels = ((state.panels as SerializableState[]) || []).map((panel) => {
return migrateFn(panel, version);
});
}
updatedInput.enhancements = {};
Object.keys(enhancements).forEach((key) => {
if (!enhancements[key]) return;
@ -35,4 +43,6 @@ export const getMigrateFunction = (embeddables: CommonEmbeddableStartContract) =
return updatedInput;
};
return migrateFn;
};

View file

@ -12,7 +12,7 @@ export const createEmbeddablePersistableStateServiceMock = (): jest.Mocked<Embed
return {
inject: jest.fn((state, references) => state),
extract: jest.fn((state) => ({ state, references: [] })),
migrate: jest.fn((state, version) => state),
getAllMigrations: jest.fn(() => ({})),
telemetry: jest.fn((state, collector) => ({})),
};
};

View file

@ -113,7 +113,7 @@ const createStartContract = (): Start => {
telemetry: jest.fn(),
extract: jest.fn(),
inject: jest.fn(),
migrate: jest.fn(),
getAllMigrations: jest.fn(),
EmbeddablePanel: jest.fn(),
getStateTransfer: jest.fn(() => createEmbeddableStateTransferMock() as EmbeddableStateTransfer),
getAttributeService: jest.fn(),

View file

@ -106,8 +106,34 @@ describe('embeddable factory', () => {
my: 'state',
} as any;
const containerEmbeddableFactoryId = 'CONTAINER';
const containerEmbeddableFactory = {
type: containerEmbeddableFactoryId,
create: jest.fn(),
getDisplayName: () => 'Container',
isContainer: true,
isEditable: () => Promise.resolve(true),
extract: jest.fn().mockImplementation((state) => ({ state, references: [] })),
inject: jest.fn().mockImplementation((state) => state),
telemetry: jest.fn().mockResolvedValue({}),
migrations: { '7.12.0': jest.fn().mockImplementation((state) => state) },
};
const containerState = {
id: containerEmbeddableFactoryId,
type: containerEmbeddableFactoryId,
some: 'state',
panels: [
{
...embeddableState,
},
],
} as any;
setup.registerEmbeddableFactory(embeddableFactoryId, embeddableFactory);
setup.registerEmbeddableFactory(containerEmbeddableFactoryId, containerEmbeddableFactory);
test('cannot register embeddable factory with the same ID', async () => {
setup.registerEmbeddableFactory(embeddableFactoryId, embeddableFactory);
expect(() =>
setup.registerEmbeddableFactory(embeddableFactoryId, embeddableFactory)
).toThrowError(
@ -131,7 +157,12 @@ describe('embeddable factory', () => {
});
test('embeddableFactory migrate function gets called when calling embeddable migrate', () => {
start.migrate(embeddableState, '7.11.0');
start.getAllMigrations!()['7.11.0']!(embeddableState);
expect(embeddableFactory.migrations['7.11.0']).toBeCalledWith(embeddableState);
});
test('panels inside container get automatically migrated when migrating conta1iner', () => {
start.getAllMigrations!()['7.11.0']!(containerState);
expect(embeddableFactory.migrations['7.11.0']).toBeCalledWith(embeddableState);
});
});
@ -156,8 +187,9 @@ describe('embeddable enhancements', () => {
},
} as any;
setup.registerEnhancement(embeddableEnhancement);
test('cannot register embeddable enhancement with the same ID', async () => {
setup.registerEnhancement(embeddableEnhancement);
expect(() => setup.registerEnhancement(embeddableEnhancement)).toThrowError(
'enhancement with id test already exists in the registry'
);
@ -179,7 +211,7 @@ describe('embeddable enhancements', () => {
});
test('enhancement migrate function gets called when calling embeddable migrate', () => {
start.migrate(embeddableState, '7.11.0');
start.getAllMigrations!()['7.11.0']!(embeddableState);
expect(embeddableEnhancement.migrations['7.11.0']).toBeCalledWith(
embeddableState.enhancements.test
);
@ -187,9 +219,9 @@ describe('embeddable enhancements', () => {
test('doesnt fail if there is no migration function registered for specific version', () => {
expect(() => {
start.migrate(embeddableState, '7.10.0');
start.getAllMigrations!()['7.11.0']!(embeddableState);
}).not.toThrow();
expect(start.migrate(embeddableState, '7.10.0')).toEqual(embeddableState);
expect(start.getAllMigrations!()['7.11.0']!(embeddableState)).toEqual(embeddableState);
});
});

View file

@ -49,6 +49,7 @@ import {
getMigrateFunction,
getTelemetryFunction,
} from '../common/lib';
import { getAllMigrations } from '../common/lib/get_all_migrations';
export interface EmbeddableSetupDependencies {
uiActions: UiActionsSetup;
@ -205,7 +206,12 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
telemetry: getTelemetryFunction(commonContract),
extract: getExtractFunction(commonContract),
inject: getInjectFunction(commonContract),
migrate: getMigrateFunction(commonContract),
getAllMigrations: () =>
getAllMigrations(
Array.from(this.embeddableFactories.values()),
Array.from(this.enhancements.values()),
getMigrateFunction(commonContract)
),
};
}

View file

@ -12,7 +12,7 @@ import { EmbeddableSetup, EmbeddableStart } from './plugin';
export const createEmbeddableSetupMock = (): jest.Mocked<EmbeddableSetup> => ({
...createEmbeddablePersistableStateServiceMock(),
registerEmbeddableFactory: jest.fn(),
getMigrationVersions: jest.fn().mockReturnValue([]),
getAllMigrations: jest.fn().mockReturnValue({}),
registerEnhancement: jest.fn(),
});

View file

@ -16,19 +16,24 @@ import {
EmbeddableRegistryDefinition,
} from './types';
import {
baseEmbeddableMigrations,
getExtractFunction,
getInjectFunction,
getMigrateFunction,
getTelemetryFunction,
} from '../common/lib';
import { PersistableStateService, SerializableState } from '../../kibana_utils/common';
import {
PersistableStateService,
SerializableState,
PersistableStateMigrateFn,
MigrateFunctionsObject,
} from '../../kibana_utils/common';
import { EmbeddableStateWithType } from '../common/types';
import { getAllMigrations } from '../common/lib/get_all_migrations';
export interface EmbeddableSetup extends PersistableStateService<EmbeddableStateWithType> {
registerEmbeddableFactory: (factory: EmbeddableRegistryDefinition) => void;
registerEnhancement: (enhancement: EnhancementRegistryDefinition) => void;
getMigrationVersions: () => string[];
getAllMigrations: () => MigrateFunctionsObject;
}
export type EmbeddableStart = PersistableStateService<EmbeddableStateWithType>;
@ -36,20 +41,27 @@ export type EmbeddableStart = PersistableStateService<EmbeddableStateWithType>;
export class EmbeddableServerPlugin implements Plugin<EmbeddableSetup, EmbeddableStart> {
private readonly embeddableFactories: EmbeddableFactoryRegistry = new Map();
private readonly enhancements: EnhancementsRegistry = new Map();
private migrateFn: PersistableStateMigrateFn | undefined;
public setup(core: CoreSetup) {
const commonContract = {
getEmbeddableFactory: this.getEmbeddableFactory,
getEnhancement: this.getEnhancement,
};
this.migrateFn = getMigrateFunction(commonContract);
return {
getMigrationVersions: this.getMigrationVersions,
registerEmbeddableFactory: this.registerEmbeddableFactory,
registerEnhancement: this.registerEnhancement,
telemetry: getTelemetryFunction(commonContract),
extract: getExtractFunction(commonContract),
inject: getInjectFunction(commonContract),
migrate: getMigrateFunction(commonContract),
getAllMigrations: () =>
getAllMigrations(
Array.from(this.embeddableFactories.values()),
Array.from(this.enhancements.values()),
this.migrateFn!
),
};
}
@ -63,7 +75,12 @@ export class EmbeddableServerPlugin implements Plugin<EmbeddableSetup, Embeddabl
telemetry: getTelemetryFunction(commonContract),
extract: getExtractFunction(commonContract),
inject: getInjectFunction(commonContract),
migrate: getMigrateFunction(commonContract),
getAllMigrations: () =>
getAllMigrations(
Array.from(this.embeddableFactories.values()),
Array.from(this.enhancements.values()),
this.migrateFn!
),
};
}
@ -128,20 +145,4 @@ export class EmbeddableServerPlugin implements Plugin<EmbeddableSetup, Embeddabl
}
);
};
private getMigrationVersions = () => {
const uniqueVersions = new Set<string>();
for (const baseMigrationVersion of Object.keys(baseEmbeddableMigrations)) {
uniqueVersions.add(baseMigrationVersion);
}
const factories = this.embeddableFactories.values();
for (const factory of factories) {
Object.keys(factory.migrations).forEach((version) => uniqueVersions.add(version));
}
const enhancements = this.enhancements.values();
for (const enhancement of enhancements) {
Object.keys(enhancement.migrations).forEach((version) => uniqueVersions.add(version));
}
return Array.from(uniqueVersions);
};
}

View file

@ -23,8 +23,10 @@ export interface EmbeddableRegistryDefinition<P extends EmbeddableStateWithType
//
// @public (undocumented)
export interface EmbeddableSetup extends PersistableStateService<EmbeddableStateWithType> {
// Warning: (ae-forgotten-export) The symbol "MigrateFunctionsObject" needs to be exported by the entry point index.d.ts
//
// (undocumented)
getMigrationVersions: () => string[];
getAllMigrations: () => MigrateFunctionsObject;
// (undocumented)
registerEmbeddableFactory: (factory: EmbeddableRegistryDefinition) => void;
// (undocumented)

View file

@ -8,3 +8,4 @@
export * from './types';
export { migrateToLatest } from './migrate_to_latest';
export { mergeMigrationFunctionMaps } from './merge_migration_function_map';

View file

@ -0,0 +1,28 @@
/*
* 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.
*/
import { mergeMigrationFunctionMaps } from './merge_migration_function_map';
describe('mergeSavedObjectMigrationMaps', () => {
const obj1 = {
'7.12.1': (state: number) => state + 1,
'7.12.2': (state: number) => state + 2,
};
const obj2 = {
'7.12.0': (state: number) => state - 2,
'7.12.2': (state: number) => state + 2,
};
test('correctly merges two saved object migration maps', () => {
const result = mergeMigrationFunctionMaps(obj1, obj2);
expect(result['7.12.0'](5)).toEqual(3);
expect(result['7.12.1'](5)).toEqual(6);
expect(result['7.12.2'](5)).toEqual(9);
});
});

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 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.
*/
import { mergeWith } from 'lodash';
import { MigrateFunctionsObject, MigrateFunction, SerializableState } from './types';
export const mergeMigrationFunctionMaps = (
obj1: MigrateFunctionsObject,
obj2: MigrateFunctionsObject
) => {
const customizer = (objValue: MigrateFunction, srcValue: MigrateFunction) => {
if (!srcValue || !objValue) {
return srcValue || objValue;
}
return (state: SerializableState) => objValue(srcValue(state));
};
return mergeWith({ ...obj1 }, obj2, customizer);
};

View file

@ -99,12 +99,22 @@ export interface PersistableState<P extends SerializableState = SerializableStat
* accumulated over time. Migration functions are keyed using semver version
* of Kibana releases.
*/
export type MigrateFunctionsObject = { [semver: string]: MigrateFunction };
export type MigrateFunctionsObject = { [semver: string]: MigrateFunction<any, any> };
export type MigrateFunction<
FromVersion extends SerializableState = SerializableState,
ToVersion extends SerializableState = SerializableState
> = (state: FromVersion) => ToVersion;
/**
* migrate function runs the specified migration
* @param state
* @param version
*/
export type PersistableStateMigrateFn = (
state: SerializableState,
version: string
) => SerializableState;
/**
* @todo Shall we remove this?
*/
@ -150,23 +160,6 @@ export interface PersistableStateService<P extends SerializableState = Serializa
*/
extract(state: P): { state: P; references: SavedObjectReference[] };
/**
* Migrate function runs a specified migration of a {@link PersistableState}
* item.
*
* When using this method it is up to consumer to make sure that the
* migration function are executed in the right semver order. To avoid such
* potentially error prone complexity, prefer using `migrateToLatest` method
* instead.
*
* @param state The old persistable state serializable state object, which
* needs a migration.
* @param version Semver version of the migration to execute.
* @returns Persistable state object updated with the specified migration
* applied to it.
*/
migrate(state: SerializableState, version: string): SerializableState;
/**
* A function which receives the state of an older object and version and
* should migrate the state of the object to the latest possible version using
@ -177,4 +170,9 @@ export interface PersistableStateService<P extends SerializableState = Serializa
* @returns A serializable state object migrated to the latest state.
*/
migrateToLatest?: (state: VersionedState) => VersionedState<P>;
/**
* returns all registered migrations
*/
getAllMigrations?: () => MigrateFunctionsObject;
}