- Registrar wants to add a migration to every registry item.
Example: Author of
Embeddable
api wants to migrate baseEmbeddableInput
. - Enhancer wants to add a migration to every registry item.
Author of
EnhancedEmbeddableDrilldowns
wants to migrateEnhancedEmbeddableInput
. - Registrator wants to add a migration to it's specific registry item.
Example: Author of
VisState
wants to migrateVisualizeEmbeddableInput
- We need migrations to hang around forever. Do we need to support calling
injectReferences
andextractReferences
on legacy state, or can we assume these functions will always get the latest migrated state?
There is only a single PersistableStateDefinition
per id, so the registrator needs
to ensure they are manually migrating PersistableState
incrementally, through every
version.
interface PersistableStatePlugin {
setup: () => {
register: (persistableStateDefinition: {
id: string,
migrate: (state: PersistableState, version: string) => PersistableState;
}
}
}
Every registrator could implement this differently, but here is a pretty clean example of how it could be done.
const greetingMigrator: PersistableStateMigrationFn (state: unknown, version: string): State {
let versionStep: string = version;
let state710?: GreetingState710 = version === '7.1.0' ? state as GreetingState710 : undefined;
let state730?: GreetingState730 = version === '7.3.0' ? state as GreetingState730 : undefined;
let state750?: GreetingState750 = version === '7.5.0' ? state as GreetingState750 : undefined;
let stateLatest?: GreetingStateLatest = version > '7.5.0' ? state as GreetingStateLatest : undefined;
if (state710 !=== undefined) {
state730 = greetingMigrator710to730(state as GreetingState710);
}
if (state730 !=== undefined) {
state750 = greetingMigrator730to750(state730);
}
if (state750 !=== undefined) {
stateLatest = greetingMigrator750toLatest(state750);
}
// Do we have to migrate these enhancements at each step above? Or can we operate only on the latest version?
stateLatest.enhancements = stateLatest.enhancements.forEach(key =>
// What do we pass in for "key" here? How do we ensure uniqueness and avoid subtle bugs if an enhancer
// fails to register a PersistableStateDefinition for `key`, and it accidentally matches some other
// PersistableStateDefinition?
persistableStatePlugin.get(key).migrate(
stateLatest.enhancements[key],
// What do we pass in for "version" here?
stateLatest.enhancements[key].version)
);
return stateLatest;
}
persistableStatePlugin.register({ id: 'greeter', migrate: greetingMigrator });
// Because of "unknown", this isn't type safe.
persistableStatePlugin.get('greeter').migrate({ foo: 'hi' }, '7.0');
persistableStatePlugin.get('greeter').migrate({ bar: 'hi' }, '7.0');
// However, debateable, this is inherently not typesafe because you probably will
// never know the version or state shape at compile time. It'll look more like:
persistableStatePlugin.get('greeter').migrate(
savedObject.attributes.greeter.state,
savedObject.attributes.greeter.version
);
PersistableStatePlugin handles the recursive state migrations until there are no more.
interface PersistableStatePlugin {
setup: () => {
register: <State, MigratedState>(persistableStateDefinition: {
id: string;
version: string;
migrate: (state: State) => { migratedState: MigratedState, version: string };
}
}
start: () => ({
afterLoad(id: string, state: State, version: string) => {
const { migratedState, version as latestVersion } = migrateState(id, state, version);
const stateDefinition = this.getPersistableStateDefinitionForVersion(id, version);
return
},
migrateState(id: string, state: State, version: string) => {
let stateDefinition = this.getPersistableStateDefinitionForVersion(id, version);
const { migratedState, version } = stateDefinition.migrate(state);
// Continue to look for migrations until there are no more migrations registered.
while (version != stateDefinition.version) {
stateDefinition = this.getPersistableStateDefinitionForVersion(id, version);
const { migratedState, version } = stateDefinition.migrate(state);
}
return { migratedState, version };
}
})
getPersistableStateDefinitionForVersion(id: string, version: string) {
const definitions = this.persistableStateDefinitions.get(id);
let foundDefinition?: PersistableStateDefinition;
definitions.forEach(definition => {
if (definition.version < version) {
// Potential migrator found. If definitions are registered for 7.0, 7.1 and 7.3, but
// the caller is looking for the definition to use for 7.2, 7.1 is the appropriate
// definition. 7.2 wouldn't be registered if nothing changed since 7.1.
if (!foundDefinition || definition.version > foundDefinition.version ) {
foundDefinition = definition;
}
}
});
if (!foundDefinition) {
throw new Error(`No persistable state handler for id ${id} and version ${version}`);
}
return foundDefinition;
}
}
persistableStatePlugin.register<GreetingState710, GreetingState730>({
id: 'greeter',
version: '7.1.0',
migrate: (state: GreetingState710) => { migratedState: greetingMigrator710to730(state), version: '7.3.0' }
});
persistableStatePlugin.register<GreetingState730, GreetingState750>({
id: 'greeter',
version: '7.3.0',
migrate: (state: GreetingState730) => { migratedState: greetingMigrator730to750(state), version: '7.5.0' }
});
persistableStatePlugin.register<GreetingState750, GreetingStateLatest>({
id: 'greeter',
version: '7.5.0',
migrate: (state: GreetingState750) => { migratedState: greetingMigrator750toLatest(state), version: getLatestKibanaVersion() }
});
persistableStatePlugin.register<GreetingStateLatest>({
id: 'greeter',
version: getLatestKibanaVersion(),
// The "identity" migrator. Just returns the same state - we've reached the latest version.
migrate: (state: GreetingStateLatest) => { migratedState: state, version: getLatestKibanaVersion() }
});
I think the above may solve some of the problems with keeping state in sync. For instance, if you put it all in one function, it requires a lot of coordination on the part of the author of the migrator.
You can see in the code snippet in the next section that there is a question of
What happens if
VisualizeEmbeddableInput
tries to access something off an older version of the baseEmbeddableInput
?
But there are still issues with this version because what is VisualizeEmbeddable
registers a
migration for 7.2 but Embeddable
only has migrations registered for 7.1 and 7.3?
interface EmbeddableInput710 {
title: string;
id: string;
}
interface EmbeddableInput730 {
defaultTitle: string;
id: string;
}
interface EmbeddableInput750 {
defaultTitle: string;
enhancements: Record<string, unknown>;
id: string;
}
function embeddableMigrator710to730(state: EmbeddableInput710) {
return {
...state
defaultPanelTitle: state.title,
title: undefined,
}
}
const embeddableMigrator: PersistableStateMigrationFn (state: { type: string; input: unknown }, version: string): State {
let versionStep: string = version;
let input710?: EmbeddableInput710 = version === '7.1.0' ? state.input as EmbeddableInput710 : undefined;
let input730?: EmbeddableInput730 = version === '7.3.0' ? state.input as EmbeddableInput730 : undefined;
let input750?: EmbeddableInput750 = version === '7.5.0' ? state.input as EmbeddableInput750 : undefined;
let inputLatest?: EmbeddableInputLatest = version > '7.5.0' ? state.input as EmbeddableInputLatest : undefined;
if (input710 !=== undefined) {
input730 = embeddableMigrator710to730(input710 as EmbeddableInput710);
}
if (input730 !=== undefined) {
input750 = embeddableMigrator730to750(input730);
}
if (input750 !=== undefined) {
inputLatest = embeddableMigrator750toLatest(input750);
}
// Do we have to migrate these enhancements at each step above? Or can we operate only on the latest version?
inputLatest.enhancements = inputLatest.enhancements.forEach(key =>
// What do we pass in for "key" here? How do we ensure uniqueness and avoid subtle bugs if an enhancer
// fails to register a PersistableStateDefinition for `key`, and it accidentally matches some other
// PersistableStateDefinition?
persistableStatePlugin.get(key).migrate(
inputLatest.enhancements[key],
version)
);
// When and how do we migrate the specific state (like VisualizeEmbeddableInput) instead of the generic state?
// How do we protect against clashes (e.g. what if `defaultTitle` already existed in `VisualizeEmbeddableInput`?).
// What happens if `VisualizeEmbeddableInput` tries to access something off an older version of the base
// `EmbeddableInput`?
// Also, how do we help registrators know what `state` type to use? Should we pass type in or just input? If
// just input, what if an Embeddable wants to migrate from one type to another?
return persistableStatePlugin.get(state.type).migrate({ type, input: inputLatest }, version),
}