[Saved Objects] Update the migrationVersion property to hold a plain string value (#150075)

* Update document migrator to rely on `typeMigrationVersion` instead of `migrationVersion`.
* Refactor document migrator to extract migration pipeline logic.
* Add `core` migration type.
This commit is contained in:
Michael Dokolin 2023-03-24 13:45:30 +01:00 committed by GitHub
parent 1a0cab832d
commit 17876df41a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
69 changed files with 1205 additions and 828 deletions

View file

@ -252,7 +252,7 @@ Having said that, if a document is encountered that is not in the expected shape
fail an upgrade than to silently ignore a corrupt document which can cause unexpected behaviour at some future point in time. When such a scenario is encountered, fail an upgrade than to silently ignore a corrupt document which can cause unexpected behaviour at some future point in time. When such a scenario is encountered,
the error should be verbose and informative so that the corrupt document can be corrected, if possible. the error should be verbose and informative so that the corrupt document can be corrected, if possible.
**WARNING:** Do not attempt to change the `migrationVersion`, `id`, or `type` fields within a migration function, this is not supported. **WARNING:** Do not attempt to change the `typeMigrationVersion`, `id`, or `type` fields within a migration function, this is not supported.
### Testing Migrations ### Testing Migrations

View file

@ -24,10 +24,15 @@ export interface SavedObjectsCreateOptions {
id?: string; id?: string;
/** If a document with the given `id` already exists, overwrite it's contents (default=false). */ /** If a document with the given `id` already exists, overwrite it's contents (default=false). */
overwrite?: boolean; overwrite?: boolean;
/** {@inheritDoc SavedObjectsMigrationVersion} */ /**
* {@inheritDoc SavedObjectsMigrationVersion}
* @deprecated
*/
migrationVersion?: SavedObjectsMigrationVersion; migrationVersion?: SavedObjectsMigrationVersion;
/** A semver value that is used when upgrading objects between Kibana versions. */ /** A semver value that is used when upgrading objects between Kibana versions. */
coreMigrationVersion?: string; coreMigrationVersion?: string;
/** A semver value that is used when migrating documents between Kibana versions. */
typeMigrationVersion?: string;
/** Array of referenced saved objects. */ /** Array of referenced saved objects. */
references?: SavedObjectReference[]; references?: SavedObjectReference[];
} }

View file

@ -181,7 +181,6 @@ export interface SavedObjectsClientContract {
* @param {object} attributes - the attributes to update * @param {object} attributes - the attributes to update
* @param {object} options {@link SavedObjectsUpdateOptions} * @param {object} options {@link SavedObjectsUpdateOptions}
* @prop {integer} options.version - ensures version matches that of persisted object * @prop {integer} options.version - ensures version matches that of persisted object
* @prop {object} options.migrationVersion - The optional migrationVersion of this document
* @returns the udpated simple saved object * @returns the udpated simple saved object
* @deprecated See https://github.com/elastic/kibana/issues/149098 * @deprecated See https://github.com/elastic/kibana/issues/149098
*/ */

View file

@ -27,10 +27,15 @@ export interface SimpleSavedObject<T = unknown> {
id: SavedObjectType<T>['id']; id: SavedObjectType<T>['id'];
/** Type of the saved object */ /** Type of the saved object */
type: SavedObjectType<T>['type']; type: SavedObjectType<T>['type'];
/** Migration version of the saved object */ /**
* Migration version of the saved object
* @deprecated
*/
migrationVersion: SavedObjectType<T>['migrationVersion']; migrationVersion: SavedObjectType<T>['migrationVersion'];
/** Core migration version of the saved object */ /** Core migration version of the saved object */
coreMigrationVersion: SavedObjectType<T>['coreMigrationVersion']; coreMigrationVersion: SavedObjectType<T>['coreMigrationVersion'];
/** Core migration version of the saved object */
typeMigrationVersion: SavedObjectType<T>['typeMigrationVersion'];
/** Error associated with this object, undefined if no error */ /** Error associated with this object, undefined if no error */
error: SavedObjectType<T>['error']; error: SavedObjectType<T>['error'];
/** References to other saved objects */ /** References to other saved objects */

View file

@ -19,6 +19,7 @@ describe('getRootFields', () => {
"references", "references",
"migrationVersion", "migrationVersion",
"coreMigrationVersion", "coreMigrationVersion",
"typeMigrationVersion",
"updated_at", "updated_at",
"created_at", "created_at",
"originId", "originId",

View file

@ -17,6 +17,7 @@ const ROOT_FIELDS = [
'references', 'references',
'migrationVersion', 'migrationVersion',
'coreMigrationVersion', 'coreMigrationVersion',
'typeMigrationVersion',
'updated_at', 'updated_at',
'created_at', 'created_at',
'originId', 'originId',

View file

@ -94,6 +94,7 @@ describe('#getSavedObjectFromSource', () => {
const references = [{ type: 'ref-type', id: 'ref-id', name: 'ref-name' }]; const references = [{ type: 'ref-type', id: 'ref-id', name: 'ref-name' }];
const migrationVersion = { foo: 'migrationVersion' }; const migrationVersion = { foo: 'migrationVersion' };
const coreMigrationVersion = 'coreMigrationVersion'; const coreMigrationVersion = 'coreMigrationVersion';
const typeMigrationVersion = 'typeMigrationVersion';
const originId = 'originId'; const originId = 'originId';
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
const updated_at = 'updatedAt'; const updated_at = 'updatedAt';
@ -112,6 +113,7 @@ describe('#getSavedObjectFromSource', () => {
references, references,
migrationVersion, migrationVersion,
coreMigrationVersion, coreMigrationVersion,
typeMigrationVersion,
originId, originId,
updated_at, updated_at,
...namespaceAttrs, ...namespaceAttrs,
@ -127,6 +129,7 @@ describe('#getSavedObjectFromSource', () => {
expect(result).toEqual({ expect(result).toEqual({
attributes, attributes,
coreMigrationVersion, coreMigrationVersion,
typeMigrationVersion,
id, id,
migrationVersion, migrationVersion,
namespaces: expect.anything(), // see specific test cases below namespaces: expect.anything(), // see specific test cases below

View file

@ -149,6 +149,7 @@ export function getSavedObjectFromSource<T>(
references: doc._source.references || [], references: doc._source.references || [],
migrationVersion: doc._source.migrationVersion, migrationVersion: doc._source.migrationVersion,
coreMigrationVersion: doc._source.coreMigrationVersion, coreMigrationVersion: doc._source.coreMigrationVersion,
typeMigrationVersion: doc._source.typeMigrationVersion,
}; };
} }

View file

@ -934,6 +934,8 @@ describe('SavedObjectsRepository', () => {
_source: { _source: {
...response.items[0].create._source, ...response.items[0].create._source,
namespaces: response.items[0].create._source.namespaces, namespaces: response.items[0].create._source.namespaces,
coreMigrationVersion: expect.any(String),
typeMigrationVersion: '1.1.1',
}, },
_id: expect.stringMatching(/^myspace:config:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/), _id: expect.stringMatching(/^myspace:config:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/),
}); });
@ -942,6 +944,8 @@ describe('SavedObjectsRepository', () => {
_source: { _source: {
...response.items[1].create._source, ...response.items[1].create._source,
namespaces: response.items[1].create._source.namespaces, namespaces: response.items[1].create._source.namespaces,
coreMigrationVersion: expect.any(String),
typeMigrationVersion: '1.1.1',
}, },
}); });
@ -2946,7 +2950,8 @@ describe('SavedObjectsRepository', () => {
attributes, attributes,
references, references,
namespaces: [namespace ?? 'default'], namespaces: [namespace ?? 'default'],
migrationVersion: { [MULTI_NAMESPACE_TYPE]: '1.1.1' }, coreMigrationVersion: expect.any(String),
typeMigrationVersion: '1.1.1',
}); });
}); });
}); });
@ -3533,6 +3538,7 @@ describe('SavedObjectsRepository', () => {
'references', 'references',
'migrationVersion', 'migrationVersion',
'coreMigrationVersion', 'coreMigrationVersion',
'typeMigrationVersion',
'updated_at', 'updated_at',
'created_at', 'created_at',
'originId', 'originId',

View file

@ -303,6 +303,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
const { const {
migrationVersion, migrationVersion,
coreMigrationVersion, coreMigrationVersion,
typeMigrationVersion,
overwrite = false, overwrite = false,
references = [], references = [],
refresh = DEFAULT_REFRESH_SETTING, refresh = DEFAULT_REFRESH_SETTING,
@ -381,6 +382,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
), ),
migrationVersion, migrationVersion,
coreMigrationVersion, coreMigrationVersion,
typeMigrationVersion,
created_at: time, created_at: time,
updated_at: time, updated_at: time,
...(Array.isArray(references) && { references }), ...(Array.isArray(references) && { references }),
@ -591,6 +593,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
), ),
migrationVersion: object.migrationVersion, migrationVersion: object.migrationVersion,
coreMigrationVersion: object.coreMigrationVersion, coreMigrationVersion: object.coreMigrationVersion,
typeMigrationVersion: object.typeMigrationVersion,
...(savedObjectNamespace && { namespace: savedObjectNamespace }), ...(savedObjectNamespace && { namespace: savedObjectNamespace }),
...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }),
updated_at: time, updated_at: time,
@ -2311,6 +2314,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
): Promise<SavedObject<T>> { ): Promise<SavedObject<T>> {
const { const {
migrationVersion, migrationVersion,
typeMigrationVersion,
refresh = DEFAULT_REFRESH_SETTING, refresh = DEFAULT_REFRESH_SETTING,
initialize = false, initialize = false,
upsertAttributes, upsertAttributes,
@ -2384,6 +2388,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
}, {} as Record<string, number>), }, {} as Record<string, number>),
}, },
migrationVersion, migrationVersion,
typeMigrationVersion,
updated_at: time, updated_at: time,
}); });

View file

@ -589,23 +589,25 @@ export const getMockBulkCreateResponse = (
return { return {
errors: false, errors: false,
took: 1, took: 1,
items: objects.map(({ type, id, originId, attributes, references, migrationVersion }) => ({ items: objects.map(
create: { ({ type, id, originId, attributes, references, migrationVersion, typeMigrationVersion }) => ({
// status: 1, create: {
// _index: '.kibana', // status: 1,
_id: `${namespace ? `${namespace}:` : ''}${type}:${id}`, // _index: '.kibana',
_source: { _id: `${namespace ? `${namespace}:` : ''}${type}:${id}`,
[type]: attributes, _source: {
type, [type]: attributes,
namespace, type,
...(originId && { originId }), namespace,
references, ...(originId && { originId }),
...mockTimestampFieldsWithCreated, references,
migrationVersion: migrationVersion || { [type]: '1.1.1' }, ...mockTimestampFieldsWithCreated,
typeMigrationVersion: typeMigrationVersion || migrationVersion?.[type] || '1.1.1',
},
...mockVersionProps,
}, },
...mockVersionProps, })
}, ),
})),
} as unknown as estypes.BulkResponse; } as unknown as estypes.BulkResponse;
}; };
@ -627,7 +629,8 @@ export const expectCreateResult = (obj: {
namespaces?: string[]; namespaces?: string[];
}) => ({ }) => ({
...obj, ...obj,
migrationVersion: { [obj.type]: '1.1.1' }, coreMigrationVersion: expect.any(String),
typeMigrationVersion: '1.1.1',
version: mockVersion, version: mockVersion,
namespaces: obj.namespaces ?? [obj.namespace ?? 'default'], namespaces: obj.namespaces ?? [obj.namespace ?? 'default'],
...mockTimestampFieldsWithCreated, ...mockTimestampFieldsWithCreated,

View file

@ -25,7 +25,10 @@ export interface SavedObjectsBulkCreateObject<T = unknown> {
version?: string; version?: string;
/** Array of references to other saved objects */ /** Array of references to other saved objects */
references?: SavedObjectReference[]; references?: SavedObjectReference[];
/** {@inheritDoc SavedObjectsMigrationVersion} */ /**
* {@inheritDoc SavedObjectsMigrationVersion}
* @deprecated
*/
migrationVersion?: SavedObjectsMigrationVersion; migrationVersion?: SavedObjectsMigrationVersion;
/** /**
* A semver value that is used when upgrading objects between Kibana versions. If undefined, this will be automatically set to the current * A semver value that is used when upgrading objects between Kibana versions. If undefined, this will be automatically set to the current
@ -37,6 +40,8 @@ export interface SavedObjectsBulkCreateObject<T = unknown> {
* field set and you want to create it again. * field set and you want to create it again.
*/ */
coreMigrationVersion?: string; coreMigrationVersion?: string;
/** A semver value that is used when migrating documents between Kibana versions. */
typeMigrationVersion?: string;
/** Optional ID of the original saved object, if this object's `id` was regenerated */ /** Optional ID of the original saved object, if this object's `id` was regenerated */
originId?: string; originId?: string;
/** /**

View file

@ -25,7 +25,10 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions {
* Can be used in conjunction with `overwrite` for implementing optimistic concurrency control. * Can be used in conjunction with `overwrite` for implementing optimistic concurrency control.
**/ **/
version?: string; version?: string;
/** {@inheritDoc SavedObjectsMigrationVersion} */ /**
* {@inheritDoc SavedObjectsMigrationVersion}
* @deprecated Use {@link SavedObjectsCreateOptions.typeMigrationVersion} instead.
*/
migrationVersion?: SavedObjectsMigrationVersion; migrationVersion?: SavedObjectsMigrationVersion;
/** /**
* A semver value that is used when upgrading objects between Kibana versions. If undefined, this will be automatically set to the current * A semver value that is used when upgrading objects between Kibana versions. If undefined, this will be automatically set to the current
@ -37,6 +40,10 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions {
* field set and you want to create it again. * field set and you want to create it again.
*/ */
coreMigrationVersion?: string; coreMigrationVersion?: string;
/**
* A semver value that is used when migrating documents between Kibana versions.
*/
typeMigrationVersion?: string;
/** Array of references to other saved objects */ /** Array of references to other saved objects */
references?: SavedObjectReference[]; references?: SavedObjectReference[];
/** The Elasticsearch Refresh setting for this operation */ /** The Elasticsearch Refresh setting for this operation */

View file

@ -21,8 +21,15 @@ export interface SavedObjectsIncrementCounterOptions<Attributes = unknown>
* already exist. Existing fields will be left as-is and won't be incremented. * already exist. Existing fields will be left as-is and won't be incremented.
*/ */
initialize?: boolean; initialize?: boolean;
/** {@link SavedObjectsMigrationVersion} */ /**
* {@link SavedObjectsMigrationVersion}
* @deprecated
*/
migrationVersion?: SavedObjectsMigrationVersion; migrationVersion?: SavedObjectsMigrationVersion;
/**
* A semver value that is used when migrating documents between Kibana versions.
*/
typeMigrationVersion?: string;
/** /**
* (default='wait_for') The Elasticsearch refresh setting for this * (default='wait_for') The Elasticsearch refresh setting for this
* operation. See {@link MutatingOperationRefreshSetting} * operation. See {@link MutatingOperationRefreshSetting}

View file

@ -76,7 +76,6 @@ export interface ISavedObjectsRepository {
* @param {object} [options={}] {@link SavedObjectsCreateOptions} - options for the create operation * @param {object} [options={}] {@link SavedObjectsCreateOptions} - options for the create operation
* @property {string} [options.id] - force id on creation, not recommended * @property {string} [options.id] - force id on creation, not recommended
* @property {boolean} [options.overwrite=false] * @property {boolean} [options.overwrite=false]
* @property {object} [options.migrationVersion=undefined]
* @property {string} [options.namespace] * @property {string} [options.namespace]
* @property {array} [options.references=[]] - [{ name, type, id }] * @property {array} [options.references=[]] - [{ name, type, id }]
* @returns {promise} the created saved object { id, type, version, attributes } * @returns {promise} the created saved object { id, type, version, attributes }

View file

@ -87,8 +87,15 @@ export class SavedObjectsSerializer implements ISavedObjectsSerializer {
const { namespaceTreatment = 'strict' } = options; const { namespaceTreatment = 'strict' } = options;
const { _id, _source, _seq_no, _primary_term } = doc; const { _id, _source, _seq_no, _primary_term } = doc;
const { type, namespaces, originId, migrationVersion, references, coreMigrationVersion } = const {
_source; type,
namespaces,
originId,
migrationVersion,
references,
coreMigrationVersion,
typeMigrationVersion,
} = _source;
const version = const version =
_seq_no != null || _primary_term != null _seq_no != null || _primary_term != null
@ -109,6 +116,7 @@ export class SavedObjectsSerializer implements ISavedObjectsSerializer {
references: references || [], references: references || [],
...(migrationVersion && { migrationVersion }), ...(migrationVersion && { migrationVersion }),
...(coreMigrationVersion && { coreMigrationVersion }), ...(coreMigrationVersion && { coreMigrationVersion }),
...(typeMigrationVersion != null ? { typeMigrationVersion } : {}),
...(_source.updated_at && { updated_at: _source.updated_at }), ...(_source.updated_at && { updated_at: _source.updated_at }),
...(_source.created_at && { created_at: _source.created_at }), ...(_source.created_at && { created_at: _source.created_at }),
...(version && { version }), ...(version && { version }),
@ -135,6 +143,7 @@ export class SavedObjectsSerializer implements ISavedObjectsSerializer {
version, version,
references, references,
coreMigrationVersion, coreMigrationVersion,
typeMigrationVersion,
} = savedObj; } = savedObj;
const source = { const source = {
[type]: attributes, [type]: attributes,
@ -145,6 +154,7 @@ export class SavedObjectsSerializer implements ISavedObjectsSerializer {
...(originId && { originId }), ...(originId && { originId }),
...(migrationVersion && { migrationVersion }), ...(migrationVersion && { migrationVersion }),
...(coreMigrationVersion && { coreMigrationVersion }), ...(coreMigrationVersion && { coreMigrationVersion }),
...(typeMigrationVersion != null ? { typeMigrationVersion } : {}),
...(updated_at && { updated_at }), ...(updated_at && { updated_at }),
...(createdAt && { created_at: createdAt }), ...(createdAt && { created_at: createdAt }),
}; };

View file

@ -42,6 +42,7 @@ export const createSavedObjectSanitizedDocSchema = (attributesSchema: SavedObjec
namespaces: schema.maybe(schema.arrayOf(schema.string())), namespaces: schema.maybe(schema.arrayOf(schema.string())),
migrationVersion: schema.maybe(schema.recordOf(schema.string(), schema.string())), migrationVersion: schema.maybe(schema.recordOf(schema.string(), schema.string())),
coreMigrationVersion: schema.maybe(schema.string()), coreMigrationVersion: schema.maybe(schema.string()),
typeMigrationVersion: schema.maybe(schema.string()),
updated_at: schema.maybe(schema.string()), updated_at: schema.maybe(schema.string()),
created_at: schema.maybe(schema.string()), created_at: schema.maybe(schema.string()),
version: schema.maybe(schema.string()), version: schema.maybe(schema.string()),

View file

@ -206,6 +206,7 @@ export class SavedObjectsClient implements SavedObjectsClientContract {
body: JSON.stringify({ body: JSON.stringify({
attributes, attributes,
migrationVersion: options.migrationVersion, migrationVersion: options.migrationVersion,
typeMigrationVersion: options.typeMigrationVersion,
references: options.references, references: options.references,
}), }),
}); });
@ -216,7 +217,7 @@ export class SavedObjectsClient implements SavedObjectsClientContract {
/** /**
* Creates multiple documents at once * Creates multiple documents at once
* *
* @param {array} objects - [{ type, id, attributes, references, migrationVersion }] * @param {array} objects - [{ type, id, attributes, references, migrationVersion, typeMigrationVersion }]
* @param {object} [options={}] * @param {object} [options={}]
* @property {boolean} [options.overwrite=false] * @property {boolean} [options.overwrite=false]
* @returns The result of the create operation containing created saved objects. * @returns The result of the create operation containing created saved objects.

View file

@ -25,8 +25,10 @@ export class SimpleSavedObjectImpl<T = unknown> implements SimpleSavedObject<T>
public _version?: SavedObjectType<T>['version']; public _version?: SavedObjectType<T>['version'];
public id: SavedObjectType<T>['id']; public id: SavedObjectType<T>['id'];
public type: SavedObjectType<T>['type']; public type: SavedObjectType<T>['type'];
/** @deprecated */
public migrationVersion: SavedObjectType<T>['migrationVersion']; public migrationVersion: SavedObjectType<T>['migrationVersion'];
public coreMigrationVersion: SavedObjectType<T>['coreMigrationVersion']; public coreMigrationVersion: SavedObjectType<T>['coreMigrationVersion'];
public typeMigrationVersion: SavedObjectType<T>['typeMigrationVersion'];
public error: SavedObjectType<T>['error']; public error: SavedObjectType<T>['error'];
public references: SavedObjectType<T>['references']; public references: SavedObjectType<T>['references'];
public updatedAt: SavedObjectType<T>['updated_at']; public updatedAt: SavedObjectType<T>['updated_at'];
@ -44,6 +46,7 @@ export class SimpleSavedObjectImpl<T = unknown> implements SimpleSavedObject<T>
references, references,
migrationVersion, migrationVersion,
coreMigrationVersion, coreMigrationVersion,
typeMigrationVersion,
namespaces, namespaces,
updated_at: updatedAt, updated_at: updatedAt,
created_at: createdAt, created_at: createdAt,
@ -56,6 +59,7 @@ export class SimpleSavedObjectImpl<T = unknown> implements SimpleSavedObject<T>
this._version = version; this._version = version;
this.migrationVersion = migrationVersion; this.migrationVersion = migrationVersion;
this.coreMigrationVersion = coreMigrationVersion; this.coreMigrationVersion = coreMigrationVersion;
this.typeMigrationVersion = typeMigrationVersion;
this.namespaces = namespaces; this.namespaces = namespaces;
this.updatedAt = updatedAt; this.updatedAt = updatedAt;
this.createdAt = createdAt; this.createdAt = createdAt;
@ -91,6 +95,7 @@ export class SimpleSavedObjectImpl<T = unknown> implements SimpleSavedObject<T>
.create(this.type, this.attributes, { .create(this.type, this.attributes, {
migrationVersion: this.migrationVersion, migrationVersion: this.migrationVersion,
coreMigrationVersion: this.coreMigrationVersion, coreMigrationVersion: this.coreMigrationVersion,
typeMigrationVersion: this.typeMigrationVersion,
references: this.references, references: this.references,
}) })
.then((sso) => { .then((sso) => {

View file

@ -39,6 +39,7 @@ const createSimpleSavedObjectMock = (
type: savedObject.type, type: savedObject.type,
migrationVersion: savedObject.migrationVersion, migrationVersion: savedObject.migrationVersion,
coreMigrationVersion: savedObject.coreMigrationVersion, coreMigrationVersion: savedObject.coreMigrationVersion,
typeMigrationVersion: savedObject.typeMigrationVersion,
error: savedObject.error, error: savedObject.error,
references: savedObject.references, references: savedObject.references,
updatedAt: savedObject.updated_at, updatedAt: savedObject.updated_at,

View file

@ -80,10 +80,15 @@ export interface SavedObject<T = unknown> {
attributes: T; attributes: T;
/** {@inheritdoc SavedObjectReference} */ /** {@inheritdoc SavedObjectReference} */
references: SavedObjectReference[]; references: SavedObjectReference[];
/** {@inheritdoc SavedObjectsMigrationVersion} */ /**
* {@inheritdoc SavedObjectsMigrationVersion}
* @deprecated Use `typeMigrationVersion` instead.
*/
migrationVersion?: SavedObjectsMigrationVersion; migrationVersion?: SavedObjectsMigrationVersion;
/** A semver value that is used when upgrading objects between Kibana versions. */ /** A semver value that is used when upgrading objects between Kibana versions. */
coreMigrationVersion?: string; coreMigrationVersion?: string;
/** A semver value that is used when migrating documents between Kibana versions. */
typeMigrationVersion?: string;
/** /**
* Space(s) that this saved object exists in. This attribute is not used for "global" saved object types which are registered with * Space(s) that this saved object exists in. This attribute is not used for "global" saved object types which are registered with
* `namespaceType: 'agnostic'`. * `namespaceType: 'agnostic'`.

View file

@ -139,8 +139,8 @@ describe('collectSavedObjects()', () => {
const result = await collectSavedObjects({ readStream, supportedTypes, objectLimit }); const result = await collectSavedObjects({ readStream, supportedTypes, objectLimit });
const collectedObjects = [ const collectedObjects = [
{ ...obj1, migrationVersion: {} }, { ...obj1, typeMigrationVersion: '' },
{ ...obj2, migrationVersion: {} }, { ...obj2, typeMigrationVersion: '' },
]; ];
const importStateMap = new Map([ const importStateMap = new Map([
[`a:1`, {}], // a:1 is included because it is present in the collected objects [`a:1`, {}], // a:1 is included because it is present in the collected objects
@ -166,7 +166,7 @@ describe('collectSavedObjects()', () => {
const supportedTypes = [obj1.type]; const supportedTypes = [obj1.type];
const result = await collectSavedObjects({ readStream, supportedTypes, objectLimit }); const result = await collectSavedObjects({ readStream, supportedTypes, objectLimit });
const collectedObjects = [{ ...obj1, migrationVersion: {} }]; const collectedObjects = [{ ...obj1, typeMigrationVersion: '' }];
const importStateMap = new Map([ const importStateMap = new Map([
[`a:1`, {}], // a:1 is included because it is present in the collected objects [`a:1`, {}], // a:1 is included because it is present in the collected objects
[`b:2`, { isOnlyReference: true }], // b:2 was filtered out due to an unsupported type; b:2 is included because a:1 has a reference to b:2, but this is marked as `isOnlyReference` because b:2 is not present in the collected objects [`b:2`, { isOnlyReference: true }], // b:2 was filtered out due to an unsupported type; b:2 is included because a:1 has a reference to b:2, but this is marked as `isOnlyReference` because b:2 is not present in the collected objects
@ -178,6 +178,19 @@ describe('collectSavedObjects()', () => {
expect(result).toEqual({ collectedObjects, errors, importStateMap }); expect(result).toEqual({ collectedObjects, errors, importStateMap });
}); });
test('keeps the original migration versions', async () => {
const collectedObjects = [
{ ...obj1, migrationVersion: { a: '1.0.0' } },
{ ...obj2, typeMigrationVersion: '2.0.0' },
];
const readStream = createReadStream(...collectedObjects);
const supportedTypes = [obj1.type, obj2.type];
const result = await collectSavedObjects({ readStream, supportedTypes, objectLimit });
expect(result).toEqual(expect.objectContaining({ collectedObjects }));
});
describe('with optional filter', () => { describe('with optional filter', () => {
test('filters out objects when result === false', async () => { test('filters out objects when result === false', async () => {
const readStream = createReadStream(obj1, obj2); const readStream = createReadStream(obj1, obj2);
@ -207,7 +220,7 @@ describe('collectSavedObjects()', () => {
filter, filter,
}); });
const collectedObjects = [{ ...obj2, migrationVersion: {} }]; const collectedObjects = [{ ...obj2, typeMigrationVersion: '' }];
const importStateMap = new Map([ const importStateMap = new Map([
// a:1 was filtered out due to an unsupported type; a:1 is not included because there are no other references to a:1 // a:1 was filtered out due to an unsupported type; a:1 is not included because there are no other references to a:1
[`b:2`, {}], // b:2 is included because it is present in the collected objects [`b:2`, {}], // b:2 is included because it is present in the collected objects

View file

@ -65,7 +65,10 @@ export async function collectSavedObjects({
} }
} }
// Ensure migrations execute on every saved object // Ensure migrations execute on every saved object
return Object.assign({ migrationVersion: {} }, obj); return {
...obj,
...(!obj.migrationVersion && !obj.typeMigrationVersion ? { typeMigrationVersion: '' } : {}),
};
}), }),
createConcatStream([]), createConcatStream([]),
]); ]);

View file

@ -14,6 +14,7 @@ Object {
"originId": "2f4316de49999235636386fe51dc06c1", "originId": "2f4316de49999235636386fe51dc06c1",
"references": "7997cf5a56cc02bdc9c93361bde732b0", "references": "7997cf5a56cc02bdc9c93361bde732b0",
"type": "2f4316de49999235636386fe51dc06c1", "type": "2f4316de49999235636386fe51dc06c1",
"typeMigrationVersion": "539e3ecebb3abc1133618094cc3b7ae7",
"updated_at": "00da57df13e94e9d98437d13ace4bfe0", "updated_at": "00da57df13e94e9d98437d13ace4bfe0",
}, },
}, },
@ -69,6 +70,9 @@ Object {
"type": Object { "type": Object {
"type": "keyword", "type": "keyword",
}, },
"typeMigrationVersion": Object {
"type": "version",
},
"updated_at": Object { "updated_at": Object {
"type": "date", "type": "date",
}, },

View file

@ -14,6 +14,7 @@ Object {
"originId": "2f4316de49999235636386fe51dc06c1", "originId": "2f4316de49999235636386fe51dc06c1",
"references": "7997cf5a56cc02bdc9c93361bde732b0", "references": "7997cf5a56cc02bdc9c93361bde732b0",
"type": "2f4316de49999235636386fe51dc06c1", "type": "2f4316de49999235636386fe51dc06c1",
"typeMigrationVersion": "539e3ecebb3abc1133618094cc3b7ae7",
"updated_at": "00da57df13e94e9d98437d13ace4bfe0", "updated_at": "00da57df13e94e9d98437d13ace4bfe0",
}, },
}, },
@ -61,6 +62,9 @@ Object {
"type": Object { "type": Object {
"type": "keyword", "type": "keyword",
}, },
"typeMigrationVersion": Object {
"type": "version",
},
"updated_at": Object { "updated_at": Object {
"type": "date", "type": "date",
}, },
@ -83,6 +87,7 @@ Object {
"secondType": "72d57924f415fbadb3ee293b67d233ab", "secondType": "72d57924f415fbadb3ee293b67d233ab",
"thirdType": "510f1f0adb69830cf8a1c5ce2923ed82", "thirdType": "510f1f0adb69830cf8a1c5ce2923ed82",
"type": "2f4316de49999235636386fe51dc06c1", "type": "2f4316de49999235636386fe51dc06c1",
"typeMigrationVersion": "539e3ecebb3abc1133618094cc3b7ae7",
"updated_at": "00da57df13e94e9d98437d13ace4bfe0", "updated_at": "00da57df13e94e9d98437d13ace4bfe0",
}, },
}, },
@ -147,6 +152,9 @@ Object {
"type": Object { "type": Object {
"type": "keyword", "type": "keyword",
}, },
"typeMigrationVersion": Object {
"type": "version",
},
"updated_at": Object { "updated_at": Object {
"type": "date", "type": "date",
}, },

View file

@ -159,6 +159,9 @@ export function getBaseMappings(): IndexMapping {
coreMigrationVersion: { coreMigrationVersion: {
type: 'keyword', type: 'keyword',
}, },
typeMigrationVersion: {
type: 'version',
},
}, },
}; };
} }

View file

@ -31,18 +31,36 @@ describe('migrateRawDocs', () => {
transform, transform,
[ [
{ _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }, { _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } },
{ _id: 'a:c', _source: { type: 'a', a: { name: 'AAA' }, typeMigrationVersion: '1.0.0' } },
{ _id: 'c:d', _source: { type: 'c', c: { name: 'DDD' } } }, { _id: 'c:d', _source: { type: 'c', c: { name: 'DDD' } } },
{
_id: 'c:e',
_source: { type: 'c', c: { name: 'DDD' }, migrationVersion: { c: '2.0.0' } },
},
] ]
); );
expect(result).toEqual([ expect(result).toEqual([
{ {
_id: 'a:b', _id: 'a:b',
_source: { type: 'a', a: { name: 'HOI!' }, migrationVersion: {}, references: [] }, _source: { type: 'a', a: { name: 'HOI!' }, typeMigrationVersion: '', references: [] },
},
{
_id: 'a:c',
_source: { type: 'a', a: { name: 'HOI!' }, typeMigrationVersion: '1.0.0', references: [] },
}, },
{ {
_id: 'c:d', _id: 'c:d',
_source: { type: 'c', c: { name: 'HOI!' }, migrationVersion: {}, references: [] }, _source: { type: 'c', c: { name: 'HOI!' }, typeMigrationVersion: '', references: [] },
},
{
_id: 'c:e',
_source: {
type: 'c',
c: { name: 'HOI!' },
migrationVersion: { c: '2.0.0' },
references: [],
},
}, },
]); ]);
@ -50,19 +68,35 @@ describe('migrateRawDocs', () => {
id: 'b', id: 'b',
type: 'a', type: 'a',
attributes: { name: 'AAA' }, attributes: { name: 'AAA' },
migrationVersion: {}, typeMigrationVersion: '',
references: [], references: [],
}; };
const obj2 = { const obj2 = {
id: 'c',
type: 'a',
attributes: { name: 'AAA' },
typeMigrationVersion: '1.0.0',
references: [],
};
const obj3 = {
id: 'd', id: 'd',
type: 'c', type: 'c',
attributes: { name: 'DDD' }, attributes: { name: 'DDD' },
migrationVersion: {}, typeMigrationVersion: '',
references: [], references: [],
}; };
expect(transform).toHaveBeenCalledTimes(2); const obj4 = {
id: 'e',
type: 'c',
attributes: { name: 'DDD' },
migrationVersion: { c: '2.0.0' },
references: [],
};
expect(transform).toHaveBeenCalledTimes(4);
expect(transform).toHaveBeenNthCalledWith(1, obj1); expect(transform).toHaveBeenNthCalledWith(1, obj1);
expect(transform).toHaveBeenNthCalledWith(2, obj2); expect(transform).toHaveBeenNthCalledWith(2, obj2);
expect(transform).toHaveBeenNthCalledWith(3, obj3);
expect(transform).toHaveBeenNthCalledWith(4, obj4);
}); });
test('throws when encountering a corrupt saved object document', async () => { test('throws when encountering a corrupt saved object document', async () => {
@ -99,7 +133,7 @@ describe('migrateRawDocs', () => {
expect(result).toEqual([ expect(result).toEqual([
{ {
_id: 'a:b', _id: 'a:b',
_source: { type: 'a', a: { name: 'HOI!' }, migrationVersion: {}, references: [] }, _source: { type: 'a', a: { name: 'HOI!' }, typeMigrationVersion: '', references: [] },
}, },
{ {
_id: 'foo:bar', _id: 'foo:bar',
@ -111,7 +145,7 @@ describe('migrateRawDocs', () => {
id: 'b', id: 'b',
type: 'a', type: 'a',
attributes: { name: 'AAA' }, attributes: { name: 'AAA' },
migrationVersion: {}, typeMigrationVersion: '',
references: [], references: [],
}; };
expect(transform).toHaveBeenCalledTimes(1); expect(transform).toHaveBeenCalledTimes(1);
@ -144,7 +178,12 @@ describe('migrateRawDocsSafely', () => {
migrateDoc: transform, migrateDoc: transform,
rawDocs: [ rawDocs: [
{ _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }, { _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } },
{ _id: 'a:c', _source: { type: 'a', a: { name: 'AAA' }, typeMigrationVersion: '1.0.0' } },
{ _id: 'c:d', _source: { type: 'c', c: { name: 'DDD' } } }, { _id: 'c:d', _source: { type: 'c', c: { name: 'DDD' } } },
{
_id: 'c:e',
_source: { type: 'c', c: { name: 'DDD' }, migrationVersion: { c: '2.0.0' } },
},
], ],
}); });
const result = (await task()) as Either.Right<DocumentsTransformSuccess>; const result = (await task()) as Either.Right<DocumentsTransformSuccess>;
@ -152,11 +191,24 @@ describe('migrateRawDocsSafely', () => {
expect(result.right.processedDocs).toEqual([ expect(result.right.processedDocs).toEqual([
{ {
_id: 'a:b', _id: 'a:b',
_source: { type: 'a', a: { name: 'HOI!' }, migrationVersion: {}, references: [] }, _source: { type: 'a', a: { name: 'HOI!' }, typeMigrationVersion: '', references: [] },
},
{
_id: 'a:c',
_source: { type: 'a', a: { name: 'HOI!' }, typeMigrationVersion: '1.0.0', references: [] },
}, },
{ {
_id: 'c:d', _id: 'c:d',
_source: { type: 'c', c: { name: 'HOI!' }, migrationVersion: {}, references: [] }, _source: { type: 'c', c: { name: 'HOI!' }, typeMigrationVersion: '', references: [] },
},
{
_id: 'c:e',
_source: {
type: 'c',
c: { name: 'HOI!' },
migrationVersion: { c: '2.0.0' },
references: [],
},
}, },
]); ]);
@ -164,19 +216,35 @@ describe('migrateRawDocsSafely', () => {
id: 'b', id: 'b',
type: 'a', type: 'a',
attributes: { name: 'AAA' }, attributes: { name: 'AAA' },
migrationVersion: {}, typeMigrationVersion: '',
references: [], references: [],
}; };
const obj2 = { const obj2 = {
id: 'c',
type: 'a',
attributes: { name: 'AAA' },
typeMigrationVersion: '1.0.0',
references: [],
};
const obj3 = {
id: 'd', id: 'd',
type: 'c', type: 'c',
attributes: { name: 'DDD' }, attributes: { name: 'DDD' },
migrationVersion: {}, typeMigrationVersion: '',
references: [], references: [],
}; };
expect(transform).toHaveBeenCalledTimes(2); const obj4 = {
id: 'e',
type: 'c',
attributes: { name: 'DDD' },
migrationVersion: { c: '2.0.0' },
references: [],
};
expect(transform).toHaveBeenCalledTimes(4);
expect(transform).toHaveBeenNthCalledWith(1, obj1); expect(transform).toHaveBeenNthCalledWith(1, obj1);
expect(transform).toHaveBeenNthCalledWith(2, obj2); expect(transform).toHaveBeenNthCalledWith(2, obj2);
expect(transform).toHaveBeenNthCalledWith(3, obj3);
expect(transform).toHaveBeenNthCalledWith(4, obj4);
}); });
test('returns a `left` tag when encountering a corrupt saved object document', async () => { test('returns a `left` tag when encountering a corrupt saved object document', async () => {
@ -220,7 +288,7 @@ describe('migrateRawDocsSafely', () => {
expect(result.right.processedDocs).toEqual([ expect(result.right.processedDocs).toEqual([
{ {
_id: 'a:b', _id: 'a:b',
_source: { type: 'a', a: { name: 'HOI!' }, migrationVersion: {}, references: [] }, _source: { type: 'a', a: { name: 'HOI!' }, typeMigrationVersion: '', references: [] },
}, },
{ {
_id: 'foo:bar', _id: 'foo:bar',
@ -232,7 +300,7 @@ describe('migrateRawDocsSafely', () => {
id: 'b', id: 'b',
type: 'a', type: 'a',
attributes: { name: 'AAA' }, attributes: { name: 'AAA' },
migrationVersion: {}, typeMigrationVersion: '',
references: [], references: [],
}; };
expect(transform).toHaveBeenCalledTimes(1); expect(transform).toHaveBeenCalledTimes(1);

View file

@ -204,6 +204,8 @@ function convertToRawAddMigrationVersion(
serializer: SavedObjectsSerializer serializer: SavedObjectsSerializer
): SavedObjectSanitizedDoc<unknown> { ): SavedObjectSanitizedDoc<unknown> {
const savedObject = serializer.rawToSavedObject(rawDoc, options); const savedObject = serializer.rawToSavedObject(rawDoc, options);
savedObject.migrationVersion = savedObject.migrationVersion || {}; if (!savedObject.migrationVersion && !savedObject.typeMigrationVersion) {
savedObject.typeMigrationVersion = '';
}
return savedObject; return savedObject;
} }

View file

@ -6,10 +6,12 @@
* Side Public License, v 1. * Side Public License, v 1.
*/ */
export const getCoreTransformsMock = jest.fn();
export const getReferenceTransformsMock = jest.fn(); export const getReferenceTransformsMock = jest.fn();
export const getConversionTransformsMock = jest.fn(); export const getConversionTransformsMock = jest.fn();
jest.doMock('./internal_transforms', () => ({ jest.doMock('./internal_transforms', () => ({
getCoreTransforms: getCoreTransformsMock,
getReferenceTransforms: getReferenceTransformsMock, getReferenceTransforms: getReferenceTransformsMock,
getConversionTransforms: getConversionTransformsMock, getConversionTransforms: getConversionTransformsMock,
})); }));
@ -27,6 +29,7 @@ jest.doMock('./validate_migrations', () => ({
})); }));
export const resetAllMocks = () => { export const resetAllMocks = () => {
getCoreTransformsMock.mockReset().mockReturnValue([]);
getReferenceTransformsMock.mockReset().mockReturnValue([]); getReferenceTransformsMock.mockReset().mockReturnValue([]);
getConversionTransformsMock.mockReset().mockReturnValue([]); getConversionTransformsMock.mockReset().mockReturnValue([]);
getModelVersionTransformsMock.mockReset().mockReturnValue([]); getModelVersionTransformsMock.mockReset().mockReturnValue([]);

View file

@ -7,6 +7,7 @@
*/ */
import { import {
getCoreTransformsMock,
getConversionTransformsMock, getConversionTransformsMock,
getModelVersionTransformsMock, getModelVersionTransformsMock,
getReferenceTransformsMock, getReferenceTransformsMock,
@ -221,6 +222,21 @@ describe('buildActiveMigrations', () => {
]); ]);
}); });
it('adds the transform from getCoreTransforms to each type', () => {
const foo = createType({ name: 'foo' });
const bar = createType({ name: 'bar' });
addType(foo);
addType(bar);
getCoreTransformsMock.mockReturnValue([transform(TransformType.Core, '8.8.0')]);
const migrations = buildMigrations();
expect(Object.keys(migrations).sort()).toEqual(['bar', 'foo']);
expect(migrations.foo.transforms).toEqual([expectTransform(TransformType.Core, '8.8.0')]);
expect(migrations.bar.transforms).toEqual([expectTransform(TransformType.Core, '8.8.0')]);
});
it('calls getConversionTransforms with the correct parameters', () => { it('calls getConversionTransforms with the correct parameters', () => {
const foo = createType({ name: 'foo' }); const foo = createType({ name: 'foo' });
const bar = createType({ name: 'bar' }); const bar = createType({ name: 'bar' });
@ -285,6 +301,8 @@ describe('buildActiveMigrations', () => {
} }
); );
getCoreTransformsMock.mockReturnValue([transform(TransformType.Core, '8.8.0')]);
getReferenceTransformsMock.mockReturnValue([ getReferenceTransformsMock.mockReturnValue([
transform(TransformType.Reference, '7.12.0'), transform(TransformType.Reference, '7.12.0'),
transform(TransformType.Reference, '7.17.3'), transform(TransformType.Reference, '7.17.3'),
@ -302,6 +320,7 @@ describe('buildActiveMigrations', () => {
expect(Object.keys(migrations).sort()).toEqual(['bar', 'foo']); expect(Object.keys(migrations).sort()).toEqual(['bar', 'foo']);
expect(migrations.foo.transforms).toEqual([ expect(migrations.foo.transforms).toEqual([
expectTransform(TransformType.Core, '8.8.0'),
expectTransform(TransformType.Reference, '7.12.0'), expectTransform(TransformType.Reference, '7.12.0'),
expectTransform(TransformType.Migrate, '7.12.0'), expectTransform(TransformType.Migrate, '7.12.0'),
expectTransform(TransformType.Convert, '7.14.0'), expectTransform(TransformType.Convert, '7.14.0'),
@ -310,6 +329,7 @@ describe('buildActiveMigrations', () => {
expectTransform(TransformType.Migrate, '7.18.2'), expectTransform(TransformType.Migrate, '7.18.2'),
]); ]);
expect(migrations.bar.transforms).toEqual([ expect(migrations.bar.transforms).toEqual([
expectTransform(TransformType.Core, '8.8.0'),
expectTransform(TransformType.Reference, '7.12.0'), expectTransform(TransformType.Reference, '7.12.0'),
expectTransform(TransformType.Migrate, '7.17.0'), expectTransform(TransformType.Migrate, '7.17.0'),
expectTransform(TransformType.Reference, '7.17.3'), expectTransform(TransformType.Reference, '7.17.3'),

View file

@ -10,7 +10,11 @@ import _ from 'lodash';
import type { Logger } from '@kbn/logging'; import type { Logger } from '@kbn/logging';
import type { ISavedObjectTypeRegistry, SavedObjectsType } from '@kbn/core-saved-objects-server'; import type { ISavedObjectTypeRegistry, SavedObjectsType } from '@kbn/core-saved-objects-server';
import { type ActiveMigrations, type Transform, type TypeTransforms, TransformType } from './types'; import { type ActiveMigrations, type Transform, type TypeTransforms, TransformType } from './types';
import { getReferenceTransforms, getConversionTransforms } from './internal_transforms'; import {
getCoreTransforms,
getReferenceTransforms,
getConversionTransforms,
} from './internal_transforms';
import { validateTypeMigrations } from './validate_migrations'; import { validateTypeMigrations } from './validate_migrations';
import { transformComparator, convertMigrationFunction } from './utils'; import { transformComparator, convertMigrationFunction } from './utils';
import { getModelVersionTransforms } from './model_version'; import { getModelVersionTransforms } from './model_version';
@ -32,6 +36,7 @@ export function buildActiveMigrations({
convertVersion?: string; convertVersion?: string;
log: Logger; log: Logger;
}): ActiveMigrations { }): ActiveMigrations {
const coreTransforms = getCoreTransforms();
const referenceTransforms = getReferenceTransforms(typeRegistry); const referenceTransforms = getReferenceTransforms(typeRegistry);
return typeRegistry.getAllTypes().reduce((migrations, type) => { return typeRegistry.getAllTypes().reduce((migrations, type) => {
@ -41,6 +46,7 @@ export function buildActiveMigrations({
type, type,
log, log,
kibanaVersion, kibanaVersion,
coreTransforms,
referenceTransforms, referenceTransforms,
}); });
@ -58,11 +64,13 @@ export function buildActiveMigrations({
const buildTypeTransforms = ({ const buildTypeTransforms = ({
type, type,
log, log,
coreTransforms,
referenceTransforms, referenceTransforms,
}: { }: {
type: SavedObjectsType; type: SavedObjectsType;
kibanaVersion: string; kibanaVersion: string;
log: Logger; log: Logger;
coreTransforms: Transform[];
referenceTransforms: Transform[]; referenceTransforms: Transform[];
}): TypeTransforms => { }): TypeTransforms => {
const migrationsMap = const migrationsMap =
@ -80,6 +88,7 @@ const buildTypeTransforms = ({
const conversionTransforms = getConversionTransforms(type); const conversionTransforms = getConversionTransforms(type);
const transforms = [ const transforms = [
...coreTransforms,
...referenceTransforms, ...referenceTransforms,
...conversionTransforms, ...conversionTransforms,
...migrationTransforms, ...migrationTransforms,

View file

@ -124,7 +124,7 @@ describe('DocumentMigrator', () => {
id: 'me', id: 'me',
type: 'user', type: 'user',
attributes: { name: 'Christopher' }, attributes: { name: 'Christopher' },
migrationVersion: {}, typeMigrationVersion: '',
}) })
).toThrow(/Migrations are not ready. Make sure prepareMigrations is called first./i); ).toThrow(/Migrations are not ready. Make sure prepareMigrations is called first./i);
@ -133,7 +133,7 @@ describe('DocumentMigrator', () => {
id: 'me', id: 'me',
type: 'user', type: 'user',
attributes: { name: 'Christopher' }, attributes: { name: 'Christopher' },
migrationVersion: {}, typeMigrationVersion: '',
}) })
).toThrow(/Migrations are not ready. Make sure prepareMigrations is called first./i); ).toThrow(/Migrations are not ready. Make sure prepareMigrations is called first./i);
}); });
@ -155,13 +155,15 @@ describe('DocumentMigrator', () => {
id: 'me', id: 'me',
type: 'user', type: 'user',
attributes: { name: 'Christopher' }, attributes: { name: 'Christopher' },
migrationVersion: {}, coreMigrationVersion: '8.8.0',
typeMigrationVersion: '',
}); });
expect(actual).toEqual({ expect(actual).toEqual({
id: 'me', id: 'me',
type: 'user', type: 'user',
attributes: { name: 'Chris' }, attributes: { name: 'Chris' },
migrationVersion: { user: '1.2.3' }, coreMigrationVersion: '8.8.0',
typeMigrationVersion: '1.2.3',
}); });
}); });
@ -183,40 +185,14 @@ describe('DocumentMigrator', () => {
id: 'me', id: 'me',
type: 'user', type: 'user',
attributes: {}, attributes: {},
migrationVersion: {}, coreMigrationVersion: '8.8.0',
typeMigrationVersion: '',
}; };
const migratedDoc = migrator.migrate(originalDoc); const migratedDoc = migrator.migrate(originalDoc);
expect(_.get(originalDoc, 'attributes.name')).toBeUndefined(); expect(_.get(originalDoc, 'attributes.name')).toBeUndefined();
expect(_.get(migratedDoc, 'attributes.name')).toBe('Mike'); expect(_.get(migratedDoc, 'attributes.name')).toBe('Mike');
}); });
it('migrates root properties', () => {
const migrator = new DocumentMigrator({
...testOpts(),
typeRegistry: createRegistry({
name: 'acl',
migrations: {
'2.3.5': setAttr('acl', 'admins-only, sucka!'),
},
}),
});
migrator.prepareMigrations();
const actual = migrator.migrate({
id: 'me',
type: 'user',
attributes: { name: 'Tyler' },
acl: 'anyone',
migrationVersion: {},
} as SavedObjectUnsanitizedDoc);
expect(actual).toEqual({
id: 'me',
type: 'user',
attributes: { name: 'Tyler' },
migrationVersion: { acl: '2.3.5' },
acl: 'admins-only, sucka!',
});
});
it('does not apply migrations to unrelated docs', () => { it('does not apply migrations to unrelated docs', () => {
const migrator = new DocumentMigrator({ const migrator = new DocumentMigrator({
...testOpts(), ...testOpts(),
@ -246,7 +222,7 @@ describe('DocumentMigrator', () => {
id: 'me', id: 'me',
type: 'user', type: 'user',
attributes: { name: 'Tyler' }, attributes: { name: 'Tyler' },
migrationVersion: {}, typeMigrationVersion: '',
}); });
expect(actual).toEqual({ expect(actual).toEqual({
id: 'me', id: 'me',
@ -255,7 +231,7 @@ describe('DocumentMigrator', () => {
}); });
}); });
it('assumes documents w/ undefined migrationVersion and correct coreMigrationVersion are up to date', () => { it('assumes documents w/ undefined typeMigrationVersion and correct coreMigrationVersion are up to date', () => {
const migrator = new DocumentMigrator({ const migrator = new DocumentMigrator({
...testOpts(), ...testOpts(),
typeRegistry: createRegistry( typeRegistry: createRegistry(
@ -292,11 +268,8 @@ describe('DocumentMigrator', () => {
type: 'user', type: 'user',
attributes: { name: 'Tyler' }, attributes: { name: 'Tyler' },
bbb: 'Shazm', bbb: 'Shazm',
migrationVersion: {
user: '1.0.0',
bbb: '2.3.4',
},
coreMigrationVersion: kibanaVersion, coreMigrationVersion: kibanaVersion,
typeMigrationVersion: '1.0.0',
}); });
}); });
@ -317,17 +290,19 @@ describe('DocumentMigrator', () => {
id: 'smelly', id: 'smelly',
type: 'dog', type: 'dog',
attributes: { name: 'Callie' }, attributes: { name: 'Callie' },
migrationVersion: { dog: '1.2.3' }, coreMigrationVersion: '8.8.0',
typeMigrationVersion: '1.2.3',
}); });
expect(actual).toEqual({ expect(actual).toEqual({
id: 'smelly', id: 'smelly',
type: 'dog', type: 'dog',
attributes: { name: 'Callie', b: 'B', c: 'C' }, attributes: { name: 'Callie', b: 'B', c: 'C' },
migrationVersion: { dog: '2.0.1' }, coreMigrationVersion: '8.8.0',
typeMigrationVersion: '2.0.1',
}); });
}); });
it('rejects docs with a migrationVersion[type] for a type that does not have any migrations defined', () => { it('rejects docs with a typeMigrationVersion for a type that does not have any migrations defined', () => {
const migrator = new DocumentMigrator({ const migrator = new DocumentMigrator({
...testOpts(), ...testOpts(),
}); });
@ -337,14 +312,15 @@ describe('DocumentMigrator', () => {
id: 'smelly', id: 'smelly',
type: 'dog', type: 'dog',
attributes: { name: 'Callie' }, attributes: { name: 'Callie' },
migrationVersion: { dog: '10.2.0' }, coreMigrationVersion: '8.8.0',
typeMigrationVersion: '10.2.0',
}) })
).toThrow( ).toThrow(
/Document "smelly" has property "dog" which belongs to a more recent version of Kibana \[10\.2\.0\]\. The last known version is \[undefined\]/i /Document "smelly" belongs to a more recent version of Kibana \[10\.2\.0\] when the last known version is \[undefined\]/i
); );
}); });
it('rejects docs with a migrationVersion[type] for a type that does not have a migration >= that version defined', () => { it('rejects docs with a typeMigrationVersion for a type that does not have a migration >= that version defined', () => {
const migrator = new DocumentMigrator({ const migrator = new DocumentMigrator({
...testOpts(), ...testOpts(),
typeRegistry: createRegistry({ typeRegistry: createRegistry({
@ -360,10 +336,10 @@ describe('DocumentMigrator', () => {
id: 'fleabag', id: 'fleabag',
type: 'dawg', type: 'dawg',
attributes: { name: 'Callie' }, attributes: { name: 'Callie' },
migrationVersion: { dawg: '1.2.4' }, typeMigrationVersion: '1.2.4',
}) })
).toThrow( ).toThrow(
/Document "fleabag" has property "dawg" which belongs to a more recent version of Kibana \[1\.2\.4\]\. The last known version is \[1\.2\.3\]/i /Document "fleabag" belongs to a more recent version of Kibana \[1\.2\.4\]\ when the last known version is \[1\.2\.3\]/i
); );
}); });
@ -421,47 +397,43 @@ describe('DocumentMigrator', () => {
id: 'smelly', id: 'smelly',
type: 'dog', type: 'dog',
attributes: { name: 'Callie' }, attributes: { name: 'Callie' },
migrationVersion: { dog: '1.2.0' }, coreMigrationVersion: '8.8.0',
typeMigrationVersion: '1.2.0',
}); });
expect(actual).toEqual({ expect(actual).toEqual({
id: 'smelly', id: 'smelly',
type: 'dog', type: 'dog',
attributes: { name: 'Callie', a: 1, b: 2, c: 3 }, attributes: { name: 'Callie', a: 1, b: 2, c: 3 },
migrationVersion: { dog: '10.0.1' }, coreMigrationVersion: '8.8.0',
typeMigrationVersion: '10.0.1',
}); });
}); });
it('allows props to be added', () => { it('allows props to be added', () => {
const migrator = new DocumentMigrator({ const migrator = new DocumentMigrator({
...testOpts(), ...testOpts(),
typeRegistry: createRegistry( typeRegistry: createRegistry({
{ name: 'dog',
name: 'animal', migrations: {
migrations: { '2.2.4': setAttr('animal', 'Doggie'),
'1.0.0': setAttr('animal', (name: string) => `Animal: ${name}`),
},
}, },
{ }),
name: 'dog',
migrations: {
'2.2.4': setAttr('animal', 'Doggie'),
},
}
),
}); });
migrator.prepareMigrations(); migrator.prepareMigrations();
const actual = migrator.migrate({ const actual = migrator.migrate({
id: 'smelly', id: 'smelly',
type: 'dog', type: 'dog',
attributes: { name: 'Callie' }, attributes: { name: 'Callie' },
migrationVersion: { dog: '1.2.0' }, coreMigrationVersion: '8.8.0',
typeMigrationVersion: '1.2.0',
}); });
expect(actual).toEqual({ expect(actual).toEqual({
id: 'smelly', id: 'smelly',
type: 'dog', type: 'dog',
attributes: { name: 'Callie' }, attributes: { name: 'Callie' },
animal: 'Animal: Doggie', animal: 'Doggie',
migrationVersion: { animal: '1.0.0', dog: '2.2.4' }, coreMigrationVersion: '8.8.0',
typeMigrationVersion: '2.2.4',
}); });
}); });
@ -482,13 +454,15 @@ describe('DocumentMigrator', () => {
id: 'smelly', id: 'smelly',
type: 'dog', type: 'dog',
attributes: { name: 'Callie' }, attributes: { name: 'Callie' },
migrationVersion: {}, coreMigrationVersion: '8.8.0',
typeMigrationVersion: '',
}); });
expect(actual).toEqual({ expect(actual).toEqual({
id: 'smelly', id: 'smelly',
type: 'dog', type: 'dog',
attributes: { title: 'Title: Name: Callie' }, attributes: { title: 'Title: Name: Callie' },
migrationVersion: { dog: '1.0.2' }, coreMigrationVersion: '8.8.0',
typeMigrationVersion: '1.0.2',
}); });
}); });
@ -515,23 +489,25 @@ describe('DocumentMigrator', () => {
id: 'smelly', id: 'smelly',
type: 'dog', type: 'dog',
attributes: { name: 'Callie' }, attributes: { name: 'Callie' },
migrationVersion: {}, typeMigrationVersion: '',
coreMigrationVersion: '8.8.0',
}); });
expect(actual).toEqual({ expect(actual).toEqual({
id: 'smelly', id: 'smelly',
type: 'cat', type: 'cat',
attributes: { name: 'Kitty Callie' }, attributes: { name: 'Kitty Callie' },
migrationVersion: { dog: '2.2.4', cat: '1.0.0' }, coreMigrationVersion: '8.8.0',
typeMigrationVersion: '1.0.0',
}); });
}); });
it('disallows updating a migrationVersion prop to a lower version', () => { it('disallows updating a typeMigrationVersion prop to a lower version', () => {
const migrator = new DocumentMigrator({ const migrator = new DocumentMigrator({
...testOpts(), ...testOpts(),
typeRegistry: createRegistry({ typeRegistry: createRegistry({
name: 'cat', name: 'cat',
migrations: { migrations: {
'1.0.0': setAttr('migrationVersion.foo', '3.2.1'), '4.5.7': setAttr('typeMigrationVersion', '3.2.1'),
}, },
}), }),
}); });
@ -541,20 +517,21 @@ describe('DocumentMigrator', () => {
id: 'smelly', id: 'smelly',
type: 'cat', type: 'cat',
attributes: { name: 'Boo' }, attributes: { name: 'Boo' },
migrationVersion: { foo: '4.5.6' }, typeMigrationVersion: '4.5.6',
coreMigrationVersion: '8.8.0',
}) })
).toThrow( ).toThrow(
/Migration "cat v 1.0.0" attempted to downgrade "migrationVersion.foo" from 4.5.6 to 3.2.1./ /Migration "cat v4.5.7" attempted to downgrade "typeMigrationVersion" from 4.5.6 to 3.2.1./
); );
}); });
it('disallows removing a migrationVersion prop', () => { it('disallows removing a typeMigrationVersion prop', () => {
const migrator = new DocumentMigrator({ const migrator = new DocumentMigrator({
...testOpts(), ...testOpts(),
typeRegistry: createRegistry({ typeRegistry: createRegistry({
name: 'cat', name: 'cat',
migrations: { migrations: {
'1.0.0': setAttr('migrationVersion', {}), '4.5.7': setAttr('typeMigrationVersion', undefined),
}, },
}), }),
}); });
@ -564,45 +541,21 @@ describe('DocumentMigrator', () => {
id: 'smelly', id: 'smelly',
type: 'cat', type: 'cat',
attributes: { name: 'Boo' }, attributes: { name: 'Boo' },
migrationVersion: { foo: '4.5.6' }, coreMigrationVersion: '8.8.0',
typeMigrationVersion: '4.5.6',
}) })
).toThrow( ).toThrow(
/Migration "cat v 1.0.0" attempted to downgrade "migrationVersion.foo" from 4.5.6 to undefined./ /Migration "cat v4.5.7" attempted to downgrade "typeMigrationVersion" from 4.5.6 to undefined./
); );
}); });
it('allows adding props to migrationVersion', () => {
const migrator = new DocumentMigrator({
...testOpts(),
typeRegistry: createRegistry({
name: 'cat',
migrations: {
'1.0.0': setAttr('migrationVersion.foo', '5.6.7'),
},
}),
});
migrator.prepareMigrations();
const actual = migrator.migrate({
id: 'smelly',
type: 'cat',
attributes: { name: 'Boo' },
migrationVersion: {},
});
expect(actual).toEqual({
id: 'smelly',
type: 'cat',
attributes: { name: 'Boo' },
migrationVersion: { cat: '1.0.0', foo: '5.6.7' },
});
});
it('logs the original error and throws a transform error if a document transform fails', () => { it('logs the original error and throws a transform error if a document transform fails', () => {
const log = mockLogger; const log = mockLogger;
const failedDoc = { const failedDoc = {
id: 'smelly', id: 'smelly',
type: 'dog', type: 'dog',
attributes: {}, attributes: {},
migrationVersion: {}, typeMigrationVersion: '',
}; };
const migrator = new DocumentMigrator({ const migrator = new DocumentMigrator({
...testOpts(), ...testOpts(),
@ -648,7 +601,7 @@ describe('DocumentMigrator', () => {
id: 'joker', id: 'joker',
type: 'dog', type: 'dog',
attributes: {}, attributes: {},
migrationVersion: {}, typeMigrationVersion: '',
}; };
migrator.migrate(doc); migrator.migrate(doc);
expect(loggingSystemMock.collect(mockLoggerFactory).info[0][0]).toEqual(logTestMsg); expect(loggingSystemMock.collect(mockLoggerFactory).info[0][0]).toEqual(logTestMsg);
@ -680,7 +633,7 @@ describe('DocumentMigrator', () => {
migrations: { migrations: {
'9.0.0': (doc: SavedObjectUnsanitizedDoc) => doc, '9.0.0': (doc: SavedObjectUnsanitizedDoc) => doc,
}, },
convertToMultiNamespaceTypeVersion: '11.0.0', // this results in reference transforms getting added to other types, but does not increase the migrationVersion of those types convertToMultiNamespaceTypeVersion: '11.0.0', // this results in reference transforms getting added to other types, but does not increase the typeMigrationVersion of those types
} }
), ),
}); });
@ -694,12 +647,12 @@ describe('DocumentMigrator', () => {
}); });
describe('conversion to multi-namespace type', () => { describe('conversion to multi-namespace type', () => {
it('assumes documents w/ undefined migrationVersion and correct coreMigrationVersion are up to date', () => { it('assumes documents w/ undefined typeMigrationVersion and correct coreMigrationVersion are up to date', () => {
const migrator = new DocumentMigrator({ const migrator = new DocumentMigrator({
...testOpts(), ...testOpts(),
typeRegistry: createRegistry( typeRegistry: createRegistry(
{ name: 'dog', namespaceType: 'multiple', convertToMultiNamespaceTypeVersion: '1.0.0' } { name: 'dog', namespaceType: 'multiple', convertToMultiNamespaceTypeVersion: '1.0.0' }
// no migration transforms are defined, the migrationVersion will be derived from 'convertToMultiNamespaceTypeVersion' // no migration transforms are defined, the typeMigrationVersion will be derived from 'convertToMultiNamespaceTypeVersion'
), ),
}); });
migrator.prepareMigrations(); migrator.prepareMigrations();
@ -715,8 +668,8 @@ describe('DocumentMigrator', () => {
id: 'mischievous', id: 'mischievous',
type: 'dog', type: 'dog',
attributes: { name: 'Ann' }, attributes: { name: 'Ann' },
migrationVersion: { dog: '1.0.0' },
coreMigrationVersion: kibanaVersion, coreMigrationVersion: kibanaVersion,
typeMigrationVersion: '1.0.0',
// there is no 'namespaces' field because no transforms were applied; this scenario is contrived for a clean test case but is not indicative of a real-world scenario // there is no 'namespaces' field because no transforms were applied; this scenario is contrived for a clean test case but is not indicative of a real-world scenario
}, },
]); ]);
@ -727,7 +680,7 @@ describe('DocumentMigrator', () => {
...testOpts(), ...testOpts(),
typeRegistry: createRegistry( typeRegistry: createRegistry(
{ name: 'dog', namespaceType: 'multiple', convertToMultiNamespaceTypeVersion: '1.0.0' } { name: 'dog', namespaceType: 'multiple', convertToMultiNamespaceTypeVersion: '1.0.0' }
// no migration transforms are defined, the migrationVersion will be derived from 'convertToMultiNamespaceTypeVersion' // no migration transforms are defined, the typeMigrationVersion will be derived from 'convertToMultiNamespaceTypeVersion'
), ),
}); });
migrator.prepareMigrations(); migrator.prepareMigrations();
@ -735,8 +688,8 @@ describe('DocumentMigrator', () => {
id: 'mischievous', id: 'mischievous',
type: 'dog', type: 'dog',
attributes: { name: 'Ann' }, attributes: { name: 'Ann' },
migrationVersion: { dog: '0.1.0' }, coreMigrationVersion: '20.0.0',
coreMigrationVersion: '2.0.0', typeMigrationVersion: '0.1.0',
} as SavedObjectUnsanitizedDoc; } as SavedObjectUnsanitizedDoc;
const actual = migrator.migrateAndConvert(obj); const actual = migrator.migrateAndConvert(obj);
expect(actual).toEqual([ expect(actual).toEqual([
@ -744,8 +697,8 @@ describe('DocumentMigrator', () => {
id: 'mischievous', id: 'mischievous',
type: 'dog', type: 'dog',
attributes: { name: 'Ann' }, attributes: { name: 'Ann' },
migrationVersion: { dog: '1.0.0' }, coreMigrationVersion: '20.0.0',
coreMigrationVersion: '2.0.0', typeMigrationVersion: '1.0.0',
namespaces: ['default'], namespaces: ['default'],
}, },
]); ]);
@ -755,8 +708,8 @@ describe('DocumentMigrator', () => {
const migrator = new DocumentMigrator({ const migrator = new DocumentMigrator({
...testOpts(), ...testOpts(),
typeRegistry: createRegistry( typeRegistry: createRegistry(
{ name: 'dog', namespaceType: 'multiple', convertToMultiNamespaceTypeVersion: '1.0.0' }, { name: 'dog', namespaceType: 'multiple', convertToMultiNamespaceTypeVersion: '8.8.0' },
{ name: 'toy', namespaceType: 'multiple', convertToMultiNamespaceTypeVersion: '1.0.0' } { name: 'toy', namespaceType: 'multiple', convertToMultiNamespaceTypeVersion: '8.8.0' }
), ),
}); });
migrator.prepareMigrations(); migrator.prepareMigrations();
@ -764,9 +717,10 @@ describe('DocumentMigrator', () => {
id: 'cowardly', id: 'cowardly',
type: 'dog', type: 'dog',
attributes: { name: 'Leslie' }, attributes: { name: 'Leslie' },
migrationVersion: {},
references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }], references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }],
namespace: 'foo-namespace', namespace: 'foo-namespace',
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '',
}; };
const actual = migrator.migrate(obj); const actual = migrator.migrate(obj);
expect(mockGetConvertedObjectId).not.toHaveBeenCalled(); expect(mockGetConvertedObjectId).not.toHaveBeenCalled();
@ -775,13 +729,14 @@ describe('DocumentMigrator', () => {
type: 'dog', type: 'dog',
attributes: { name: 'Leslie' }, attributes: { name: 'Leslie' },
references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }], references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }],
coreMigrationVersion: '1.0.0', coreMigrationVersion: '8.8.0',
typeMigrationVersion: '8.8.0',
namespace: 'foo-namespace', namespace: 'foo-namespace',
// there is no 'namespaces' field because no conversion transform was applied; this scenario is contrived for a clean test case but is not indicative of a real-world scenario // there is no 'namespaces' field because no conversion transform was applied; this scenario is contrived for a clean test case but is not indicative of a real-world scenario
}); });
}); });
it('should keep the same `migrationVersion` when the conversion transforms are skipped', () => { it('should keep the same `typeMigrationVersion` when the conversion transforms are skipped', () => {
const migrator = new DocumentMigrator({ const migrator = new DocumentMigrator({
...testOpts(), ...testOpts(),
typeRegistry: createRegistry({ typeRegistry: createRegistry({
@ -798,7 +753,8 @@ describe('DocumentMigrator', () => {
id: 'cowardly', id: 'cowardly',
type: 'dog', type: 'dog',
attributes: { name: 'Leslie' }, attributes: { name: 'Leslie' },
migrationVersion: { dog: '2.0.0' }, coreMigrationVersion: '8.8.0',
typeMigrationVersion: '2.0.0',
references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }], references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }],
namespace: 'foo-namespace', namespace: 'foo-namespace',
}; };
@ -808,14 +764,67 @@ describe('DocumentMigrator', () => {
id: 'cowardly', id: 'cowardly',
type: 'dog', type: 'dog',
attributes: { name: 'Leslie' }, attributes: { name: 'Leslie' },
migrationVersion: { dog: '2.0.0' },
references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }], references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }],
coreMigrationVersion: '3.0.0', coreMigrationVersion: '8.8.0',
typeMigrationVersion: '2.0.0',
namespace: 'foo-namespace', namespace: 'foo-namespace',
// there is no 'namespaces' field because no conversion transform was applied; this scenario is contrived for a clean test case but is not indicative of a real-world scenario // there is no 'namespaces' field because no conversion transform was applied; this scenario is contrived for a clean test case but is not indicative of a real-world scenario
}); });
}); });
describe('correctly applies core transforms', () => {
const migrator = new DocumentMigrator({
...testOpts(),
typeRegistry: createRegistry(
{
name: 'dog',
namespaceType: 'single',
migrations: { '1.0.0': (doc) => doc },
},
{ name: 'toy', namespaceType: 'multiple' }
),
});
migrator.prepareMigrations();
const obj = {
id: 'bad',
type: 'dog',
attributes: { name: 'Sweet Peach' },
migrationVersion: { dog: '1.0.0' },
references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }],
};
it('in the default space', () => {
const actual = migrator.migrateAndConvert(obj);
expect(mockGetConvertedObjectId).not.toHaveBeenCalled();
expect(actual).toEqual([
{
id: 'bad',
type: 'dog',
attributes: { name: 'Sweet Peach' },
references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }],
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '1.0.0',
},
]);
});
it('in a non-default space', () => {
const actual = migrator.migrateAndConvert({ ...obj, namespace: 'foo-namespace' });
expect(mockGetConvertedObjectId).not.toHaveBeenCalled();
expect(actual).toEqual([
{
id: 'bad',
type: 'dog',
attributes: { name: 'Sweet Peach' },
references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }],
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '1.0.0',
namespace: 'foo-namespace',
},
]);
});
});
describe('correctly applies reference transforms', () => { describe('correctly applies reference transforms', () => {
const migrator = new DocumentMigrator({ const migrator = new DocumentMigrator({
...testOpts(), ...testOpts(),
@ -829,7 +838,7 @@ describe('DocumentMigrator', () => {
id: 'bad', id: 'bad',
type: 'dog', type: 'dog',
attributes: { name: 'Sweet Peach' }, attributes: { name: 'Sweet Peach' },
migrationVersion: {}, typeMigrationVersion: '',
references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }], references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }],
}; };
@ -842,7 +851,7 @@ describe('DocumentMigrator', () => {
type: 'dog', type: 'dog',
attributes: { name: 'Sweet Peach' }, attributes: { name: 'Sweet Peach' },
references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }], // no change references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }], // no change
coreMigrationVersion: '1.0.0', coreMigrationVersion: '8.8.0',
}, },
]); ]);
}); });
@ -857,7 +866,7 @@ describe('DocumentMigrator', () => {
type: 'dog', type: 'dog',
attributes: { name: 'Sweet Peach' }, attributes: { name: 'Sweet Peach' },
references: [{ id: 'uuidv5', type: 'toy', name: 'BALL!' }], // changed references: [{ id: 'uuidv5', type: 'toy', name: 'BALL!' }], // changed
coreMigrationVersion: '1.0.0', coreMigrationVersion: '8.8.0',
namespace: 'foo-namespace', namespace: 'foo-namespace',
}, },
]); ]);
@ -878,7 +887,8 @@ describe('DocumentMigrator', () => {
id: 'loud', id: 'loud',
type: 'dog', type: 'dog',
attributes: { name: 'Wally' }, attributes: { name: 'Wally' },
migrationVersion: {}, coreMigrationVersion: '8.8.0',
typeMigrationVersion: '',
}; };
it('in the default space', () => { it('in the default space', () => {
@ -889,8 +899,8 @@ describe('DocumentMigrator', () => {
id: 'loud', id: 'loud',
type: 'dog', type: 'dog',
attributes: { name: 'Wally' }, attributes: { name: 'Wally' },
migrationVersion: { dog: '1.0.0' }, coreMigrationVersion: '8.8.0',
coreMigrationVersion: '1.0.0', typeMigrationVersion: '1.0.0',
namespaces: ['default'], namespaces: ['default'],
}, },
]); ]);
@ -905,8 +915,8 @@ describe('DocumentMigrator', () => {
id: 'uuidv5', id: 'uuidv5',
type: 'dog', type: 'dog',
attributes: { name: 'Wally' }, attributes: { name: 'Wally' },
migrationVersion: { dog: '1.0.0' }, coreMigrationVersion: '8.8.0',
coreMigrationVersion: '1.0.0', typeMigrationVersion: '1.0.0',
namespaces: ['foo-namespace'], namespaces: ['foo-namespace'],
originId: 'loud', originId: 'loud',
}, },
@ -920,14 +930,14 @@ describe('DocumentMigrator', () => {
targetId: 'uuidv5', targetId: 'uuidv5',
purpose: 'savedObjectConversion', purpose: 'savedObjectConversion',
}, },
migrationVersion: { [LEGACY_URL_ALIAS_TYPE]: '0.1.2' }, coreMigrationVersion: '8.8.0',
coreMigrationVersion: '1.0.0', typeMigrationVersion: '0.1.2',
}, },
]); ]);
}); });
}); });
describe('correctly applies reference and conversion transforms', () => { describe('correctly applies core, reference, and conversion transforms', () => {
const migrator = new DocumentMigrator({ const migrator = new DocumentMigrator({
...testOpts(), ...testOpts(),
typeRegistry: createRegistry( typeRegistry: createRegistry(
@ -952,9 +962,9 @@ describe('DocumentMigrator', () => {
id: 'cute', id: 'cute',
type: 'dog', type: 'dog',
attributes: { name: 'Too' }, attributes: { name: 'Too' },
migrationVersion: { dog: '1.0.0' },
references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }], // no change references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }], // no change
coreMigrationVersion: '1.0.0', coreMigrationVersion: '8.8.0',
typeMigrationVersion: '1.0.0',
namespaces: ['default'], namespaces: ['default'],
}, },
]); ]);
@ -980,9 +990,9 @@ describe('DocumentMigrator', () => {
id: 'uuidv5', id: 'uuidv5',
type: 'dog', type: 'dog',
attributes: { name: 'Too' }, attributes: { name: 'Too' },
migrationVersion: { dog: '1.0.0' },
references: [{ id: 'uuidv5', type: 'toy', name: 'BALL!' }], // changed references: [{ id: 'uuidv5', type: 'toy', name: 'BALL!' }], // changed
coreMigrationVersion: '1.0.0', coreMigrationVersion: '8.8.0',
typeMigrationVersion: '1.0.0',
namespaces: ['foo-namespace'], namespaces: ['foo-namespace'],
originId: 'cute', originId: 'cute',
}, },
@ -996,14 +1006,14 @@ describe('DocumentMigrator', () => {
targetId: 'uuidv5', targetId: 'uuidv5',
purpose: 'savedObjectConversion', purpose: 'savedObjectConversion',
}, },
migrationVersion: { [LEGACY_URL_ALIAS_TYPE]: '0.1.2' }, coreMigrationVersion: '8.8.0',
coreMigrationVersion: '1.0.0', typeMigrationVersion: '0.1.2',
}, },
]); ]);
}); });
}); });
describe('correctly applies reference and migration transforms', () => { describe('correctly applies core, reference, and migration transforms', () => {
const migrator = new DocumentMigrator({ const migrator = new DocumentMigrator({
...testOpts(), ...testOpts(),
typeRegistry: createRegistry( typeRegistry: createRegistry(
@ -1037,9 +1047,9 @@ describe('DocumentMigrator', () => {
id: 'sleepy', id: 'sleepy',
type: 'dog', type: 'dog',
attributes: { name: 'Patches', age: '11', color: 'tri-color' }, attributes: { name: 'Patches', age: '11', color: 'tri-color' },
migrationVersion: { dog: '2.0.0' },
references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }], // no change references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }], // no change
coreMigrationVersion: '1.0.0', coreMigrationVersion: '8.8.0',
typeMigrationVersion: '2.0.0',
}, },
]); ]);
}); });
@ -1053,9 +1063,9 @@ describe('DocumentMigrator', () => {
id: 'sleepy', id: 'sleepy',
type: 'dog', type: 'dog',
attributes: { name: 'Patches', age: '11', color: 'tri-color' }, attributes: { name: 'Patches', age: '11', color: 'tri-color' },
migrationVersion: { dog: '2.0.0' },
references: [{ id: 'uuidv5', type: 'toy', name: 'BALL!' }], // changed references: [{ id: 'uuidv5', type: 'toy', name: 'BALL!' }], // changed
coreMigrationVersion: '1.0.0', coreMigrationVersion: '8.8.0',
typeMigrationVersion: '2.0.0',
namespace: 'foo-namespace', namespace: 'foo-namespace',
}, },
]); ]);
@ -1069,7 +1079,7 @@ describe('DocumentMigrator', () => {
name: 'dog', name: 'dog',
namespaceType: 'multiple', namespaceType: 'multiple',
migrations: { migrations: {
'1.0.0': setAttr('migrationVersion.dog', '2.0.0'), '1.0.0': setAttr('typeMigrationVersion', '2.0.0'),
'2.0.0': (doc) => doc, // noop '2.0.0': (doc) => doc, // noop
}, },
convertToMultiNamespaceTypeVersion: '1.0.0', // the conversion transform occurs before the migration transform above convertToMultiNamespaceTypeVersion: '1.0.0', // the conversion transform occurs before the migration transform above
@ -1080,7 +1090,8 @@ describe('DocumentMigrator', () => {
id: 'hungry', id: 'hungry',
type: 'dog', type: 'dog',
attributes: { name: 'Remy' }, attributes: { name: 'Remy' },
migrationVersion: {}, coreMigrationVersion: '8.8.0',
typeMigrationVersion: '',
}; };
it('in the default space', () => { it('in the default space', () => {
@ -1091,8 +1102,8 @@ describe('DocumentMigrator', () => {
id: 'hungry', id: 'hungry',
type: 'dog', type: 'dog',
attributes: { name: 'Remy' }, attributes: { name: 'Remy' },
migrationVersion: { dog: '2.0.0' }, coreMigrationVersion: '8.8.0',
coreMigrationVersion: '1.0.0', typeMigrationVersion: '2.0.0',
namespaces: ['default'], namespaces: ['default'],
}, },
]); ]);
@ -1107,8 +1118,8 @@ describe('DocumentMigrator', () => {
id: 'uuidv5', id: 'uuidv5',
type: 'dog', type: 'dog',
attributes: { name: 'Remy' }, attributes: { name: 'Remy' },
migrationVersion: { dog: '2.0.0' }, coreMigrationVersion: '8.8.0',
coreMigrationVersion: '1.0.0', typeMigrationVersion: '2.0.0',
namespaces: ['foo-namespace'], namespaces: ['foo-namespace'],
originId: 'hungry', originId: 'hungry',
}, },
@ -1122,14 +1133,14 @@ describe('DocumentMigrator', () => {
targetId: 'uuidv5', targetId: 'uuidv5',
purpose: 'savedObjectConversion', purpose: 'savedObjectConversion',
}, },
migrationVersion: { [LEGACY_URL_ALIAS_TYPE]: '0.1.2' }, coreMigrationVersion: '8.8.0',
coreMigrationVersion: '1.0.0', typeMigrationVersion: '0.1.2',
}, },
]); ]);
}); });
}); });
describe('correctly applies reference, conversion, and migration transforms', () => { describe('correctly applies core, reference, conversion, and migration transforms', () => {
const migrator = new DocumentMigrator({ const migrator = new DocumentMigrator({
...testOpts(), ...testOpts(),
typeRegistry: createRegistry( typeRegistry: createRegistry(
@ -1137,7 +1148,7 @@ describe('DocumentMigrator', () => {
name: 'dog', name: 'dog',
namespaceType: 'multiple', namespaceType: 'multiple',
migrations: { migrations: {
'1.0.0': setAttr('migrationVersion.dog', '2.0.0'), '1.0.0': setAttr('typeMigrationVersion', '2.0.0'),
'2.0.0': (doc) => doc, // noop '2.0.0': (doc) => doc, // noop
}, },
convertToMultiNamespaceTypeVersion: '1.0.0', convertToMultiNamespaceTypeVersion: '1.0.0',
@ -1162,9 +1173,9 @@ describe('DocumentMigrator', () => {
id: 'pretty', id: 'pretty',
type: 'dog', type: 'dog',
attributes: { name: 'Sasha' }, attributes: { name: 'Sasha' },
migrationVersion: { dog: '2.0.0' },
references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }], // no change references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }], // no change
coreMigrationVersion: '1.0.0', coreMigrationVersion: '8.8.0',
typeMigrationVersion: '2.0.0',
namespaces: ['default'], namespaces: ['default'],
}, },
]); ]);
@ -1190,9 +1201,9 @@ describe('DocumentMigrator', () => {
id: 'uuidv5', id: 'uuidv5',
type: 'dog', type: 'dog',
attributes: { name: 'Sasha' }, attributes: { name: 'Sasha' },
migrationVersion: { dog: '2.0.0' },
references: [{ id: 'uuidv5', type: 'toy', name: 'BALL!' }], // changed references: [{ id: 'uuidv5', type: 'toy', name: 'BALL!' }], // changed
coreMigrationVersion: '1.0.0', coreMigrationVersion: '8.8.0',
typeMigrationVersion: '2.0.0',
namespaces: ['foo-namespace'], namespaces: ['foo-namespace'],
originId: 'pretty', originId: 'pretty',
}, },
@ -1206,13 +1217,117 @@ describe('DocumentMigrator', () => {
targetId: 'uuidv5', targetId: 'uuidv5',
purpose: 'savedObjectConversion', purpose: 'savedObjectConversion',
}, },
migrationVersion: { [LEGACY_URL_ALIAS_TYPE]: '0.1.2' }, coreMigrationVersion: '8.8.0',
coreMigrationVersion: '1.0.0', typeMigrationVersion: '0.1.2',
}, },
]); ]);
}); });
}); });
}); });
describe('`typeMigrationVersion` core migration', () => {
let migrator: DocumentMigrator;
let noop: jest.MockedFunction<(doc: SavedObjectUnsanitizedDoc) => SavedObjectUnsanitizedDoc>;
beforeEach(() => {
noop = jest.fn((doc) => doc);
migrator = new DocumentMigrator({
...testOpts(),
typeRegistry: createRegistry({
name: 'dog',
migrations: {
'1.0.0': noop,
},
}),
});
migrator.prepareMigrations();
});
it('migrates to `typeMigrationVersion`', () => {
const actual = migrator.migrate({
id: 'smelly',
type: 'dog',
attributes: {},
migrationVersion: { dog: '1.0.0' },
});
expect(actual).toHaveProperty('typeMigrationVersion', '1.0.0');
});
it('ignores unrelated versions', () => {
const actual = migrator.migrate({
id: 'smelly',
type: 'dog',
attributes: {},
migrationVersion: {
dog: '1.0.0',
cat: '2.0.0',
},
});
expect(actual).toHaveProperty('typeMigrationVersion', '1.0.0');
});
it('removes `migrationVersion` property', () => {
const actual = migrator.migrate({
id: 'smelly',
type: 'dog',
attributes: {},
migrationVersion: {
dog: '1.0.0',
cat: '2.0.0',
},
});
expect(actual).not.toHaveProperty('migrationVersion');
});
it('migrates to the latest on missing version', () => {
const actual = migrator.migrate({
id: 'smelly',
type: 'dog',
attributes: {},
migrationVersion: {},
coreMigrationVersion: '8.7.0',
});
expect(noop).toHaveBeenCalledWith(
expect.objectContaining({ typeMigrationVersion: '' }),
expect.anything()
);
expect(actual).toHaveProperty('typeMigrationVersion', '1.0.0');
});
it('does not migrate if there is no `migrationVersion`', () => {
const actual = migrator.migrate({
id: 'smelly',
type: 'dog',
attributes: {},
coreMigrationVersion: '8.7.0',
});
expect(noop).not.toHaveBeenCalled();
expect(actual).toHaveProperty('coreMigrationVersion', '8.8.0');
expect(actual).toHaveProperty('typeMigrationVersion', '1.0.0');
expect(actual).not.toHaveProperty('migrationVersion');
});
it('does not add `typeMigrationVersion` if there are no migrations', () => {
migrator = new DocumentMigrator({
...testOpts(),
typeRegistry: createRegistry({
name: 'dog',
}),
});
migrator.prepareMigrations();
const actual = migrator.migrate({
id: 'smelly',
type: 'dog',
attributes: {},
coreMigrationVersion: '8.7.0',
});
expect(noop).not.toHaveBeenCalled();
expect(actual).toHaveProperty('coreMigrationVersion', '8.8.0');
expect(actual).not.toHaveProperty('typeMigrationVersion');
expect(actual).not.toHaveProperty('migrationVersion');
});
});
}); });
}); });

View file

@ -41,28 +41,21 @@
* given an empty migrationVersion property {} if no such property exists. * given an empty migrationVersion property {} if no such property exists.
*/ */
import Boom from '@hapi/boom';
import { set } from '@kbn/safer-lodash-set';
import _ from 'lodash'; import _ from 'lodash';
import Semver from 'semver';
import type { Logger } from '@kbn/logging'; import type { Logger } from '@kbn/logging';
import type { SavedObjectsMigrationVersion } from '@kbn/core-saved-objects-common'; import type { SavedObjectsMigrationVersion } from '@kbn/core-saved-objects-common';
import type { import type {
SavedObjectUnsanitizedDoc, SavedObjectUnsanitizedDoc,
ISavedObjectTypeRegistry, ISavedObjectTypeRegistry,
} from '@kbn/core-saved-objects-server'; } from '@kbn/core-saved-objects-server';
import type { ActiveMigrations, TransformResult } from './types'; import type { ActiveMigrations } from './types';
import { maxVersion } from './utils'; import { maxVersion } from './utils';
import { buildActiveMigrations } from './build_active_migrations'; import { buildActiveMigrations } from './build_active_migrations';
import { DocumentMigratorPipeline } from './document_migrator_pipeline';
export type MigrateFn = (doc: SavedObjectUnsanitizedDoc) => SavedObjectUnsanitizedDoc; export type MigrateFn = (doc: SavedObjectUnsanitizedDoc) => SavedObjectUnsanitizedDoc;
export type MigrateAndConvertFn = (doc: SavedObjectUnsanitizedDoc) => SavedObjectUnsanitizedDoc[]; export type MigrateAndConvertFn = (doc: SavedObjectUnsanitizedDoc) => SavedObjectUnsanitizedDoc[];
type ApplyTransformsFn = (
doc: SavedObjectUnsanitizedDoc,
options?: TransformOptions
) => TransformResult;
interface TransformOptions { interface TransformOptions {
convertNamespaceTypes?: boolean; convertNamespaceTypes?: boolean;
} }
@ -90,7 +83,6 @@ export interface VersionedTransformer {
export class DocumentMigrator implements VersionedTransformer { export class DocumentMigrator implements VersionedTransformer {
private documentMigratorOptions: DocumentMigratorOptions; private documentMigratorOptions: DocumentMigratorOptions;
private migrations?: ActiveMigrations; private migrations?: ActiveMigrations;
private transformDoc?: ApplyTransformsFn;
/** /**
* Creates an instance of DocumentMigrator. * Creates an instance of DocumentMigrator.
@ -143,12 +135,33 @@ export class DocumentMigrator implements VersionedTransformer {
log, log,
convertVersion, convertVersion,
}); });
this.transformDoc = buildDocumentTransform({
kibanaVersion,
migrations: this.migrations,
});
}; };
private transform(
doc: SavedObjectUnsanitizedDoc,
{ convertNamespaceTypes = false }: TransformOptions = {}
) {
if (!this.migrations) {
throw new Error('Migrations are not ready. Make sure prepareMigrations is called first.');
}
// Clone the document to prevent accidental mutations on the original data
// Ex: Importing sample data that is cached at import level, migrations would
// execute on mutated data the second time.
const clonedDoc = _.cloneDeep(doc);
const pipeline = new DocumentMigratorPipeline(
clonedDoc,
this.migrations,
this.documentMigratorOptions.kibanaVersion,
convertNamespaceTypes
);
pipeline.run();
const { document, additionalDocs } = pipeline;
return { document, additionalDocs };
}
/** /**
* Migrates a document to the latest version. * Migrates a document to the latest version.
* *
@ -157,16 +170,9 @@ export class DocumentMigrator implements VersionedTransformer {
* @memberof DocumentMigrator * @memberof DocumentMigrator
*/ */
public migrate = (doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc => { public migrate = (doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc => {
if (!this.migrations || !this.transformDoc) { const { document } = this.transform(doc);
throw new Error('Migrations are not ready. Make sure prepareMigrations is called first.');
}
// Clone the document to prevent accidental mutations on the original data return document;
// Ex: Importing sample data that is cached at import level, migrations would
// execute on mutated data the second time.
const clonedDoc = _.cloneDeep(doc);
const { transformedDoc } = this.transformDoc(clonedDoc);
return transformedDoc;
}; };
/** /**
@ -178,364 +184,8 @@ export class DocumentMigrator implements VersionedTransformer {
* @memberof DocumentMigrator * @memberof DocumentMigrator
*/ */
public migrateAndConvert = (doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc[] => { public migrateAndConvert = (doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc[] => {
if (!this.migrations || !this.transformDoc) { const { document, additionalDocs } = this.transform(doc, { convertNamespaceTypes: true });
throw new Error('Migrations are not ready. Make sure prepareMigrations is called first.');
}
// Clone the document to prevent accidental mutations on the original data return [document, ...additionalDocs];
// Ex: Importing sample data that is cached at import level, migrations would
// execute on mutated data the second time.
const clonedDoc = _.cloneDeep(doc);
const { transformedDoc, additionalDocs } = this.transformDoc(clonedDoc, {
convertNamespaceTypes: true,
});
return [transformedDoc, ...additionalDocs];
}; };
} }
/**
* Creates a function which migrates and validates any document that is passed to it.
*/
function buildDocumentTransform({
kibanaVersion,
migrations,
}: {
kibanaVersion: string;
migrations: ActiveMigrations;
}): ApplyTransformsFn {
return function transformAndValidate(
doc: SavedObjectUnsanitizedDoc,
options: TransformOptions = {}
) {
validateCoreMigrationVersion(doc, kibanaVersion);
const { convertNamespaceTypes = false } = options;
let transformedDoc: SavedObjectUnsanitizedDoc;
let additionalDocs: SavedObjectUnsanitizedDoc[] = [];
if (doc.migrationVersion) {
const result = applyMigrations(doc, migrations, convertNamespaceTypes);
transformedDoc = result.transformedDoc;
additionalDocs = additionalDocs.concat(
result.additionalDocs.map((x) => markAsUpToDate(x, migrations))
);
} else {
transformedDoc = markAsUpToDate(doc, migrations);
}
// In order to keep tests a bit more stable, we won't
// tack on an empty migrationVersion to docs that have
// no migrations defined.
if (_.isEmpty(transformedDoc.migrationVersion)) {
delete transformedDoc.migrationVersion;
}
return { transformedDoc, additionalDocs };
};
}
function validateCoreMigrationVersion(doc: SavedObjectUnsanitizedDoc, kibanaVersion: string) {
const { id, coreMigrationVersion: docVersion } = doc;
if (!docVersion) {
return;
}
// We verify that the object's coreMigrationVersion is valid, and that it is not greater than the version supported by Kibana.
// If we have a coreMigrationVersion and the kibanaVersion is smaller than it or does not exist, we are dealing with a document that
// belongs to a future Kibana / plugin version.
if (!Semver.valid(docVersion)) {
throw Boom.badData(
`Document "${id}" has an invalid "coreMigrationVersion" [${docVersion}]. This must be a semver value.`,
doc
);
}
if (docVersion && Semver.gt(docVersion, kibanaVersion)) {
throw Boom.badData(
`Document "${id}" has a "coreMigrationVersion" which belongs to a more recent version` +
` of Kibana [${docVersion}]. The current version is [${kibanaVersion}].`,
doc
);
}
}
function applyMigrations(
doc: SavedObjectUnsanitizedDoc,
migrations: ActiveMigrations,
convertNamespaceTypes: boolean
) {
let additionalDocs: SavedObjectUnsanitizedDoc[] = [];
while (true) {
const prop = nextUnmigratedProp(doc, migrations, convertNamespaceTypes);
if (!prop) {
// Ensure that newly created documents have an up-to-date coreMigrationVersion field
const { coreMigrationVersion = getLatestCoreVersion(doc, migrations), ...transformedDoc } =
doc;
return {
transformedDoc: {
...transformedDoc,
...(coreMigrationVersion ? { coreMigrationVersion } : {}),
},
additionalDocs,
};
}
const result = migrateProp(doc, prop, migrations, convertNamespaceTypes);
doc = result.transformedDoc;
additionalDocs = [...additionalDocs, ...result.additionalDocs];
}
}
/**
* Gets the doc's props, handling the special case of "type".
*/
function props(doc: SavedObjectUnsanitizedDoc) {
return Object.keys(doc).concat(doc.type);
}
/**
* Looks up the prop version in a saved object document or in our latest migrations.
*/
function propVersion(doc: SavedObjectUnsanitizedDoc, prop: string) {
return doc.migrationVersion && (doc as any).migrationVersion[prop];
}
/**
* Sets the doc's migrationVersion to be the most recent version
*/
function markAsUpToDate(doc: SavedObjectUnsanitizedDoc, migrations: ActiveMigrations) {
const { coreMigrationVersion = getLatestCoreVersion(doc, migrations), ...rest } = doc;
return {
...rest,
migrationVersion: props(doc).reduce((acc, prop) => {
const version = maxVersion(
migrations[prop]?.latestVersion.migrate,
migrations[prop]?.latestVersion.convert
);
return version ? set(acc, prop, version) : acc;
}, {}),
...(coreMigrationVersion ? { coreMigrationVersion } : {}),
};
}
/**
* Determines whether or not a document has any pending transforms that should be applied based on its coreMigrationVersion field.
* Currently, only reference transforms qualify.
*/
function hasPendingCoreTransform(
doc: SavedObjectUnsanitizedDoc,
migrations: ActiveMigrations,
prop: string
) {
if (!migrations.hasOwnProperty(prop)) {
return false;
}
const latestCoreMigrationVersion = migrations[prop].latestVersion.reference;
const { coreMigrationVersion } = doc;
return (
latestCoreMigrationVersion &&
(!coreMigrationVersion || Semver.gt(latestCoreMigrationVersion, coreMigrationVersion))
);
}
/**
* Determines whether or not a document has any pending conversion transforms that should be applied.
* Currently, only reference transforms qualify.
*/
function hasPendingConversionTransform(
doc: SavedObjectUnsanitizedDoc,
migrations: ActiveMigrations,
prop: string
) {
if (!migrations.hasOwnProperty(prop)) {
return false;
}
const latestVersion = migrations[prop].latestVersion.convert;
const migrationVersion = doc.migrationVersion?.[prop];
return latestVersion && (!migrationVersion || Semver.gt(latestVersion, migrationVersion));
}
/**
* Determines whether or not a document has any pending transforms that should be applied based on its coreMigrationVersion field.
* Currently, only reference transforms qualify.
*/
function hasPendingMigrationTransform(
doc: SavedObjectUnsanitizedDoc,
migrations: ActiveMigrations,
prop: string
) {
if (!migrations.hasOwnProperty(prop)) {
return false;
}
const latestVersion = migrations[prop].latestVersion.migrate;
const migrationVersion = doc.migrationVersion?.[prop];
return latestVersion && (!migrationVersion || Semver.gt(latestVersion, migrationVersion));
}
function getLatestCoreVersion(doc: SavedObjectUnsanitizedDoc, migrations: ActiveMigrations) {
let latestVersion: string | undefined;
for (const prop of props(doc)) {
latestVersion = maxVersion(latestVersion, migrations[prop]?.latestVersion.reference);
}
return latestVersion;
}
/**
* Finds the first unmigrated property in the specified document.
*/
function nextUnmigratedProp(
doc: SavedObjectUnsanitizedDoc,
migrations: ActiveMigrations,
convertNamespaceTypes: boolean
) {
return props(doc).find((p) => {
const latestMigrationVersion = maxVersion(
migrations[p]?.latestVersion.migrate,
migrations[p]?.latestVersion.convert
);
const docVersion = propVersion(doc, p);
// We verify that the version is not greater than the version supported by Kibana.
// If we didn't, this would cause an infinite loop, as we'd be unable to migrate the property
// but it would continue to show up as unmigrated.
// If we have a docVersion and the latestMigrationVersion is smaller than it or does not exist,
// we are dealing with a document that belongs to a future Kibana / plugin version.
if (docVersion && (!latestMigrationVersion || Semver.gt(docVersion, latestMigrationVersion))) {
throw Boom.badData(
`Document "${doc.id}" has property "${p}" which belongs to a more recent` +
` version of Kibana [${docVersion}]. The last known version is [${latestMigrationVersion}]`,
doc
);
}
return (
hasPendingMigrationTransform(doc, migrations, p) ||
(convertNamespaceTypes && // If the object itself is up-to-date, check if its references are up-to-date too
(hasPendingCoreTransform(doc, migrations, p) ||
hasPendingConversionTransform(doc, migrations, p)))
);
});
}
/**
* Applies any relevant migrations to the document for the specified property.
*/
function migrateProp(
doc: SavedObjectUnsanitizedDoc,
prop: string,
migrations: ActiveMigrations,
convertNamespaceTypes: boolean
): TransformResult {
const originalType = doc.type;
let migrationVersion = _.clone(doc.migrationVersion) || {};
let additionalDocs: SavedObjectUnsanitizedDoc[] = [];
for (const { version, transform, transformType } of applicableTransforms(
doc,
prop,
migrations,
convertNamespaceTypes
)) {
const result = transform(doc);
doc = result.transformedDoc;
additionalDocs = [...additionalDocs, ...result.additionalDocs];
if (transformType === 'reference') {
// regardless of whether or not the reference transform was applied, update the object's coreMigrationVersion
// this is needed to ensure that we don't have an endless migration loop
doc.coreMigrationVersion = version;
} else {
migrationVersion = updateMigrationVersion(doc, migrationVersion, prop, version);
doc.migrationVersion = _.clone(migrationVersion);
}
if (doc.type !== originalType) {
// the transform function changed the object's type; break out of the loop
break;
}
}
return { transformedDoc: doc, additionalDocs };
}
/**
* Retrieves any prop transforms that have not been applied to doc.
*/
function applicableTransforms(
doc: SavedObjectUnsanitizedDoc,
prop: string,
migrations: ActiveMigrations,
convertNamespaceTypes: boolean
) {
const minMigrationVersion = propVersion(doc, prop);
const minCoreMigrationVersion = doc.coreMigrationVersion || '0.0.0';
const { transforms } = migrations[prop];
return transforms
.filter(
({ transformType }) =>
convertNamespaceTypes || !['convert', 'reference'].includes(transformType)
)
.filter(
({ transformType, version }) =>
!minMigrationVersion ||
Semver.gt(
version,
transformType === 'reference' ? minCoreMigrationVersion : minMigrationVersion
)
);
}
/**
* Updates the document's migrationVersion, ensuring that the calling transform
* has not mutated migrationVersion in an unsupported way.
*/
function updateMigrationVersion(
doc: SavedObjectUnsanitizedDoc,
migrationVersion: SavedObjectsMigrationVersion,
prop: string,
version: string
) {
assertNoDowngrades(doc, migrationVersion, prop, version);
return {
...(doc.migrationVersion || migrationVersion),
[prop]: maxVersion(propVersion(doc, prop), version) ?? '0.0.0',
};
}
/**
* Transforms that remove or downgrade migrationVersion properties are not allowed,
* as this could get us into an infinite loop. So, we explicitly check for that here.
*/
function assertNoDowngrades(
doc: SavedObjectUnsanitizedDoc,
migrationVersion: SavedObjectsMigrationVersion,
prop: string,
version: string
) {
const docVersion = doc.migrationVersion;
if (!docVersion) {
return;
}
const downgrade = Object.keys(migrationVersion).find(
(k) => !docVersion.hasOwnProperty(k) || Semver.lt(docVersion[k], migrationVersion[k])
);
if (downgrade) {
throw new Error(
`Migration "${prop} v ${version}" attempted to ` +
`downgrade "migrationVersion.${downgrade}" from ${migrationVersion[downgrade]} ` +
`to ${docVersion[downgrade]}.`
);
}
}

View file

@ -0,0 +1,204 @@
/*
* 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 Boom from '@hapi/boom';
import Semver from 'semver';
import type { SavedObjectUnsanitizedDoc } from '@kbn/core-saved-objects-server';
import { ActiveMigrations, Transform, TransformType } from './types';
import { maxVersion } from './utils';
function isGreater(a?: string, b?: string) {
return !!a && (!b || Semver.gt(a, b));
}
export class DocumentMigratorPipeline {
additionalDocs = [] as SavedObjectUnsanitizedDoc[];
constructor(
public document: SavedObjectUnsanitizedDoc,
private migrations: ActiveMigrations,
private kibanaVersion: string,
private convertNamespaceTypes: boolean
) {}
protected *getPipeline(): Generator<Transform> {
while (this.hasPendingTransforms()) {
const { type } = this.document;
for (const transform of this.getPendingTransforms()) {
yield transform;
if (type !== this.document.type) {
// In the initial implementation, all the transforms for the new type should be applied.
// And at the same time, documents with `undefined` in `typeMigrationVersion` are treated as the most recent ones.
// This is a workaround to get into the loop again and apply all the migrations for the new type.
this.document.typeMigrationVersion = '';
break;
}
}
}
}
private hasPendingTransforms() {
const { coreMigrationVersion, typeMigrationVersion, type } = this.document;
const latestVersion = this.migrations[type]?.latestVersion;
if (isGreater(latestVersion?.core, coreMigrationVersion)) {
return true;
}
if (typeMigrationVersion == null) {
return false;
}
return (
isGreater(latestVersion?.migrate, typeMigrationVersion) ||
(this.convertNamespaceTypes && isGreater(latestVersion?.convert, typeMigrationVersion)) ||
(this.convertNamespaceTypes && isGreater(latestVersion?.reference, coreMigrationVersion))
);
}
private getPendingTransforms() {
const { transforms } = this.migrations[this.document.type];
return transforms.filter((transform) => this.isPendingTransform(transform));
}
private isPendingTransform({ transformType, version }: Transform) {
const { coreMigrationVersion, typeMigrationVersion, type } = this.document;
const latestVersion = this.migrations[type]?.latestVersion;
switch (transformType) {
case TransformType.Core:
return isGreater(version, coreMigrationVersion);
case TransformType.Reference:
return (
(this.convertNamespaceTypes || isGreater(latestVersion.core, coreMigrationVersion)) &&
isGreater(version, coreMigrationVersion)
);
case TransformType.Convert:
return (
typeMigrationVersion != null &&
this.convertNamespaceTypes &&
isGreater(version, typeMigrationVersion)
);
case TransformType.Migrate:
return typeMigrationVersion != null && isGreater(version, typeMigrationVersion);
}
}
/**
* Asserts the object's core version is valid and not greater than the current Kibana version.
* Hence, the object does not belong to a more recent version of Kibana.
*/
private assertValidity() {
const { id, coreMigrationVersion } = this.document;
if (!coreMigrationVersion) {
return;
}
if (!Semver.valid(coreMigrationVersion)) {
throw Boom.badData(
`Document "${id}" has an invalid "coreMigrationVersion" [${coreMigrationVersion}]. This must be a semver value.`,
this.document
);
}
if (Semver.gt(coreMigrationVersion, this.kibanaVersion)) {
throw Boom.badData(
`Document "${id}" has a "coreMigrationVersion" which belongs to a more recent version` +
` of Kibana [${coreMigrationVersion}]. The current version is [${this.kibanaVersion}].`,
this.document
);
}
}
/**
* Verifies that the document version is not greater than the version supported by Kibana.
* If we have a document with some version and no migrations available for this type,
* the document belongs to a future version.
*/
private assertCompatibility() {
const { id, type, typeMigrationVersion: currentVersion } = this.document;
const latestVersion = maxVersion(
this.migrations[type]?.latestVersion.migrate,
this.migrations[type]?.latestVersion.convert
);
if (isGreater(currentVersion, latestVersion)) {
throw Boom.badData(
`Document "${id}" belongs to a more recent version of Kibana [${currentVersion}] when the last known version is [${latestVersion}].`,
this.document
);
}
}
/**
* Transforms that remove or downgrade `typeMigrationVersion` properties are not allowed,
* as this could get us into an infinite loop. So, we explicitly check for that here.
*/
private assertUpgrade({ transformType, version }: Transform, previousVersion?: string) {
if ([TransformType.Core, TransformType.Reference].includes(transformType)) {
return;
}
const { typeMigrationVersion: currentVersion, type } = this.document;
if (isGreater(previousVersion, currentVersion)) {
throw new Error(
`Migration "${type} v${version}" attempted to downgrade "typeMigrationVersion" from ${previousVersion} to ${currentVersion}.`
);
}
}
private bumpVersion({ transformType, version }: Transform) {
this.document = {
...this.document,
...([TransformType.Core, TransformType.Reference].includes(transformType)
? { coreMigrationVersion: maxVersion(this.document.coreMigrationVersion, version) }
: { typeMigrationVersion: maxVersion(this.document.typeMigrationVersion, version) }),
};
}
private ensureVersion({
coreMigrationVersion: currentCoreMigrationVersion,
typeMigrationVersion: currentTypeMigrationVersion,
...document
}: SavedObjectUnsanitizedDoc) {
const { type } = document;
const latestVersion = this.migrations[type]?.latestVersion;
const coreMigrationVersion =
currentCoreMigrationVersion || maxVersion(latestVersion?.core, latestVersion?.reference);
const typeMigrationVersion =
currentTypeMigrationVersion || maxVersion(latestVersion?.migrate, latestVersion?.convert);
return {
...document,
...(coreMigrationVersion ? { coreMigrationVersion } : {}),
...(typeMigrationVersion ? { typeMigrationVersion } : {}),
};
}
run(): void {
this.assertValidity();
for (const transform of this.getPipeline()) {
const { typeMigrationVersion: previousVersion } = this.document;
const { additionalDocs, transformedDoc } = transform.transform(this.document);
this.document = transformedDoc;
this.additionalDocs.push(...additionalDocs.map((document) => this.ensureVersion(document)));
this.assertUpgrade(transform, previousVersion);
this.bumpVersion(transform);
}
this.assertCompatibility();
this.document = this.ensureVersion(this.document);
}
}

View file

@ -16,8 +16,20 @@ import {
LEGACY_URL_ALIAS_TYPE, LEGACY_URL_ALIAS_TYPE,
LegacyUrlAlias, LegacyUrlAlias,
} from '@kbn/core-saved-objects-base-server-internal'; } from '@kbn/core-saved-objects-base-server-internal';
import { migrations as coreMigrationsMap } from './migrations';
import { type Transform, TransformType } from './types'; import { type Transform, TransformType } from './types';
/**
* Returns all available core transforms for all object types.
*/
export function getCoreTransforms(): Transform[] {
return Object.entries(coreMigrationsMap).map<Transform>(([version, transform]) => ({
version,
transform,
transformType: TransformType.Core,
}));
}
/** /**
* Returns all applicable conversion transforms for a given object type. * Returns all applicable conversion transforms for a given object type.
*/ */

View file

@ -0,0 +1,14 @@
/*
* 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 { TransformFn } from '../types';
import { transformMigrationVersion } from './transform_migration_version';
export const migrations = {
'8.8.0': transformMigrationVersion,
} as Record<string, TransformFn>;

View file

@ -0,0 +1,60 @@
/*
* 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 { transformMigrationVersion } from './transform_migration_version';
describe('transformMigrationVersion', () => {
it('should extract the correct version from the `migrationVersion` property', () => {
expect(
transformMigrationVersion({
id: 'a',
attributes: {},
type: 'something',
migrationVersion: {
something: '1.0.0',
previous: '2.0.0',
},
})
).toHaveProperty('transformedDoc.typeMigrationVersion', '1.0.0');
});
it('should remove the original `migrationVersion` property', () => {
expect(
transformMigrationVersion({
id: 'a',
attributes: {},
type: 'something',
migrationVersion: {
something: '1.0.0',
previous: '2.0.0',
},
})
).not.toHaveProperty('transformedDoc.migrationVersion');
});
it('should not add `typeMigrationVersion` if there is no `migrationVersion`', () => {
expect(
transformMigrationVersion({
id: 'a',
attributes: {},
type: 'something',
})
).not.toHaveProperty('transformedDoc.typeMigrationVersion');
});
it('should add empty `typeMigrationVersion` if there is no related value in `migrationVersion`', () => {
expect(
transformMigrationVersion({
id: 'a',
attributes: {},
type: 'something',
migrationVersion: {},
})
).toHaveProperty('transformedDoc.typeMigrationVersion', '');
});
});

View file

@ -0,0 +1,19 @@
/*
* 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 { TransformFn } from '../types';
export const transformMigrationVersion: TransformFn = ({ migrationVersion, ...doc }) => {
return {
transformedDoc: {
...doc,
...(migrationVersion ? { typeMigrationVersion: migrationVersion[doc.type] ?? '' } : {}),
},
additionalDocs: [],
};
};

View file

@ -21,7 +21,7 @@ export interface ActiveMigrations {
export interface TypeTransforms { export interface TypeTransforms {
/** Derived from the related transforms */ /** Derived from the related transforms */
latestVersion: Record<TransformType, string>; latestVersion: Record<TransformType, string>;
/** List of transforms registered for the type **/ /** Ordered list of transforms registered for the type **/
transforms: Transform[]; transforms: Transform[];
} }
@ -37,20 +37,29 @@ export interface Transform {
transformType: TransformType; transformType: TransformType;
} }
/**
* There are two "migrationVersion" transform types:
* * `migrate` - These transforms are defined and added by consumers using the type registry; each is applied to a single object type
* based on an object's `migrationVersion[type]` field. These are applied during index migrations and document migrations.
* * `convert` - These transforms are defined by core and added by consumers using the type registry; each is applied to a single object
* type based on an object's `migrationVersion[type]` field. These are applied during index migrations, NOT document migrations.
*
* There is one "coreMigrationVersion" transform type:
* * `reference` - These transforms are defined by core and added by consumers using the type registry; they are applied to all object
* types based on their `coreMigrationVersion` field. These are applied during index migrations, NOT document migrations.
*/
export enum TransformType { export enum TransformType {
Migrate = 'migrate', /**
* These transforms are defined by core and added by consumers using the type registry; each is applied to a single object
* type based on an object's `typeMigrationVersion` field. These are applied during index migrations, NOT document migrations.
*/
Convert = 'convert', Convert = 'convert',
/**
* These transforms are defined by core internally; they are applied to all object types based on their `coreMigrationVersion` field.
* These are applied during index migrations and before any document migrations to guarantee that all documents have the most recent schema.
*/
Core = 'core',
/**
* These transforms are defined and added by consumers using the type registry; each is applied to a single object type
* based on an object's `typeMigrationVersion` field. These are applied during index migrations and document migrations.
*/
Migrate = 'migrate',
/**
* These transforms are defined by core and added by consumers using the type registry; they are applied to all object
* types based on their `coreMigrationVersion` field. These are applied during index migrations, NOT document migrations.
*/
Reference = 'reference', Reference = 'reference',
} }

View file

@ -0,0 +1,33 @@
/*
* 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 { type Transform, TransformType } from './types';
import { transformComparator } from './utils';
describe('transformComparator', () => {
const core1 = { version: '1.0.0', transformType: TransformType.Core } as Transform;
const core5 = { version: '5.0.0', transformType: TransformType.Core } as Transform;
const core6 = { version: '6.0.0', transformType: TransformType.Core } as Transform;
const reference1 = { version: '1.0.0', transformType: TransformType.Reference } as Transform;
const reference2 = { version: '2.0.0', transformType: TransformType.Reference } as Transform;
const convert1 = { version: '1.0.0', transformType: TransformType.Convert } as Transform;
const convert5 = { version: '5.0.0', transformType: TransformType.Convert } as Transform;
const migrate1 = { version: '1.0.0', transformType: TransformType.Migrate } as Transform;
const migrate2 = { version: '2.0.0', transformType: TransformType.Migrate } as Transform;
const migrate5 = { version: '5.0.0', transformType: TransformType.Migrate } as Transform;
it.each`
transforms | expected
${[migrate1, reference1, core1, convert1]} | ${[core1, reference1, convert1, migrate1]}
${[reference1, migrate1, core1, core5, core6, convert1]} | ${[core1, core5, core6, reference1, convert1, migrate1]}
${[reference2, reference1, migrate1, core6, convert5]} | ${[core6, reference1, migrate1, reference2, convert5]}
${[migrate5, convert5, core5, migrate2]} | ${[core5, migrate2, convert5, migrate5]}
`('should sort transforms correctly', ({ transforms, expected }) => {
expect(transforms.sort(transformComparator)).toEqual(expected);
});
});

View file

@ -17,6 +17,13 @@ import { MigrationLogger } from '../core/migration_logger';
import { TransformSavedObjectDocumentError } from '../core/transform_saved_object_document_error'; import { TransformSavedObjectDocumentError } from '../core/transform_saved_object_document_error';
import { type Transform, type TransformFn, TransformType } from './types'; import { type Transform, type TransformFn, TransformType } from './types';
const TRANSFORM_PRIORITY = [
TransformType.Core,
TransformType.Reference,
TransformType.Convert,
TransformType.Migrate,
];
/** /**
* If a specific transform function fails, this tacks on a bit of information * If a specific transform function fails, this tacks on a bit of information
* about the document and transform that caused the failure. * about the document and transform that caused the failure.
@ -53,28 +60,28 @@ export function convertMigrationFunction(
} }
/** /**
* Transforms are sorted in ascending order by version. One version may contain multiple transforms; 'reference' transforms always run * Transforms are sorted in ascending order by version depending on their type:
* first, 'convert' transforms always run second, and 'migrate' transforms always run last. This is because: * - `core` transforms always run first no matter version;
* - `reference` transforms have priority in case of the same version;
* - `convert` transforms run after in case of the same version;
* - 'migrate' transforms always run last.
* This is because:
* 1. 'convert' transforms get rid of the `namespace` field, which must be present for 'reference' transforms to function correctly. * 1. 'convert' transforms get rid of the `namespace` field, which must be present for 'reference' transforms to function correctly.
* 2. 'migrate' transforms are defined by the consumer, and may change the object type or migrationVersion which resets the migration loop * 2. 'migrate' transforms are defined by the consumer, and may change the object type or `migrationVersion` which resets the migration loop
* and could cause any remaining transforms for this version to be skipped. * and could cause any remaining transforms for this version to be skipped.One version may contain multiple transforms.
*/ */
export function transformComparator(a: Transform, b: Transform) { export function transformComparator(a: Transform, b: Transform) {
const semver = Semver.compare(a.version, b.version); const aPriority = TRANSFORM_PRIORITY.indexOf(a.transformType);
if (semver !== 0) { const bPriority = TRANSFORM_PRIORITY.indexOf(b.transformType);
return semver;
} else if (a.transformType !== b.transformType) { if (
if (a.transformType === TransformType.Migrate) { aPriority !== bPriority &&
return 1; (a.transformType === TransformType.Core || b.transformType === TransformType.Core)
} else if (b.transformType === TransformType.Migrate) { ) {
return -1; return aPriority - bPriority;
} else if (a.transformType === TransformType.Convert) {
return 1;
} else if (b.transformType === TransformType.Convert) {
return -1;
}
} }
return 0;
return Semver.compare(a.version, b.version) || aPriority - bPriority;
} }
export function maxVersion(a?: string, b?: string) { export function maxVersion(a?: string, b?: string) {

View file

@ -369,41 +369,107 @@ describe('createInitialState', () => {
logger: mockLogger.get(), logger: mockLogger.get(),
}).outdatedDocumentsQuery }).outdatedDocumentsQuery
).toMatchInlineSnapshot(` ).toMatchInlineSnapshot(`
Object { Object {
"bool": Object { "bool": Object {
"should": Array [ "should": Array [
Object { Object {
"bool": Object { "bool": Object {
"must": Object { "must": Array [
Object {
"term": Object { "term": Object {
"type": "my_dashboard", "type": "my_dashboard",
}, },
}, },
"must_not": Object { Object {
"term": Object { "bool": Object {
"migrationVersion.my_dashboard": "7.10.1", "should": Array [
Object {
"bool": Object {
"must": Object {
"exists": Object {
"field": "migrationVersion",
},
},
"must_not": Object {
"term": Object {
"migrationVersion.my_dashboard": "7.10.1",
},
},
},
},
Object {
"bool": Object {
"must_not": Array [
Object {
"exists": Object {
"field": "migrationVersion",
},
},
Object {
"term": Object {
"typeMigrationVersion": "7.10.1",
},
},
],
},
},
],
}, },
}, },
}, ],
}, },
Object { },
"bool": Object { Object {
"must": Object { "bool": Object {
"must": Array [
Object {
"term": Object { "term": Object {
"type": "my_viz", "type": "my_viz",
}, },
}, },
"must_not": Object { Object {
"term": Object { "bool": Object {
"migrationVersion.my_viz": "8.0.0", "should": Array [
Object {
"bool": Object {
"must": Object {
"exists": Object {
"field": "migrationVersion",
},
},
"must_not": Object {
"term": Object {
"migrationVersion.my_viz": "8.0.0",
},
},
},
},
Object {
"bool": Object {
"must_not": Array [
Object {
"exists": Object {
"field": "migrationVersion",
},
},
Object {
"term": Object {
"typeMigrationVersion": "8.0.0",
},
},
],
},
},
],
}, },
}, },
}, ],
}, },
], },
}, ],
} },
`); }
`);
}); });
it('initializes the `discardUnknownObjects` flag to false if the flag is not provided in the config', () => { it('initializes the `discardUnknownObjects` flag to false if the flag is not provided in the config', () => {

View file

@ -7,6 +7,7 @@
*/ */
import * as Option from 'fp-ts/Option'; import * as Option from 'fp-ts/Option';
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import type { DocLinksServiceStart } from '@kbn/core-doc-links-server'; import type { DocLinksServiceStart } from '@kbn/core-doc-links-server';
import type { Logger } from '@kbn/logging'; import type { Logger } from '@kbn/logging';
import type { SavedObjectsMigrationVersion } from '@kbn/core-saved-objects-common'; import type { SavedObjectsMigrationVersion } from '@kbn/core-saved-objects-common';
@ -44,12 +45,33 @@ export const createInitialState = ({
docLinks: DocLinksServiceStart; docLinks: DocLinksServiceStart;
logger: Logger; logger: Logger;
}): InitState => { }): InitState => {
const outdatedDocumentsQuery = { const outdatedDocumentsQuery: QueryDslQueryContainer = {
bool: { bool: {
should: Object.entries(migrationVersionPerType).map(([type, latestVersion]) => ({ should: Object.entries(migrationVersionPerType).map(([type, latestVersion]) => ({
bool: { bool: {
must: { term: { type } }, must: [
must_not: { term: { [`migrationVersion.${type}`]: latestVersion } }, { term: { type } },
{
bool: {
should: [
{
bool: {
must: { exists: { field: 'migrationVersion' } },
must_not: { term: { [`migrationVersion.${type}`]: latestVersion } },
},
},
{
bool: {
must_not: [
{ exists: { field: 'migrationVersion' } },
{ term: { typeMigrationVersion: latestVersion } },
],
},
},
],
},
},
],
}, },
})), })),
}, },

View file

@ -43,6 +43,7 @@ export const registerBulkCreateRoute = (
version: schema.maybe(schema.string()), version: schema.maybe(schema.string()),
migrationVersion: schema.maybe(schema.recordOf(schema.string(), schema.string())), migrationVersion: schema.maybe(schema.recordOf(schema.string(), schema.string())),
coreMigrationVersion: schema.maybe(schema.string()), coreMigrationVersion: schema.maybe(schema.string()),
typeMigrationVersion: schema.maybe(schema.string()),
references: schema.maybe( references: schema.maybe(
schema.arrayOf( schema.arrayOf(
schema.object({ schema.object({

View file

@ -43,6 +43,7 @@ export const registerCreateRoute = (
attributes: schema.recordOf(schema.string(), schema.any()), attributes: schema.recordOf(schema.string(), schema.any()),
migrationVersion: schema.maybe(schema.recordOf(schema.string(), schema.string())), migrationVersion: schema.maybe(schema.recordOf(schema.string(), schema.string())),
coreMigrationVersion: schema.maybe(schema.string()), coreMigrationVersion: schema.maybe(schema.string()),
typeMigrationVersion: schema.maybe(schema.string()),
references: schema.maybe( references: schema.maybe(
schema.arrayOf( schema.arrayOf(
schema.object({ schema.object({
@ -65,8 +66,14 @@ export const registerCreateRoute = (
}); });
const { type, id } = req.params; const { type, id } = req.params;
const { overwrite } = req.query; const { overwrite } = req.query;
const { attributes, migrationVersion, coreMigrationVersion, references, initialNamespaces } = const {
req.body; attributes,
migrationVersion,
coreMigrationVersion,
typeMigrationVersion,
references,
initialNamespaces,
} = req.body;
const usageStatsClient = coreUsageData.getClient(); const usageStatsClient = coreUsageData.getClient();
usageStatsClient.incrementSavedObjectsCreate({ request: req }).catch(() => {}); usageStatsClient.incrementSavedObjectsCreate({ request: req }).catch(() => {});
@ -80,6 +87,7 @@ export const registerCreateRoute = (
overwrite, overwrite,
migrationVersion, migrationVersion,
coreMigrationVersion, coreMigrationVersion,
typeMigrationVersion,
references, references,
initialNamespaces, initialNamespaces,
}; };

View file

@ -41,14 +41,14 @@ describe('importDashboards(req)', () => {
type: 'dashboard', type: 'dashboard',
attributes: { panelJSON: '{}' }, attributes: { panelJSON: '{}' },
references: [], references: [],
migrationVersion: {}, typeMigrationVersion: '',
}, },
{ {
id: 'panel-01', id: 'panel-01',
type: 'visualization', type: 'visualization',
attributes: { visState: '{}' }, attributes: { visState: '{}' },
references: [], references: [],
migrationVersion: {}, typeMigrationVersion: '',
}, },
], ],
{ overwrite: false } { overwrite: false }
@ -78,7 +78,7 @@ describe('importDashboards(req)', () => {
type: 'dashboard', type: 'dashboard',
attributes: { panelJSON: '{}' }, attributes: { panelJSON: '{}' },
references: [], references: [],
migrationVersion: {}, typeMigrationVersion: '',
}, },
], ],
{ overwrite: false } { overwrite: false }

View file

@ -14,15 +14,18 @@ export async function importDashboards(
objects: SavedObject[], objects: SavedObject[],
{ overwrite, exclude }: { overwrite: boolean; exclude: string[] } { overwrite, exclude }: { overwrite: boolean; exclude: string[] }
) { ) {
// The server assumes that documents with no migrationVersion are up to date. // The server assumes that documents with no `typeMigrationVersion` are up to date.
// That assumption enables Kibana and other API consumers to not have to build // That assumption enables Kibana and other API consumers to not have to determine
// up migrationVersion prior to creating new objects. But it means that imports // `typeMigrationVersion` prior to creating new objects. But it means that imports
// need to set migrationVersion to something other than undefined, so that imported // need to set `typeMigrationVersion` to something other than undefined, so that imported
// docs are not seen as automatically up-to-date. // docs are not seen as automatically up-to-date.
const docs = objects const docs = objects
.filter((item) => !exclude.includes(item.type)) .filter((item) => !exclude.includes(item.type))
// filter out any document version, if present // filter out any document version, if present
.map(({ version, ...doc }) => ({ ...doc, migrationVersion: doc.migrationVersion || {} })); .map(({ version, ...doc }) => ({
...doc,
...(!doc.migrationVersion && !doc.typeMigrationVersion ? { typeMigrationVersion: '' } : {}),
}));
const results = await savedObjectsClient.bulkCreate(docs, { overwrite }); const results = await savedObjectsClient.bulkCreate(docs, { overwrite });
return { objects: results.saved_objects }; return { objects: results.saved_objects };

View file

@ -79,6 +79,7 @@ export interface SavedObjectsRawDocSource {
namespace?: string; namespace?: string;
namespaces?: string[]; namespaces?: string[];
migrationVersion?: SavedObjectsMigrationVersion; migrationVersion?: SavedObjectsMigrationVersion;
typeMigrationVersion?: string;
updated_at?: string; updated_at?: string;
created_at?: string; created_at?: string;
references?: SavedObjectReference[]; references?: SavedObjectReference[];
@ -100,6 +101,7 @@ interface SavedObjectDoc<T = unknown> {
namespaces?: string[]; namespaces?: string[];
migrationVersion?: SavedObjectsMigrationVersion; migrationVersion?: SavedObjectsMigrationVersion;
coreMigrationVersion?: string; coreMigrationVersion?: string;
typeMigrationVersion?: string;
version?: string; version?: string;
updated_at?: string; updated_at?: string;
created_at?: string; created_at?: string;

View file

@ -118,7 +118,7 @@ describe('migration v2', () => {
await root.preboot(); await root.preboot();
await root.setup(); await root.setup();
await expect(root.start()).rejects.toMatchInlineSnapshot( await expect(root.start()).rejects.toMatchInlineSnapshot(
`[Error: Unable to complete saved object migrations for the [.kibana] index: The document with _id "canvas-workpad-template:workpad-template-061d7868-2b4e-4dc8-8bf7-3772b52926e5" is 1715272 bytes which exceeds the configured maximum batch size of 1015275 bytes. To proceed, please increase the 'migrations.maxBatchSizeBytes' Kibana configuration option and ensure that the Elasticsearch 'http.max_content_length' configuration option is set to an equal or larger value.]` `[Error: Unable to complete saved object migrations for the [.kibana] index: The document with _id "canvas-workpad-template:workpad-template-061d7868-2b4e-4dc8-8bf7-3772b52926e5" is 1715248 bytes which exceeds the configured maximum batch size of 1015275 bytes. To proceed, please increase the 'migrations.maxBatchSizeBytes' Kibana configuration option and ensure that the Elasticsearch 'http.max_content_length' configuration option is set to an equal or larger value.]`
); );
await retryAsync( await retryAsync(
@ -131,7 +131,7 @@ describe('migration v2', () => {
expect( expect(
records.find((rec) => records.find((rec) =>
rec.message.startsWith( rec.message.startsWith(
`Unable to complete saved object migrations for the [.kibana] index: The document with _id "canvas-workpad-template:workpad-template-061d7868-2b4e-4dc8-8bf7-3772b52926e5" is 1715272 bytes which exceeds the configured maximum batch size of 1015275 bytes. To proceed, please increase the 'migrations.maxBatchSizeBytes' Kibana configuration option and ensure that the Elasticsearch 'http.max_content_length' configuration option is set to an equal or larger value.` `Unable to complete saved object migrations for the [.kibana] index: The document with _id "canvas-workpad-template:workpad-template-061d7868-2b4e-4dc8-8bf7-3772b52926e5" is 1715248 bytes which exceeds the configured maximum batch size of 1015275 bytes. To proceed, please increase the 'migrations.maxBatchSizeBytes' Kibana configuration option and ensure that the Elasticsearch 'http.max_content_length' configuration option is set to an equal or larger value.`
) )
) )
).toBeDefined(); ).toBeDefined();

View file

@ -126,7 +126,7 @@ describe('migration v2 with corrupt saved object documents', () => {
}, },
{ {
mode: 'contain', mode: 'contain',
value: 'at tryTransformDoc', value: 'at transform',
}, },
{ {
mode: 'equal', mode: 'equal',
@ -146,7 +146,7 @@ describe('migration v2 with corrupt saved object documents', () => {
}, },
{ {
mode: 'contain', mode: 'contain',
value: 'at tryTransformDoc', value: 'at transform',
}, },
{ {
mode: 'equal', mode: 'equal',

View file

@ -196,7 +196,7 @@ describe('migration v2', () => {
migratedDocs.forEach((doc, i) => { migratedDocs.forEach((doc, i) => {
expect(doc.id).toBe(`foo:${i}`); expect(doc.id).toBe(`foo:${i}`);
expect(doc.foo.status).toBe(`migrated`); expect(doc.foo.status).toBe(`migrated`);
expect(doc.migrationVersion.foo).toBe('7.14.0'); expect(doc.typeMigrationVersion).toBe('7.14.0');
}); });
}); });
@ -215,7 +215,7 @@ describe('migration v2', () => {
migratedDocs.forEach((doc, i) => { migratedDocs.forEach((doc, i) => {
expect(doc.id).toBe(`foo:${i}`); expect(doc.id).toBe(`foo:${i}`);
expect(doc.foo.status).toBe(`migrated`); expect(doc.foo.status).toBe(`migrated`);
expect(doc.migrationVersion.foo).toBe('7.14.0'); expect(doc.typeMigrationVersion).toBe('7.14.0');
}); });
}); });
@ -234,7 +234,7 @@ describe('migration v2', () => {
migratedDocs.forEach((doc, i) => { migratedDocs.forEach((doc, i) => {
expect(doc.id).toBe(`foo:${i}`); expect(doc.id).toBe(`foo:${i}`);
expect(doc.foo.status).toBe(`migrated`); expect(doc.foo.status).toBe(`migrated`);
expect(doc.migrationVersion.foo).toBe('7.14.0'); expect(doc.typeMigrationVersion).toBe('7.14.0');
}); });
}); });
@ -253,7 +253,7 @@ describe('migration v2', () => {
migratedDocs.forEach((doc, i) => { migratedDocs.forEach((doc, i) => {
expect(doc.id).toBe(`foo:${i}`); expect(doc.id).toBe(`foo:${i}`);
expect(doc.foo.status).toBe(`migrated`); expect(doc.foo.status).toBe(`migrated`);
expect(doc.migrationVersion.foo).toBe('7.14.0'); expect(doc.typeMigrationVersion).toBe('7.14.0');
}); });
}); });
}); });

View file

@ -94,8 +94,8 @@ describe('migration v2', () => {
expect(migratedDocs.length).toBe(1); expect(migratedDocs.length).toBe(1);
const [doc] = migratedDocs; const [doc] = migratedDocs;
expect(doc._source.migrationVersion.foo).toBe('7.14.0'); expect(doc._source.coreMigrationVersion).toBe('8.8.0');
expect(doc._source.coreMigrationVersion).toBe('8.0.0'); expect(doc._source.typeMigrationVersion).toBe('7.14.0');
}); });
}); });

View file

@ -130,7 +130,7 @@ describe('migrating from 7.3.0-xpack which used v1 migrations', () => {
const migrationsMap = typeof migrations === 'function' ? migrations() : migrations; const migrationsMap = typeof migrations === 'function' ? migrations() : migrations;
const migrationsKeys = migrationsMap ? Object.keys(migrationsMap) : []; const migrationsKeys = migrationsMap ? Object.keys(migrationsMap) : [];
if (convertToMultiNamespaceTypeVersion) { if (convertToMultiNamespaceTypeVersion) {
// Setting this option registers a conversion migration that is reflected in the object's `migrationVersions` field // Setting this option registers a conversion migration that is reflected in the object's `typeMigrationVersions` field
migrationsKeys.push(convertToMultiNamespaceTypeVersion); migrationsKeys.push(convertToMultiNamespaceTypeVersion);
} }
const highestVersion = migrationsKeys.sort(Semver.compare).reverse()[0]; const highestVersion = migrationsKeys.sort(Semver.compare).reverse()[0];
@ -150,9 +150,8 @@ describe('migrating from 7.3.0-xpack which used v1 migrations', () => {
doc: SavedObjectsRawDoc, doc: SavedObjectsRawDoc,
expectedVersions: Record<string, string | undefined> expectedVersions: Record<string, string | undefined>
) => { ) => {
const migrationVersions = doc._source.migrationVersion;
const type = doc._source.type; const type = doc._source.type;
expect(migrationVersions ? migrationVersions[type] : undefined).toEqual(expectedVersions[type]); expect(doc._source.typeMigrationVersion).toEqual(expectedVersions[type]);
}; };
const stopServers = async () => { const stopServers = async () => {

View file

@ -191,7 +191,7 @@ describe('migration v2', () => {
migratedFooDocs.forEach((doc, i) => { migratedFooDocs.forEach((doc, i) => {
expect(doc.id).toBe(`foo:${i}`); expect(doc.id).toBe(`foo:${i}`);
expect(doc.foo.status).toBe(`migrated_${i}`); expect(doc.foo.status).toBe(`migrated_${i}`);
expect(doc.migrationVersion.foo).toBe('7.14.0'); expect(doc.typeMigrationVersion).toBe('7.14.0');
}); });
const migratedBarDocs = await fetchDocs(esClient, migratedIndexAlias, 'bar'); const migratedBarDocs = await fetchDocs(esClient, migratedIndexAlias, 'bar');
@ -199,7 +199,7 @@ describe('migration v2', () => {
migratedBarDocs.forEach((doc, i) => { migratedBarDocs.forEach((doc, i) => {
expect(doc.id).toBe(`bar:${i}`); expect(doc.id).toBe(`bar:${i}`);
expect(doc.bar.status).toBe(`migrated_${i}`); expect(doc.bar.status).toBe(`migrated_${i}`);
expect(doc.migrationVersion.bar).toBe('7.14.0'); expect(doc.typeMigrationVersion).toBe('7.14.0');
}); });
}); });
}); });

View file

@ -187,8 +187,8 @@ describe('migration v2', () => {
foo: { name: 'Foo 1 default' }, foo: { name: 'Foo 1 default' },
references: [], references: [],
namespaces: ['default'], namespaces: ['default'],
migrationVersion: { foo: '8.0.0' }, coreMigrationVersion: expect.any(String),
coreMigrationVersion: '8.0.0', typeMigrationVersion: '8.0.0',
}, },
{ {
id: `foo:${newFooId}`, id: `foo:${newFooId}`,
@ -197,8 +197,8 @@ describe('migration v2', () => {
references: [], references: [],
namespaces: ['spacex'], namespaces: ['spacex'],
originId: '1', originId: '1',
migrationVersion: { foo: '8.0.0' }, coreMigrationVersion: expect.any(String),
coreMigrationVersion: '8.0.0', typeMigrationVersion: '8.0.0',
}, },
{ {
// new object for spacex:foo:1 // new object for spacex:foo:1
@ -211,9 +211,9 @@ describe('migration v2', () => {
targetType: 'foo', targetType: 'foo',
purpose: 'savedObjectConversion', purpose: 'savedObjectConversion',
}, },
migrationVersion: { 'legacy-url-alias': '8.2.0' },
references: [], references: [],
coreMigrationVersion: '8.0.0', coreMigrationVersion: expect.any(String),
typeMigrationVersion: '8.2.0',
}, },
{ {
id: 'bar:1', id: 'bar:1',
@ -221,8 +221,8 @@ describe('migration v2', () => {
bar: { nomnom: 1 }, bar: { nomnom: 1 },
references: [{ type: 'foo', id: '1', name: 'Foo 1 default' }], references: [{ type: 'foo', id: '1', name: 'Foo 1 default' }],
namespaces: ['default'], namespaces: ['default'],
migrationVersion: { bar: '8.0.0' }, coreMigrationVersion: expect.any(String),
coreMigrationVersion: '8.0.0', typeMigrationVersion: '8.0.0',
}, },
{ {
id: `bar:${newBarId}`, id: `bar:${newBarId}`,
@ -231,8 +231,8 @@ describe('migration v2', () => {
references: [{ type: 'foo', id: newFooId, name: 'Foo 1 spacex' }], references: [{ type: 'foo', id: newFooId, name: 'Foo 1 spacex' }],
namespaces: ['spacex'], namespaces: ['spacex'],
originId: '1', originId: '1',
migrationVersion: { bar: '8.0.0' }, coreMigrationVersion: expect.any(String),
coreMigrationVersion: '8.0.0', typeMigrationVersion: '8.0.0',
}, },
{ {
// new object for spacex:bar:1 // new object for spacex:bar:1
@ -245,9 +245,9 @@ describe('migration v2', () => {
targetType: 'bar', targetType: 'bar',
purpose: 'savedObjectConversion', purpose: 'savedObjectConversion',
}, },
migrationVersion: { 'legacy-url-alias': '8.2.0' },
references: [], references: [],
coreMigrationVersion: '8.0.0', coreMigrationVersion: expect.any(String),
typeMigrationVersion: '8.2.0',
}, },
].sort(sortByTypeAndId) ].sort(sortByTypeAndId)
); );

View file

@ -147,7 +147,7 @@ describe(`POST ${URL}`, () => {
}); });
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith(
[expect.objectContaining({ migrationVersion: {} })], [expect.objectContaining({ typeMigrationVersion: '' })],
expect.any(Object) // options expect.any(Object) // options
); );
}); });

View file

@ -161,7 +161,7 @@ describe(`POST ${URL}`, () => {
}); });
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith(
[expect.objectContaining({ migrationVersion: {} })], [expect.objectContaining({ typeMigrationVersion: '' })],
expect.any(Object) // options expect.any(Object) // options
); );
}); });
@ -199,7 +199,7 @@ describe(`POST ${URL}`, () => {
}); });
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith(
[{ type, id, attributes, migrationVersion: {} }], [{ type, id, attributes, typeMigrationVersion: '' }],
expect.objectContaining({ overwrite: undefined }) expect.objectContaining({ overwrite: undefined })
); );
}); });
@ -238,7 +238,7 @@ describe(`POST ${URL}`, () => {
}); });
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith(
[{ type, id, attributes, migrationVersion: {} }], [{ type, id, attributes, typeMigrationVersion: '' }],
expect.objectContaining({ overwrite: true }) expect.objectContaining({ overwrite: true })
); );
}); });
@ -282,7 +282,7 @@ describe(`POST ${URL}`, () => {
}); });
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith(
[{ type, id, attributes, references, migrationVersion: {} }], [{ type, id, attributes, references, typeMigrationVersion: '' }],
expect.objectContaining({ overwrite: undefined }) expect.objectContaining({ overwrite: undefined })
); );
expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1); expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1);
@ -331,7 +331,7 @@ describe(`POST ${URL}`, () => {
}); });
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith(
[{ type, id, attributes, references, migrationVersion: {} }], [{ type, id, attributes, references, typeMigrationVersion: '' }],
expect.objectContaining({ overwrite: undefined }) expect.objectContaining({ overwrite: undefined })
); );
expect(savedObjectsClient.bulkGet).not.toHaveBeenCalled(); expect(savedObjectsClient.bulkGet).not.toHaveBeenCalled();

View file

@ -25,11 +25,9 @@ export const getSavedObjects = (): SavedObject[] => [
name: 'Kibana Sample Data eCommerce', name: 'Kibana Sample Data eCommerce',
typeMeta: '{}', typeMeta: '{}',
}, },
coreMigrationVersion: '8.0.0',
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
migrationVersion: { coreMigrationVersion: '8.8.0',
'index-pattern': '7.11.0', typeMigrationVersion: '7.11.0',
},
references: [], references: [],
type: 'index-pattern', type: 'index-pattern',
updated_at: '2021-08-05T12:23:57.577Z', updated_at: '2021-08-05T12:23:57.577Z',
@ -49,11 +47,9 @@ export const getSavedObjects = (): SavedObject[] => [
visState: visState:
'{"title":"[eCommerce] Promotion Tracking","type":"metrics","aggs":[],"params":{"time_range_mode":"entire_time_range","id":"61ca57f0-469d-11e7-af02-69e470af7417","type":"timeseries","series":[{"id":"ea20ae70-b88d-11e8-a451-f37365e9f268","color":"rgba(211,96,134,1)","split_mode":"everything","metrics":[{"id":"ea20ae71-b88d-11e8-a451-f37365e9f268","type":"sum","field":"taxful_total_price"}],"separate_axis":0,"axis_position":"right","formatter":"number","chart_type":"line","line_width":"2","point_size":"5","fill":"0","stacked":"none","filter":{"query":"products.product_name:*trouser*","language":"lucene"},"label":"Revenue Trousers","value_template":"${{value}}","split_color_mode":"gradient"},{"id":"062d77b0-b88e-11e8-a451-f37365e9f268","color":"rgba(84,179,153,1)","split_mode":"everything","metrics":[{"id":"062d77b1-b88e-11e8-a451-f37365e9f268","type":"sum","field":"taxful_total_price"}],"separate_axis":0,"axis_position":"right","formatter":"number","chart_type":"line","line_width":"2","point_size":"05","fill":"0","stacked":"none","filter":{"query":"products.product_name:*watch*","language":"lucene"},"label":"Revenue Watches","value_template":"${{value}}","split_color_mode":"gradient"},{"id":"61ca57f1-469d-11e7-af02-69e470af7417","color":"rgba(96,146,192,1)","split_mode":"everything","metrics":[{"id":"61ca57f2-469d-11e7-af02-69e470af7417","type":"sum","field":"taxful_total_price"}],"separate_axis":0,"axis_position":"right","formatter":"number","chart_type":"line","line_width":"2","point_size":"5","fill":"0","stacked":"none","filter":{"query":"products.product_name:*bag*","language":"lucene"},"label":"Revenue Bags","value_template":"${{value}}","split_color_mode":"gradient"},{"id":"faa2c170-b88d-11e8-a451-f37365e9f268","color":"rgba(202,142,174,1)","split_mode":"everything","metrics":[{"id":"faa2c171-b88d-11e8-a451-f37365e9f268","type":"sum","field":"taxful_total_price"}],"separate_axis":0,"axis_position":"right","formatter":"number","chart_type":"line","line_width":"2","point_size":"5","fill":"0","stacked":"none","filter":{"query":"products.product_name:*cocktail dress*","language":"lucene"},"label":"Revenue Cocktail Dresses","value_template":"${{value}}","split_color_mode":"gradient"}],"time_field":"order_date","interval":"12h","use_kibana_indexes":true,"axis_position":"left","axis_formatter":"number","axis_scale":"normal","show_legend":1,"show_grid":1,"legend_position":"bottom","annotations":[{"fields":"taxful_total_price","template":"Ring the bell! ${{taxful_total_price}}","query_string":{"query":"taxful_total_price:>250","language":"lucene"},"id":"c8c30be0-b88f-11e8-a451-f37365e9f268","color":"rgba(25,77,51,1)","time_field":"order_date","icon":"fa-bell","ignore_global_filters":1,"ignore_panel_filters":1,"index_pattern_ref_name":"metrics_1_index_pattern"}],"tooltip_mode":"show_all","drop_last_bucket":0,"isModelInvalid":false,"index_pattern_ref_name":"metrics_0_index_pattern"}}', '{"title":"[eCommerce] Promotion Tracking","type":"metrics","aggs":[],"params":{"time_range_mode":"entire_time_range","id":"61ca57f0-469d-11e7-af02-69e470af7417","type":"timeseries","series":[{"id":"ea20ae70-b88d-11e8-a451-f37365e9f268","color":"rgba(211,96,134,1)","split_mode":"everything","metrics":[{"id":"ea20ae71-b88d-11e8-a451-f37365e9f268","type":"sum","field":"taxful_total_price"}],"separate_axis":0,"axis_position":"right","formatter":"number","chart_type":"line","line_width":"2","point_size":"5","fill":"0","stacked":"none","filter":{"query":"products.product_name:*trouser*","language":"lucene"},"label":"Revenue Trousers","value_template":"${{value}}","split_color_mode":"gradient"},{"id":"062d77b0-b88e-11e8-a451-f37365e9f268","color":"rgba(84,179,153,1)","split_mode":"everything","metrics":[{"id":"062d77b1-b88e-11e8-a451-f37365e9f268","type":"sum","field":"taxful_total_price"}],"separate_axis":0,"axis_position":"right","formatter":"number","chart_type":"line","line_width":"2","point_size":"05","fill":"0","stacked":"none","filter":{"query":"products.product_name:*watch*","language":"lucene"},"label":"Revenue Watches","value_template":"${{value}}","split_color_mode":"gradient"},{"id":"61ca57f1-469d-11e7-af02-69e470af7417","color":"rgba(96,146,192,1)","split_mode":"everything","metrics":[{"id":"61ca57f2-469d-11e7-af02-69e470af7417","type":"sum","field":"taxful_total_price"}],"separate_axis":0,"axis_position":"right","formatter":"number","chart_type":"line","line_width":"2","point_size":"5","fill":"0","stacked":"none","filter":{"query":"products.product_name:*bag*","language":"lucene"},"label":"Revenue Bags","value_template":"${{value}}","split_color_mode":"gradient"},{"id":"faa2c170-b88d-11e8-a451-f37365e9f268","color":"rgba(202,142,174,1)","split_mode":"everything","metrics":[{"id":"faa2c171-b88d-11e8-a451-f37365e9f268","type":"sum","field":"taxful_total_price"}],"separate_axis":0,"axis_position":"right","formatter":"number","chart_type":"line","line_width":"2","point_size":"5","fill":"0","stacked":"none","filter":{"query":"products.product_name:*cocktail dress*","language":"lucene"},"label":"Revenue Cocktail Dresses","value_template":"${{value}}","split_color_mode":"gradient"}],"time_field":"order_date","interval":"12h","use_kibana_indexes":true,"axis_position":"left","axis_formatter":"number","axis_scale":"normal","show_legend":1,"show_grid":1,"legend_position":"bottom","annotations":[{"fields":"taxful_total_price","template":"Ring the bell! ${{taxful_total_price}}","query_string":{"query":"taxful_total_price:>250","language":"lucene"},"id":"c8c30be0-b88f-11e8-a451-f37365e9f268","color":"rgba(25,77,51,1)","time_field":"order_date","icon":"fa-bell","ignore_global_filters":1,"ignore_panel_filters":1,"index_pattern_ref_name":"metrics_1_index_pattern"}],"tooltip_mode":"show_all","drop_last_bucket":0,"isModelInvalid":false,"index_pattern_ref_name":"metrics_0_index_pattern"}}',
}, },
coreMigrationVersion: '8.0.0',
id: '45e07720-b890-11e8-a6d9-e546fe2bba5f', id: '45e07720-b890-11e8-a6d9-e546fe2bba5f',
migrationVersion: { coreMigrationVersion: '8.8.0',
visualization: '7.14.0', typeMigrationVersion: '7.14.0',
},
references: [ references: [
{ {
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
@ -84,11 +80,9 @@ export const getSavedObjects = (): SavedObject[] => [
visState: visState:
'{"title":"[eCommerce] Sold Products per Day","type":"metrics","aggs":[],"params":{"time_range_mode":"entire_time_range","id":"61ca57f0-469d-11e7-af02-69e470af7417","type":"gauge","series":[{"id":"61ca57f1-469d-11e7-af02-69e470af7417","color":"#68BC00","split_mode":"everything","metrics":[{"id":"61ca57f2-469d-11e7-af02-69e470af7417","type":"count"},{"id":"fd1e1b90-e4e3-11eb-8234-cb7bfd534fce","type":"math","variables":[{"id":"00374270-e4e4-11eb-8234-cb7bfd534fce","name":"c","field":"61ca57f2-469d-11e7-af02-69e470af7417"}],"script":"params.c / (params._interval / 1000 / 60 / 60 / 24)"}],"separate_axis":0,"axis_position":"right","formatter":"0.0","chart_type":"line","line_width":1,"point_size":1,"fill":0.5,"stacked":"none","label":"Trxns / day","split_color_mode":"gradient","value_template":""}],"time_field":"order_date","interval":"1d","axis_position":"left","axis_formatter":"number","axis_scale":"normal","show_legend":1,"show_grid":1,"gauge_color_rules":[{"value":150,"id":"6da070c0-b891-11e8-b645-195edeb9de84","gauge":"rgba(104,188,0,1)","operator":"gte"},{"value":150,"id":"9b0cdbc0-b891-11e8-b645-195edeb9de84","gauge":"rgba(244,78,59,1)","operator":"lt"}],"gauge_width":"15","gauge_inner_width":"10","gauge_style":"half","filter":"","gauge_max":"300","use_kibana_indexes":true,"hide_last_value_indicator":true,"tooltip_mode":"show_all","drop_last_bucket":0,"isModelInvalid":false,"index_pattern_ref_name":"metrics_0_index_pattern"}}', '{"title":"[eCommerce] Sold Products per Day","type":"metrics","aggs":[],"params":{"time_range_mode":"entire_time_range","id":"61ca57f0-469d-11e7-af02-69e470af7417","type":"gauge","series":[{"id":"61ca57f1-469d-11e7-af02-69e470af7417","color":"#68BC00","split_mode":"everything","metrics":[{"id":"61ca57f2-469d-11e7-af02-69e470af7417","type":"count"},{"id":"fd1e1b90-e4e3-11eb-8234-cb7bfd534fce","type":"math","variables":[{"id":"00374270-e4e4-11eb-8234-cb7bfd534fce","name":"c","field":"61ca57f2-469d-11e7-af02-69e470af7417"}],"script":"params.c / (params._interval / 1000 / 60 / 60 / 24)"}],"separate_axis":0,"axis_position":"right","formatter":"0.0","chart_type":"line","line_width":1,"point_size":1,"fill":0.5,"stacked":"none","label":"Trxns / day","split_color_mode":"gradient","value_template":""}],"time_field":"order_date","interval":"1d","axis_position":"left","axis_formatter":"number","axis_scale":"normal","show_legend":1,"show_grid":1,"gauge_color_rules":[{"value":150,"id":"6da070c0-b891-11e8-b645-195edeb9de84","gauge":"rgba(104,188,0,1)","operator":"gte"},{"value":150,"id":"9b0cdbc0-b891-11e8-b645-195edeb9de84","gauge":"rgba(244,78,59,1)","operator":"lt"}],"gauge_width":"15","gauge_inner_width":"10","gauge_style":"half","filter":"","gauge_max":"300","use_kibana_indexes":true,"hide_last_value_indicator":true,"tooltip_mode":"show_all","drop_last_bucket":0,"isModelInvalid":false,"index_pattern_ref_name":"metrics_0_index_pattern"}}',
}, },
coreMigrationVersion: '8.0.0',
id: 'b80e6540-b891-11e8-a6d9-e546fe2bba5f', id: 'b80e6540-b891-11e8-a6d9-e546fe2bba5f',
migrationVersion: { coreMigrationVersion: '8.8.0',
visualization: '7.14.0', typeMigrationVersion: '7.14.0',
},
references: [ references: [
{ {
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
@ -122,11 +116,9 @@ export const getSavedObjects = (): SavedObject[] => [
}), }),
version: 1, version: 1,
}, },
coreMigrationVersion: '8.0.0',
id: '3ba638e0-b894-11e8-a6d9-e546fe2bba5f', id: '3ba638e0-b894-11e8-a6d9-e546fe2bba5f',
migrationVersion: { coreMigrationVersion: '8.8.0',
search: '7.9.3', typeMigrationVersion: '7.9.3',
},
references: [ references: [
{ {
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
@ -143,8 +135,8 @@ export const getSavedObjects = (): SavedObject[] => [
type: 'visualization', type: 'visualization',
updated_at: '2021-10-28T15:07:24.077Z', updated_at: '2021-10-28T15:07:24.077Z',
version: '1', version: '1',
coreMigrationVersion: '8.0.0', coreMigrationVersion: '8.8.0',
migrationVersion: { visualization: '8.0.0' }, typeMigrationVersion: '8.0.0',
attributes: { attributes: {
title: i18n.translate('home.sampleData.ecommerceSpec.salesCountMapTitle', { title: i18n.translate('home.sampleData.ecommerceSpec.salesCountMapTitle', {
defaultMessage: '[eCommerce] Sales Count Map', defaultMessage: '[eCommerce] Sales Count Map',
@ -179,11 +171,9 @@ export const getSavedObjects = (): SavedObject[] => [
visState: visState:
'{"title":"[eCommerce] Markdown","type":"markdown","params":{"fontSize":12,"openLinksInNewTab":false,"markdown":"## Sample eCommerce Data\\nThis dashboard contains sample data for you to play with. You can view it, search it, and interact with the visualizations. For more information about Kibana, check our [docs](https://www.elastic.co/guide/en/kibana/current/index.html)."},"aggs":[]}', '{"title":"[eCommerce] Markdown","type":"markdown","params":{"fontSize":12,"openLinksInNewTab":false,"markdown":"## Sample eCommerce Data\\nThis dashboard contains sample data for you to play with. You can view it, search it, and interact with the visualizations. For more information about Kibana, check our [docs](https://www.elastic.co/guide/en/kibana/current/index.html)."},"aggs":[]}',
}, },
coreMigrationVersion: '8.0.0',
id: 'c00d1f90-f5ea-11eb-a78e-83aac3c38a60', id: 'c00d1f90-f5ea-11eb-a78e-83aac3c38a60',
migrationVersion: { coreMigrationVersion: '8.8.0',
visualization: '7.14.0', typeMigrationVersion: '7.14.0',
},
references: [], references: [],
type: 'visualization', type: 'visualization',
updated_at: '2021-08-05T12:43:35.817Z', updated_at: '2021-08-05T12:43:35.817Z',
@ -328,11 +318,9 @@ export const getSavedObjects = (): SavedObject[] => [
title: '% of target revenue ($10k)', title: '% of target revenue ($10k)',
visualizationType: 'lnsXY', visualizationType: 'lnsXY',
}, },
coreMigrationVersion: '8.0.0',
id: 'c762b7a0-f5ea-11eb-a78e-83aac3c38a60', id: 'c762b7a0-f5ea-11eb-a78e-83aac3c38a60',
migrationVersion: { coreMigrationVersion: '8.8.0',
lens: '7.14.0', typeMigrationVersion: '7.14.0',
},
references: [ references: [
{ {
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
@ -386,11 +374,9 @@ export const getSavedObjects = (): SavedObject[] => [
title: 'Sum of revenue', title: 'Sum of revenue',
visualizationType: 'lnsMetric', visualizationType: 'lnsMetric',
}, },
coreMigrationVersion: '8.0.0',
id: 'ce02e260-f5ea-11eb-a78e-83aac3c38a60', id: 'ce02e260-f5ea-11eb-a78e-83aac3c38a60',
migrationVersion: { coreMigrationVersion: '8.8.0',
lens: '7.14.0', typeMigrationVersion: '7.14.0',
},
references: [ references: [
{ {
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
@ -444,11 +430,9 @@ export const getSavedObjects = (): SavedObject[] => [
title: 'Median spending', title: 'Median spending',
visualizationType: 'lnsMetric', visualizationType: 'lnsMetric',
}, },
coreMigrationVersion: '8.0.0',
id: 'd5f90030-f5ea-11eb-a78e-83aac3c38a60', id: 'd5f90030-f5ea-11eb-a78e-83aac3c38a60',
migrationVersion: { coreMigrationVersion: '8.8.0',
lens: '7.14.0', typeMigrationVersion: '7.14.0',
},
references: [ references: [
{ {
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
@ -600,11 +584,9 @@ export const getSavedObjects = (): SavedObject[] => [
title: 'Transactions per day', title: 'Transactions per day',
visualizationType: 'lnsXY', visualizationType: 'lnsXY',
}, },
coreMigrationVersion: '8.0.0',
id: 'dde978b0-f5ea-11eb-a78e-83aac3c38a60', id: 'dde978b0-f5ea-11eb-a78e-83aac3c38a60',
migrationVersion: { coreMigrationVersion: '8.8.0',
lens: '7.14.0', typeMigrationVersion: '7.14.0',
},
references: [ references: [
{ {
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
@ -666,11 +648,9 @@ export const getSavedObjects = (): SavedObject[] => [
title: 'Avg. items sold', title: 'Avg. items sold',
visualizationType: 'lnsMetric', visualizationType: 'lnsMetric',
}, },
coreMigrationVersion: '8.0.0',
id: 'e3902840-f5ea-11eb-a78e-83aac3c38a60', id: 'e3902840-f5ea-11eb-a78e-83aac3c38a60',
migrationVersion: { coreMigrationVersion: '8.8.0',
lens: '7.14.0', typeMigrationVersion: '7.14.0',
},
references: [ references: [
{ {
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
@ -793,11 +773,9 @@ export const getSavedObjects = (): SavedObject[] => [
title: 'Breakdown by category', title: 'Breakdown by category',
visualizationType: 'lnsXY', visualizationType: 'lnsXY',
}, },
coreMigrationVersion: '8.0.0',
id: 'eddf7850-f5ea-11eb-a78e-83aac3c38a60', id: 'eddf7850-f5ea-11eb-a78e-83aac3c38a60',
migrationVersion: { coreMigrationVersion: '8.8.0',
lens: '7.14.0', typeMigrationVersion: '7.14.0',
},
references: [ references: [
{ {
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
@ -993,11 +971,9 @@ export const getSavedObjects = (): SavedObject[] => [
title: 'Daily comparison', title: 'Daily comparison',
visualizationType: 'lnsDatatable', visualizationType: 'lnsDatatable',
}, },
coreMigrationVersion: '8.0.0',
id: 'ff6a21b0-f5ea-11eb-a78e-83aac3c38a60', id: 'ff6a21b0-f5ea-11eb-a78e-83aac3c38a60',
migrationVersion: { coreMigrationVersion: '8.8.0',
lens: '7.14.0', typeMigrationVersion: '7.14.0',
},
references: [ references: [
{ {
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
@ -1109,11 +1085,9 @@ export const getSavedObjects = (): SavedObject[] => [
title: 'Top products this week', title: 'Top products this week',
visualizationType: 'lnsXY', visualizationType: 'lnsXY',
}, },
coreMigrationVersion: '8.0.0',
id: '03071e90-f5eb-11eb-a78e-83aac3c38a60', id: '03071e90-f5eb-11eb-a78e-83aac3c38a60',
migrationVersion: { coreMigrationVersion: '8.8.0',
lens: '7.14.0', typeMigrationVersion: '7.14.0',
},
references: [ references: [
{ {
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
@ -1225,11 +1199,9 @@ export const getSavedObjects = (): SavedObject[] => [
title: 'Top products last week', title: 'Top products last week',
visualizationType: 'lnsXY', visualizationType: 'lnsXY',
}, },
coreMigrationVersion: '8.0.0',
id: '06379e00-f5eb-11eb-a78e-83aac3c38a60', id: '06379e00-f5eb-11eb-a78e-83aac3c38a60',
migrationVersion: { coreMigrationVersion: '8.8.0',
lens: '7.14.0', typeMigrationVersion: '7.14.0',
},
references: [ references: [
{ {
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
@ -1370,9 +1342,7 @@ export const getSavedObjects = (): SavedObject[] => [
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
}, },
], ],
migrationVersion: { coreMigrationVersion: '8.8.0',
dashboard: '8.5.0', typeMigrationVersion: '8.5.0',
},
coreMigrationVersion: '8.6.0',
}, },
]; ];

View file

@ -17,9 +17,8 @@ export const getSavedObjects = (): SavedObject[] => [
type: 'search', type: 'search',
updated_at: '2021-07-01T20:41:40.379Z', updated_at: '2021-07-01T20:41:40.379Z',
version: '1', version: '1',
migrationVersion: { coreMigrationVersion: '8.8.0',
search: '7.9.3', typeMigrationVersion: '7.9.3',
},
attributes: { attributes: {
title: i18n.translate('home.sampleData.flightsSpec.flightLogTitle', { title: i18n.translate('home.sampleData.flightsSpec.flightLogTitle', {
defaultMessage: '[Flights] Flight Log', defaultMessage: '[Flights] Flight Log',
@ -57,9 +56,8 @@ export const getSavedObjects = (): SavedObject[] => [
type: 'visualization', type: 'visualization',
updated_at: '2018-05-09T15:49:03.736Z', updated_at: '2018-05-09T15:49:03.736Z',
version: '1', version: '1',
migrationVersion: { coreMigrationVersion: '8.8.0',
visualization: '7.14.0', typeMigrationVersion: '7.14.0',
},
attributes: { attributes: {
title: i18n.translate('home.sampleData.flightsSpec.delaysAndCancellationsTitle', { title: i18n.translate('home.sampleData.flightsSpec.delaysAndCancellationsTitle', {
defaultMessage: '[Flights] Delays & Cancellations', defaultMessage: '[Flights] Delays & Cancellations',
@ -91,9 +89,8 @@ export const getSavedObjects = (): SavedObject[] => [
type: 'visualization', type: 'visualization',
updated_at: '2018-05-09T15:49:03.736Z', updated_at: '2018-05-09T15:49:03.736Z',
version: '1', version: '1',
migrationVersion: { coreMigrationVersion: '8.8.0',
visualization: '7.14.0', typeMigrationVersion: '7.14.0',
},
attributes: { attributes: {
title: i18n.translate('home.sampleData.flightsSpec.delayBucketsTitle', { title: i18n.translate('home.sampleData.flightsSpec.delayBucketsTitle', {
defaultMessage: '[Flights] Delay Buckets', defaultMessage: '[Flights] Delay Buckets',
@ -126,7 +123,6 @@ export const getSavedObjects = (): SavedObject[] => [
type: 'visualization', type: 'visualization',
updated_at: '2021-07-07T01:48:55.366Z', updated_at: '2021-07-07T01:48:55.366Z',
version: '1', version: '1',
migrationVersion: {},
attributes: { attributes: {
title: i18n.translate('home.sampleData.flightsSpec.destinationWeatherTitle', { title: i18n.translate('home.sampleData.flightsSpec.destinationWeatherTitle', {
defaultMessage: '[Flights] Destination Weather', defaultMessage: '[Flights] Destination Weather',
@ -154,7 +150,6 @@ export const getSavedObjects = (): SavedObject[] => [
type: 'visualization', type: 'visualization',
updated_at: '2021-07-07T01:36:42.568Z', updated_at: '2021-07-07T01:36:42.568Z',
version: '4', version: '4',
migrationVersion: {},
attributes: { attributes: {
title: i18n.translate('home.sampleData.flightsSpec.airportConnectionsTitle', { title: i18n.translate('home.sampleData.flightsSpec.airportConnectionsTitle', {
defaultMessage: '[Flights] Airport Connections (Hover Over Airport)', defaultMessage: '[Flights] Airport Connections (Hover Over Airport)',
@ -175,9 +170,8 @@ export const getSavedObjects = (): SavedObject[] => [
type: 'visualization', type: 'visualization',
updated_at: '2018-05-09T15:49:03.736Z', updated_at: '2018-05-09T15:49:03.736Z',
version: '1', version: '1',
migrationVersion: { coreMigrationVersion: '8.8.0',
visualization: '7.14.0', typeMigrationVersion: '7.14.0',
},
attributes: { attributes: {
title: i18n.translate('home.sampleData.flightsSpec.departuresCountMapTitle', { title: i18n.translate('home.sampleData.flightsSpec.departuresCountMapTitle', {
defaultMessage: '[Flights] Departures Count Map', defaultMessage: '[Flights] Departures Count Map',
@ -205,7 +199,6 @@ export const getSavedObjects = (): SavedObject[] => [
type: 'index-pattern', type: 'index-pattern',
updated_at: '2018-05-09T15:49:03.736Z', updated_at: '2018-05-09T15:49:03.736Z',
version: '1', version: '1',
migrationVersion: {},
attributes: { attributes: {
title: 'kibana_sample_data_flights', title: 'kibana_sample_data_flights',
name: 'Kibana Sample Data Flights', name: 'Kibana Sample Data Flights',
@ -408,9 +401,7 @@ export const getSavedObjects = (): SavedObject[] => [
id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d',
}, },
], ],
migrationVersion: { coreMigrationVersion: '8.8.0',
dashboard: '8.5.0', typeMigrationVersion: '8.5.0',
},
coreMigrationVersion: '8.6.0',
}, },
]; ];

View file

@ -16,8 +16,8 @@ export const getSavedObjects = (): SavedObject[] => [
type: 'visualization', type: 'visualization',
updated_at: '2021-10-28T15:07:36.622Z', updated_at: '2021-10-28T15:07:36.622Z',
version: '1', version: '1',
coreMigrationVersion: '8.0.0', coreMigrationVersion: '8.8.0',
migrationVersion: { visualization: '8.0.0' }, typeMigrationVersion: '8.0.0',
attributes: { attributes: {
title: i18n.translate('home.sampleData.logsSpec.visitorsMapTitle', { title: i18n.translate('home.sampleData.logsSpec.visitorsMapTitle', {
defaultMessage: '[Logs] Visitors Map', defaultMessage: '[Logs] Visitors Map',
@ -45,9 +45,8 @@ export const getSavedObjects = (): SavedObject[] => [
type: 'visualization', type: 'visualization',
updated_at: '2021-07-21T21:33:42.541Z', updated_at: '2021-07-21T21:33:42.541Z',
version: '1', version: '1',
migrationVersion: { coreMigrationVersion: '8.8.0',
visualization: '7.14.0', typeMigrationVersion: '7.14.0',
},
attributes: { attributes: {
title: i18n.translate('home.sampleData.logsSpec.heatmapTitle', { title: i18n.translate('home.sampleData.logsSpec.heatmapTitle', {
defaultMessage: '[Logs] Unique Destination Heatmap', defaultMessage: '[Logs] Unique Destination Heatmap',
@ -68,9 +67,8 @@ export const getSavedObjects = (): SavedObject[] => [
type: 'visualization', type: 'visualization',
updated_at: '2021-07-21T18:52:13.586Z', updated_at: '2021-07-21T18:52:13.586Z',
version: '2', version: '2',
migrationVersion: { coreMigrationVersion: '8.8.0',
visualization: '7.14.0', typeMigrationVersion: '7.14.0',
},
attributes: { attributes: {
title: i18n.translate('home.sampleData.logsSpec.hostVisitsBytesTableTitle', { title: i18n.translate('home.sampleData.logsSpec.hostVisitsBytesTableTitle', {
defaultMessage: '[Logs] Host, Visits and Bytes Table', defaultMessage: '[Logs] Host, Visits and Bytes Table',
@ -97,8 +95,8 @@ export const getSavedObjects = (): SavedObject[] => [
type: 'visualization', type: 'visualization',
updated_at: '2021-10-28T14:38:21.435Z', updated_at: '2021-10-28T14:38:21.435Z',
version: '2', version: '2',
coreMigrationVersion: '8.0.0', coreMigrationVersion: '8.8.0',
migrationVersion: { visualization: '8.0.0' }, typeMigrationVersion: '8.0.0',
attributes: { attributes: {
title: i18n.translate('home.sampleData.logsSpec.goalsTitle', { title: i18n.translate('home.sampleData.logsSpec.goalsTitle', {
defaultMessage: '[Logs] Goals', defaultMessage: '[Logs] Goals',
@ -127,7 +125,6 @@ export const getSavedObjects = (): SavedObject[] => [
type: 'visualization', type: 'visualization',
updated_at: '2018-08-29T13:22:17.617Z', updated_at: '2018-08-29T13:22:17.617Z',
version: '1', version: '1',
migrationVersion: {},
attributes: { attributes: {
title: i18n.translate('home.sampleData.logsSpec.sourceAndDestinationSankeyChartTitle', { title: i18n.translate('home.sampleData.logsSpec.sourceAndDestinationSankeyChartTitle', {
defaultMessage: '[Logs] Machine OS and Destination Sankey Chart', defaultMessage: '[Logs] Machine OS and Destination Sankey Chart',
@ -148,9 +145,8 @@ export const getSavedObjects = (): SavedObject[] => [
type: 'visualization', type: 'visualization',
updated_at: '2021-07-21T18:52:13.586Z', updated_at: '2021-07-21T18:52:13.586Z',
version: '2', version: '2',
migrationVersion: { coreMigrationVersion: '8.8.0',
visualization: '7.14.0', typeMigrationVersion: '7.14.0',
},
attributes: { attributes: {
title: i18n.translate('home.sampleData.logsSpec.responseCodesOverTimeTitle', { title: i18n.translate('home.sampleData.logsSpec.responseCodesOverTimeTitle', {
defaultMessage: '[Logs] Response Codes Over Time + Annotations', defaultMessage: '[Logs] Response Codes Over Time + Annotations',
@ -182,9 +178,8 @@ export const getSavedObjects = (): SavedObject[] => [
type: 'lens', type: 'lens',
updated_at: '2021-07-21T22:14:59.793Z', updated_at: '2021-07-21T22:14:59.793Z',
version: '1', version: '1',
migrationVersion: { coreMigrationVersion: '8.8.0',
lens: '7.14.0', typeMigrationVersion: '7.14.0',
},
attributes: { attributes: {
title: i18n.translate('home.sampleData.logsSpec.bytesDistributionTitle', { title: i18n.translate('home.sampleData.logsSpec.bytesDistributionTitle', {
defaultMessage: '[Logs] Bytes distribution', defaultMessage: '[Logs] Bytes distribution',
@ -367,7 +362,6 @@ export const getSavedObjects = (): SavedObject[] => [
type: 'index-pattern', type: 'index-pattern',
updated_at: '2018-08-29T13:22:17.617Z', updated_at: '2018-08-29T13:22:17.617Z',
version: '1', version: '1',
migrationVersion: {},
attributes: { attributes: {
title: 'kibana_sample_data_logs', title: 'kibana_sample_data_logs',
name: 'Kibana Sample Data Logs', name: 'Kibana Sample Data Logs',
@ -508,19 +502,16 @@ export const getSavedObjects = (): SavedObject[] => [
id: '90943e30-9a47-11e8-b64d-95841ca0b247', id: '90943e30-9a47-11e8-b64d-95841ca0b247',
}, },
], ],
migrationVersion: { coreMigrationVersion: '8.8.0',
dashboard: '8.5.0', typeMigrationVersion: '8.5.0',
},
coreMigrationVersion: '8.6.0',
}, },
{ {
id: '2f360f30-ea74-11eb-b4c6-3d2afc1cb389', id: '2f360f30-ea74-11eb-b4c6-3d2afc1cb389',
type: 'search', type: 'search',
updated_at: '2021-07-21T22:37:09.415Z', updated_at: '2021-07-21T22:37:09.415Z',
version: '1', version: '1',
migrationVersion: { coreMigrationVersion: '8.8.0',
search: '7.9.3', typeMigrationVersion: '7.9.3',
},
attributes: { attributes: {
title: i18n.translate('home.sampleData.logsSpec.discoverTitle', { title: i18n.translate('home.sampleData.logsSpec.discoverTitle', {
defaultMessage: '[Logs] Visits', defaultMessage: '[Logs] Visits',

View file

@ -70,10 +70,8 @@ export default function ({ getService }: FtrProviderContext) {
attributes: { attributes: {
title: 'A great new dashboard', title: 'A great new dashboard',
}, },
migrationVersion: { coreMigrationVersion: '8.8.0',
dashboard: resp.body.saved_objects[1].migrationVersion.dashboard, typeMigrationVersion: resp.body.saved_objects[1].typeMigrationVersion,
},
coreMigrationVersion: '8.0.0',
references: [], references: [],
namespaces: [SPACE_ID], namespaces: [SPACE_ID],
}, },

View file

@ -71,8 +71,8 @@ export default function ({ getService }: FtrProviderContext) {
kibanaSavedObjectMeta: kibanaSavedObjectMeta:
resp.body.saved_objects[0].attributes.kibanaSavedObjectMeta, resp.body.saved_objects[0].attributes.kibanaSavedObjectMeta,
}, },
migrationVersion: resp.body.saved_objects[0].migrationVersion, coreMigrationVersion: '8.8.0',
coreMigrationVersion: '7.14.0', typeMigrationVersion: resp.body.saved_objects[0].typeMigrationVersion,
namespaces: ['default'], namespaces: ['default'],
references: [ references: [
{ {
@ -102,13 +102,13 @@ export default function ({ getService }: FtrProviderContext) {
defaultIndex: '91200a00-9efd-11e7-acb3-3dab96693fab', defaultIndex: '91200a00-9efd-11e7-acb3-3dab96693fab',
}, },
namespaces: ['default'], namespaces: ['default'],
migrationVersion: resp.body.saved_objects[2].migrationVersion, coreMigrationVersion: '8.8.0',
coreMigrationVersion: '7.14.0', typeMigrationVersion: resp.body.saved_objects[2].typeMigrationVersion,
references: [], references: [],
}, },
], ],
}); });
expect(resp.body.saved_objects[0].migrationVersion).to.be.ok(); expect(resp.body.saved_objects[0].typeMigrationVersion).to.be.ok();
})); }));
}); });
} }

View file

@ -49,8 +49,8 @@ export default function ({ getService }: FtrProviderContext) {
expect(resp.body).to.eql({ expect(resp.body).to.eql({
id: resp.body.id, id: resp.body.id,
type: 'visualization', type: 'visualization',
migrationVersion: resp.body.migrationVersion, coreMigrationVersion: '8.8.0',
coreMigrationVersion: '8.0.0', typeMigrationVersion: resp.body.typeMigrationVersion,
updated_at: resp.body.updated_at, updated_at: resp.body.updated_at,
created_at: resp.body.created_at, created_at: resp.body.created_at,
version: resp.body.version, version: resp.body.version,
@ -60,7 +60,7 @@ export default function ({ getService }: FtrProviderContext) {
references: [], references: [],
namespaces: ['default'], namespaces: ['default'],
}); });
expect(resp.body.migrationVersion).to.be.ok(); expect(resp.body.typeMigrationVersion).to.be.ok();
}); });
}); });
@ -71,11 +71,11 @@ export default function ({ getService }: FtrProviderContext) {
attributes: { attributes: {
title: 'My favorite vis', title: 'My favorite vis',
}, },
coreMigrationVersion: '1.2.3', coreMigrationVersion: '8.8.0',
}) })
.expect(200) .expect(200)
.then((resp) => { .then((resp) => {
expect(resp.body.coreMigrationVersion).to.eql('1.2.3'); expect(resp.body.coreMigrationVersion).to.eql('8.8.0');
}); });
}); });
}); });

View file

@ -348,8 +348,8 @@ export default function ({ getService }: FtrProviderContext) {
version: 1, version: 1,
}, },
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
migrationVersion: objects[0].migrationVersion, coreMigrationVersion: '8.8.0',
coreMigrationVersion: '7.14.0', typeMigrationVersion: objects[0].typeMigrationVersion,
references: [ references: [
{ {
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
@ -362,7 +362,7 @@ export default function ({ getService }: FtrProviderContext) {
created_at: objects[0].created_at, created_at: objects[0].created_at,
version: objects[0].version, version: objects[0].version,
}); });
expect(objects[0].migrationVersion).to.be.ok(); expect(objects[0].typeMigrationVersion).to.be.ok();
expect(() => expect(() =>
JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON) JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON)
).not.to.throwError(); ).not.to.throwError();
@ -409,8 +409,8 @@ export default function ({ getService }: FtrProviderContext) {
version: 1, version: 1,
}, },
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
migrationVersion: objects[0].migrationVersion, coreMigrationVersion: '8.8.0',
coreMigrationVersion: '7.14.0', typeMigrationVersion: objects[0].typeMigrationVersion,
references: [ references: [
{ {
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
@ -423,7 +423,7 @@ export default function ({ getService }: FtrProviderContext) {
created_at: objects[0].created_at, created_at: objects[0].created_at,
version: objects[0].version, version: objects[0].version,
}); });
expect(objects[0].migrationVersion).to.be.ok(); expect(objects[0].typeMigrationVersion).to.be.ok();
expect(() => expect(() =>
JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON) JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON)
).not.to.throwError(); ).not.to.throwError();
@ -475,8 +475,8 @@ export default function ({ getService }: FtrProviderContext) {
version: 1, version: 1,
}, },
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
migrationVersion: objects[0].migrationVersion, coreMigrationVersion: '8.8.0',
coreMigrationVersion: '7.14.0', typeMigrationVersion: objects[0].typeMigrationVersion,
references: [ references: [
{ {
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
@ -489,7 +489,7 @@ export default function ({ getService }: FtrProviderContext) {
created_at: objects[0].updated_at, created_at: objects[0].updated_at,
version: objects[0].version, version: objects[0].version,
}); });
expect(objects[0].migrationVersion).to.be.ok(); expect(objects[0].typeMigrationVersion).to.be.ok();
expect(() => expect(() =>
JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON) JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON)
).not.to.throwError(); ).not.to.throwError();

View file

@ -48,7 +48,7 @@ export default function ({ getService }: FtrProviderContext) {
expect(resp.body.saved_objects.map((so: { id: string }) => so.id)).to.eql([ expect(resp.body.saved_objects.map((so: { id: string }) => so.id)).to.eql([
'dd7caf20-9efd-11e7-acb3-3dab96693fab', 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
]); ]);
expect(resp.body.saved_objects[0].migrationVersion).to.be.ok(); expect(resp.body.saved_objects[0].typeMigrationVersion).to.be.ok();
})); }));
describe('unknown type', () => { describe('unknown type', () => {
@ -124,7 +124,7 @@ export default function ({ getService }: FtrProviderContext) {
namespaces: so.namespaces, namespaces: so.namespaces,
})) }))
).to.eql([{ id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', namespaces: [SPACE_ID] }]); ).to.eql([{ id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', namespaces: [SPACE_ID] }]);
expect(resp.body.saved_objects[0].migrationVersion).to.be.ok(); expect(resp.body.saved_objects[0].typeMigrationVersion).to.be.ok();
})); }));
}); });

View file

@ -36,8 +36,8 @@ export default function ({ getService }: FtrProviderContext) {
updated_at: resp.body.updated_at, updated_at: resp.body.updated_at,
created_at: resp.body.created_at, created_at: resp.body.created_at,
version: resp.body.version, version: resp.body.version,
migrationVersion: resp.body.migrationVersion, coreMigrationVersion: '8.8.0',
coreMigrationVersion: '7.14.0', typeMigrationVersion: resp.body.typeMigrationVersion,
attributes: { attributes: {
title: 'Count of requests', title: 'Count of requests',
description: '', description: '',
@ -56,7 +56,7 @@ export default function ({ getService }: FtrProviderContext) {
], ],
namespaces: ['default'], namespaces: ['default'],
}); });
expect(resp.body.migrationVersion).to.be.ok(); expect(resp.body.typeMigrationVersion).to.be.ok();
})); }));
describe('doc does not exist', () => { describe('doc does not exist', () => {

View file

@ -41,8 +41,8 @@ export default function ({ getService }: FtrProviderContext) {
updated_at: '2015-01-01T00:00:00.000Z', updated_at: '2015-01-01T00:00:00.000Z',
created_at: '2015-01-01T00:00:00.000Z', created_at: '2015-01-01T00:00:00.000Z',
version: resp.body.saved_object.version, version: resp.body.saved_object.version,
migrationVersion: resp.body.saved_object.migrationVersion, coreMigrationVersion: '8.8.0',
coreMigrationVersion: '7.14.0', typeMigrationVersion: resp.body.saved_object.typeMigrationVersion,
attributes: { attributes: {
title: 'Count of requests', title: 'Count of requests',
description: '', description: '',
@ -63,7 +63,7 @@ export default function ({ getService }: FtrProviderContext) {
}, },
outcome: 'exactMatch', outcome: 'exactMatch',
}); });
expect(resp.body.saved_object.migrationVersion).to.be.ok(); expect(resp.body.saved_object.typeMigrationVersion).to.be.ok();
})); }));
describe('doc does not exist', () => { describe('doc does not exist', () => {

View file

@ -754,11 +754,11 @@ export class DataRecognizer {
.map((o) => o.savedObject!); .map((o) => o.savedObject!);
if (filteredSavedObjects.length) { if (filteredSavedObjects.length) {
results = await this._savedObjectsClient.bulkCreate( results = await this._savedObjectsClient.bulkCreate(
// Add an empty migrationVersion attribute to each saved object to ensure // Add an empty typeMigrationVersion attribute to each saved object to ensure
// it is automatically migrated to the 7.0+ format with a references attribute. // it is automatically migrated to the 7.0+ format with a references attribute.
filteredSavedObjects.map((doc) => ({ filteredSavedObjects.map((doc) => ({
...doc, ...doc,
migrationVersion: {}, typeMigrationVersion: '',
})) }))
); );
} }

View file

@ -45,7 +45,7 @@ export default function ({ getService }) {
type: 'index-pattern', type: 'index-pattern',
}, },
]); ]);
expect(resp.body.migrationVersion).to.eql({ map: '8.4.0' }); // migrtionVersion is derived from both "migrations" and "convertToMultiNamespaceVersion" fields when the object is registered expect(resp.body.typeMigrationVersion).to.be('8.4.0'); // typeMigrationVersion is derived from both "migrations" and "convertToMultiNamespaceVersion" fields when the object is registered
expect(resp.body.attributes.layerListJSON.includes('indexPatternRefName')).to.be(true); expect(resp.body.attributes.layerListJSON.includes('indexPatternRefName')).to.be(true);
}); });