[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,
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

View file

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

View file

@ -181,7 +181,6 @@ export interface SavedObjectsClientContract {
* @param {object} attributes - the attributes to update
* @param {object} options {@link SavedObjectsUpdateOptions}
* @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
* @deprecated See https://github.com/elastic/kibana/issues/149098
*/

View file

@ -27,10 +27,15 @@ export interface SimpleSavedObject<T = unknown> {
id: SavedObjectType<T>['id'];
/** Type of the saved object */
type: SavedObjectType<T>['type'];
/** Migration version of the saved object */
/**
* Migration version of the saved object
* @deprecated
*/
migrationVersion: SavedObjectType<T>['migrationVersion'];
/** Core migration version of the saved object */
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: SavedObjectType<T>['error'];
/** References to other saved objects */

View file

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

View file

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

View file

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

View file

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

View file

@ -934,6 +934,8 @@ describe('SavedObjectsRepository', () => {
_source: {
...response.items[0].create._source,
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}$/),
});
@ -942,6 +944,8 @@ describe('SavedObjectsRepository', () => {
_source: {
...response.items[1].create._source,
namespaces: response.items[1].create._source.namespaces,
coreMigrationVersion: expect.any(String),
typeMigrationVersion: '1.1.1',
},
});
@ -2946,7 +2950,8 @@ describe('SavedObjectsRepository', () => {
attributes,
references,
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',
'migrationVersion',
'coreMigrationVersion',
'typeMigrationVersion',
'updated_at',
'created_at',
'originId',

View file

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

View file

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

View file

@ -25,7 +25,10 @@ export interface SavedObjectsBulkCreateObject<T = unknown> {
version?: string;
/** Array of references to other saved objects */
references?: SavedObjectReference[];
/** {@inheritDoc SavedObjectsMigrationVersion} */
/**
* {@inheritDoc SavedObjectsMigrationVersion}
* @deprecated
*/
migrationVersion?: SavedObjectsMigrationVersion;
/**
* 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.
*/
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 */
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.
**/
version?: string;
/** {@inheritDoc SavedObjectsMigrationVersion} */
/**
* {@inheritDoc SavedObjectsMigrationVersion}
* @deprecated Use {@link SavedObjectsCreateOptions.typeMigrationVersion} instead.
*/
migrationVersion?: SavedObjectsMigrationVersion;
/**
* 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.
*/
coreMigrationVersion?: string;
/**
* A semver value that is used when migrating documents between Kibana versions.
*/
typeMigrationVersion?: string;
/** Array of references to other saved objects */
references?: SavedObjectReference[];
/** 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.
*/
initialize?: boolean;
/** {@link SavedObjectsMigrationVersion} */
/**
* {@link SavedObjectsMigrationVersion}
* @deprecated
*/
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
* operation. See {@link MutatingOperationRefreshSetting}

View file

@ -76,7 +76,6 @@ export interface ISavedObjectsRepository {
* @param {object} [options={}] {@link SavedObjectsCreateOptions} - options for the create operation
* @property {string} [options.id] - force id on creation, not recommended
* @property {boolean} [options.overwrite=false]
* @property {object} [options.migrationVersion=undefined]
* @property {string} [options.namespace]
* @property {array} [options.references=[]] - [{ name, type, id }]
* @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 { _id, _source, _seq_no, _primary_term } = doc;
const { type, namespaces, originId, migrationVersion, references, coreMigrationVersion } =
_source;
const {
type,
namespaces,
originId,
migrationVersion,
references,
coreMigrationVersion,
typeMigrationVersion,
} = _source;
const version =
_seq_no != null || _primary_term != null
@ -109,6 +116,7 @@ export class SavedObjectsSerializer implements ISavedObjectsSerializer {
references: references || [],
...(migrationVersion && { migrationVersion }),
...(coreMigrationVersion && { coreMigrationVersion }),
...(typeMigrationVersion != null ? { typeMigrationVersion } : {}),
...(_source.updated_at && { updated_at: _source.updated_at }),
...(_source.created_at && { created_at: _source.created_at }),
...(version && { version }),
@ -135,6 +143,7 @@ export class SavedObjectsSerializer implements ISavedObjectsSerializer {
version,
references,
coreMigrationVersion,
typeMigrationVersion,
} = savedObj;
const source = {
[type]: attributes,
@ -145,6 +154,7 @@ export class SavedObjectsSerializer implements ISavedObjectsSerializer {
...(originId && { originId }),
...(migrationVersion && { migrationVersion }),
...(coreMigrationVersion && { coreMigrationVersion }),
...(typeMigrationVersion != null ? { typeMigrationVersion } : {}),
...(updated_at && { updated_at }),
...(createdAt && { created_at: createdAt }),
};

View file

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

View file

@ -206,6 +206,7 @@ export class SavedObjectsClient implements SavedObjectsClientContract {
body: JSON.stringify({
attributes,
migrationVersion: options.migrationVersion,
typeMigrationVersion: options.typeMigrationVersion,
references: options.references,
}),
});
@ -216,7 +217,7 @@ export class SavedObjectsClient implements SavedObjectsClientContract {
/**
* 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={}]
* @property {boolean} [options.overwrite=false]
* @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 id: SavedObjectType<T>['id'];
public type: SavedObjectType<T>['type'];
/** @deprecated */
public migrationVersion: SavedObjectType<T>['migrationVersion'];
public coreMigrationVersion: SavedObjectType<T>['coreMigrationVersion'];
public typeMigrationVersion: SavedObjectType<T>['typeMigrationVersion'];
public error: SavedObjectType<T>['error'];
public references: SavedObjectType<T>['references'];
public updatedAt: SavedObjectType<T>['updated_at'];
@ -44,6 +46,7 @@ export class SimpleSavedObjectImpl<T = unknown> implements SimpleSavedObject<T>
references,
migrationVersion,
coreMigrationVersion,
typeMigrationVersion,
namespaces,
updated_at: updatedAt,
created_at: createdAt,
@ -56,6 +59,7 @@ export class SimpleSavedObjectImpl<T = unknown> implements SimpleSavedObject<T>
this._version = version;
this.migrationVersion = migrationVersion;
this.coreMigrationVersion = coreMigrationVersion;
this.typeMigrationVersion = typeMigrationVersion;
this.namespaces = namespaces;
this.updatedAt = updatedAt;
this.createdAt = createdAt;
@ -91,6 +95,7 @@ export class SimpleSavedObjectImpl<T = unknown> implements SimpleSavedObject<T>
.create(this.type, this.attributes, {
migrationVersion: this.migrationVersion,
coreMigrationVersion: this.coreMigrationVersion,
typeMigrationVersion: this.typeMigrationVersion,
references: this.references,
})
.then((sso) => {

View file

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

View file

@ -80,10 +80,15 @@ export interface SavedObject<T = unknown> {
attributes: T;
/** {@inheritdoc SavedObjectReference} */
references: SavedObjectReference[];
/** {@inheritdoc SavedObjectsMigrationVersion} */
/**
* {@inheritdoc SavedObjectsMigrationVersion}
* @deprecated Use `typeMigrationVersion` instead.
*/
migrationVersion?: SavedObjectsMigrationVersion;
/** A semver value that is used when upgrading objects between Kibana versions. */
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
* `namespaceType: 'agnostic'`.

View file

@ -139,8 +139,8 @@ describe('collectSavedObjects()', () => {
const result = await collectSavedObjects({ readStream, supportedTypes, objectLimit });
const collectedObjects = [
{ ...obj1, migrationVersion: {} },
{ ...obj2, migrationVersion: {} },
{ ...obj1, typeMigrationVersion: '' },
{ ...obj2, typeMigrationVersion: '' },
];
const importStateMap = new Map([
[`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 result = await collectSavedObjects({ readStream, supportedTypes, objectLimit });
const collectedObjects = [{ ...obj1, migrationVersion: {} }];
const collectedObjects = [{ ...obj1, typeMigrationVersion: '' }];
const importStateMap = new Map([
[`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
@ -178,6 +178,19 @@ describe('collectSavedObjects()', () => {
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', () => {
test('filters out objects when result === false', async () => {
const readStream = createReadStream(obj1, obj2);
@ -207,7 +220,7 @@ describe('collectSavedObjects()', () => {
filter,
});
const collectedObjects = [{ ...obj2, migrationVersion: {} }];
const collectedObjects = [{ ...obj2, typeMigrationVersion: '' }];
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
[`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
return Object.assign({ migrationVersion: {} }, obj);
return {
...obj,
...(!obj.migrationVersion && !obj.typeMigrationVersion ? { typeMigrationVersion: '' } : {}),
};
}),
createConcatStream([]),
]);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,6 +7,7 @@
*/
import {
getCoreTransformsMock,
getConversionTransformsMock,
getModelVersionTransformsMock,
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', () => {
const foo = createType({ name: 'foo' });
const bar = createType({ name: 'bar' });
@ -285,6 +301,8 @@ describe('buildActiveMigrations', () => {
}
);
getCoreTransformsMock.mockReturnValue([transform(TransformType.Core, '8.8.0')]);
getReferenceTransformsMock.mockReturnValue([
transform(TransformType.Reference, '7.12.0'),
transform(TransformType.Reference, '7.17.3'),
@ -302,6 +320,7 @@ describe('buildActiveMigrations', () => {
expect(Object.keys(migrations).sort()).toEqual(['bar', 'foo']);
expect(migrations.foo.transforms).toEqual([
expectTransform(TransformType.Core, '8.8.0'),
expectTransform(TransformType.Reference, '7.12.0'),
expectTransform(TransformType.Migrate, '7.12.0'),
expectTransform(TransformType.Convert, '7.14.0'),
@ -310,6 +329,7 @@ describe('buildActiveMigrations', () => {
expectTransform(TransformType.Migrate, '7.18.2'),
]);
expect(migrations.bar.transforms).toEqual([
expectTransform(TransformType.Core, '8.8.0'),
expectTransform(TransformType.Reference, '7.12.0'),
expectTransform(TransformType.Migrate, '7.17.0'),
expectTransform(TransformType.Reference, '7.17.3'),

View file

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

View file

@ -124,7 +124,7 @@ describe('DocumentMigrator', () => {
id: 'me',
type: 'user',
attributes: { name: 'Christopher' },
migrationVersion: {},
typeMigrationVersion: '',
})
).toThrow(/Migrations are not ready. Make sure prepareMigrations is called first./i);
@ -133,7 +133,7 @@ describe('DocumentMigrator', () => {
id: 'me',
type: 'user',
attributes: { name: 'Christopher' },
migrationVersion: {},
typeMigrationVersion: '',
})
).toThrow(/Migrations are not ready. Make sure prepareMigrations is called first./i);
});
@ -155,13 +155,15 @@ describe('DocumentMigrator', () => {
id: 'me',
type: 'user',
attributes: { name: 'Christopher' },
migrationVersion: {},
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '',
});
expect(actual).toEqual({
id: 'me',
type: 'user',
attributes: { name: 'Chris' },
migrationVersion: { user: '1.2.3' },
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '1.2.3',
});
});
@ -183,40 +185,14 @@ describe('DocumentMigrator', () => {
id: 'me',
type: 'user',
attributes: {},
migrationVersion: {},
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '',
};
const migratedDoc = migrator.migrate(originalDoc);
expect(_.get(originalDoc, 'attributes.name')).toBeUndefined();
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', () => {
const migrator = new DocumentMigrator({
...testOpts(),
@ -246,7 +222,7 @@ describe('DocumentMigrator', () => {
id: 'me',
type: 'user',
attributes: { name: 'Tyler' },
migrationVersion: {},
typeMigrationVersion: '',
});
expect(actual).toEqual({
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({
...testOpts(),
typeRegistry: createRegistry(
@ -292,11 +268,8 @@ describe('DocumentMigrator', () => {
type: 'user',
attributes: { name: 'Tyler' },
bbb: 'Shazm',
migrationVersion: {
user: '1.0.0',
bbb: '2.3.4',
},
coreMigrationVersion: kibanaVersion,
typeMigrationVersion: '1.0.0',
});
});
@ -317,17 +290,19 @@ describe('DocumentMigrator', () => {
id: 'smelly',
type: 'dog',
attributes: { name: 'Callie' },
migrationVersion: { dog: '1.2.3' },
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '1.2.3',
});
expect(actual).toEqual({
id: 'smelly',
type: 'dog',
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({
...testOpts(),
});
@ -337,14 +312,15 @@ describe('DocumentMigrator', () => {
id: 'smelly',
type: 'dog',
attributes: { name: 'Callie' },
migrationVersion: { dog: '10.2.0' },
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '10.2.0',
})
).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({
...testOpts(),
typeRegistry: createRegistry({
@ -360,10 +336,10 @@ describe('DocumentMigrator', () => {
id: 'fleabag',
type: 'dawg',
attributes: { name: 'Callie' },
migrationVersion: { dawg: '1.2.4' },
typeMigrationVersion: '1.2.4',
})
).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',
type: 'dog',
attributes: { name: 'Callie' },
migrationVersion: { dog: '1.2.0' },
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '1.2.0',
});
expect(actual).toEqual({
id: 'smelly',
type: 'dog',
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', () => {
const migrator = new DocumentMigrator({
...testOpts(),
typeRegistry: createRegistry(
{
name: 'animal',
migrations: {
'1.0.0': setAttr('animal', (name: string) => `Animal: ${name}`),
},
typeRegistry: createRegistry({
name: 'dog',
migrations: {
'2.2.4': setAttr('animal', 'Doggie'),
},
{
name: 'dog',
migrations: {
'2.2.4': setAttr('animal', 'Doggie'),
},
}
),
}),
});
migrator.prepareMigrations();
const actual = migrator.migrate({
id: 'smelly',
type: 'dog',
attributes: { name: 'Callie' },
migrationVersion: { dog: '1.2.0' },
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '1.2.0',
});
expect(actual).toEqual({
id: 'smelly',
type: 'dog',
attributes: { name: 'Callie' },
animal: 'Animal: Doggie',
migrationVersion: { animal: '1.0.0', dog: '2.2.4' },
animal: 'Doggie',
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '2.2.4',
});
});
@ -482,13 +454,15 @@ describe('DocumentMigrator', () => {
id: 'smelly',
type: 'dog',
attributes: { name: 'Callie' },
migrationVersion: {},
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '',
});
expect(actual).toEqual({
id: 'smelly',
type: 'dog',
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',
type: 'dog',
attributes: { name: 'Callie' },
migrationVersion: {},
typeMigrationVersion: '',
coreMigrationVersion: '8.8.0',
});
expect(actual).toEqual({
id: 'smelly',
type: 'cat',
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({
...testOpts(),
typeRegistry: createRegistry({
name: 'cat',
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',
type: 'cat',
attributes: { name: 'Boo' },
migrationVersion: { foo: '4.5.6' },
typeMigrationVersion: '4.5.6',
coreMigrationVersion: '8.8.0',
})
).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({
...testOpts(),
typeRegistry: createRegistry({
name: 'cat',
migrations: {
'1.0.0': setAttr('migrationVersion', {}),
'4.5.7': setAttr('typeMigrationVersion', undefined),
},
}),
});
@ -564,45 +541,21 @@ describe('DocumentMigrator', () => {
id: 'smelly',
type: 'cat',
attributes: { name: 'Boo' },
migrationVersion: { foo: '4.5.6' },
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '4.5.6',
})
).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', () => {
const log = mockLogger;
const failedDoc = {
id: 'smelly',
type: 'dog',
attributes: {},
migrationVersion: {},
typeMigrationVersion: '',
};
const migrator = new DocumentMigrator({
...testOpts(),
@ -648,7 +601,7 @@ describe('DocumentMigrator', () => {
id: 'joker',
type: 'dog',
attributes: {},
migrationVersion: {},
typeMigrationVersion: '',
};
migrator.migrate(doc);
expect(loggingSystemMock.collect(mockLoggerFactory).info[0][0]).toEqual(logTestMsg);
@ -680,7 +633,7 @@ describe('DocumentMigrator', () => {
migrations: {
'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', () => {
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({
...testOpts(),
typeRegistry: createRegistry(
{ 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();
@ -715,8 +668,8 @@ describe('DocumentMigrator', () => {
id: 'mischievous',
type: 'dog',
attributes: { name: 'Ann' },
migrationVersion: { dog: '1.0.0' },
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
},
]);
@ -727,7 +680,7 @@ describe('DocumentMigrator', () => {
...testOpts(),
typeRegistry: createRegistry(
{ 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();
@ -735,8 +688,8 @@ describe('DocumentMigrator', () => {
id: 'mischievous',
type: 'dog',
attributes: { name: 'Ann' },
migrationVersion: { dog: '0.1.0' },
coreMigrationVersion: '2.0.0',
coreMigrationVersion: '20.0.0',
typeMigrationVersion: '0.1.0',
} as SavedObjectUnsanitizedDoc;
const actual = migrator.migrateAndConvert(obj);
expect(actual).toEqual([
@ -744,8 +697,8 @@ describe('DocumentMigrator', () => {
id: 'mischievous',
type: 'dog',
attributes: { name: 'Ann' },
migrationVersion: { dog: '1.0.0' },
coreMigrationVersion: '2.0.0',
coreMigrationVersion: '20.0.0',
typeMigrationVersion: '1.0.0',
namespaces: ['default'],
},
]);
@ -755,8 +708,8 @@ describe('DocumentMigrator', () => {
const migrator = new DocumentMigrator({
...testOpts(),
typeRegistry: createRegistry(
{ name: 'dog', namespaceType: 'multiple', convertToMultiNamespaceTypeVersion: '1.0.0' },
{ name: 'toy', namespaceType: 'multiple', convertToMultiNamespaceTypeVersion: '1.0.0' }
{ name: 'dog', namespaceType: 'multiple', convertToMultiNamespaceTypeVersion: '8.8.0' },
{ name: 'toy', namespaceType: 'multiple', convertToMultiNamespaceTypeVersion: '8.8.0' }
),
});
migrator.prepareMigrations();
@ -764,9 +717,10 @@ describe('DocumentMigrator', () => {
id: 'cowardly',
type: 'dog',
attributes: { name: 'Leslie' },
migrationVersion: {},
references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }],
namespace: 'foo-namespace',
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '',
};
const actual = migrator.migrate(obj);
expect(mockGetConvertedObjectId).not.toHaveBeenCalled();
@ -775,13 +729,14 @@ describe('DocumentMigrator', () => {
type: 'dog',
attributes: { name: 'Leslie' },
references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }],
coreMigrationVersion: '1.0.0',
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '8.8.0',
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
});
});
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({
...testOpts(),
typeRegistry: createRegistry({
@ -798,7 +753,8 @@ describe('DocumentMigrator', () => {
id: 'cowardly',
type: 'dog',
attributes: { name: 'Leslie' },
migrationVersion: { dog: '2.0.0' },
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '2.0.0',
references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }],
namespace: 'foo-namespace',
};
@ -808,14 +764,67 @@ describe('DocumentMigrator', () => {
id: 'cowardly',
type: 'dog',
attributes: { name: 'Leslie' },
migrationVersion: { dog: '2.0.0' },
references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }],
coreMigrationVersion: '3.0.0',
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '2.0.0',
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
});
});
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', () => {
const migrator = new DocumentMigrator({
...testOpts(),
@ -829,7 +838,7 @@ describe('DocumentMigrator', () => {
id: 'bad',
type: 'dog',
attributes: { name: 'Sweet Peach' },
migrationVersion: {},
typeMigrationVersion: '',
references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }],
};
@ -842,7 +851,7 @@ describe('DocumentMigrator', () => {
type: 'dog',
attributes: { name: 'Sweet Peach' },
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',
attributes: { name: 'Sweet Peach' },
references: [{ id: 'uuidv5', type: 'toy', name: 'BALL!' }], // changed
coreMigrationVersion: '1.0.0',
coreMigrationVersion: '8.8.0',
namespace: 'foo-namespace',
},
]);
@ -878,7 +887,8 @@ describe('DocumentMigrator', () => {
id: 'loud',
type: 'dog',
attributes: { name: 'Wally' },
migrationVersion: {},
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '',
};
it('in the default space', () => {
@ -889,8 +899,8 @@ describe('DocumentMigrator', () => {
id: 'loud',
type: 'dog',
attributes: { name: 'Wally' },
migrationVersion: { dog: '1.0.0' },
coreMigrationVersion: '1.0.0',
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '1.0.0',
namespaces: ['default'],
},
]);
@ -905,8 +915,8 @@ describe('DocumentMigrator', () => {
id: 'uuidv5',
type: 'dog',
attributes: { name: 'Wally' },
migrationVersion: { dog: '1.0.0' },
coreMigrationVersion: '1.0.0',
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '1.0.0',
namespaces: ['foo-namespace'],
originId: 'loud',
},
@ -920,14 +930,14 @@ describe('DocumentMigrator', () => {
targetId: 'uuidv5',
purpose: 'savedObjectConversion',
},
migrationVersion: { [LEGACY_URL_ALIAS_TYPE]: '0.1.2' },
coreMigrationVersion: '1.0.0',
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '0.1.2',
},
]);
});
});
describe('correctly applies reference and conversion transforms', () => {
describe('correctly applies core, reference, and conversion transforms', () => {
const migrator = new DocumentMigrator({
...testOpts(),
typeRegistry: createRegistry(
@ -952,9 +962,9 @@ describe('DocumentMigrator', () => {
id: 'cute',
type: 'dog',
attributes: { name: 'Too' },
migrationVersion: { dog: '1.0.0' },
references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }], // no change
coreMigrationVersion: '1.0.0',
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '1.0.0',
namespaces: ['default'],
},
]);
@ -980,9 +990,9 @@ describe('DocumentMigrator', () => {
id: 'uuidv5',
type: 'dog',
attributes: { name: 'Too' },
migrationVersion: { dog: '1.0.0' },
references: [{ id: 'uuidv5', type: 'toy', name: 'BALL!' }], // changed
coreMigrationVersion: '1.0.0',
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '1.0.0',
namespaces: ['foo-namespace'],
originId: 'cute',
},
@ -996,14 +1006,14 @@ describe('DocumentMigrator', () => {
targetId: 'uuidv5',
purpose: 'savedObjectConversion',
},
migrationVersion: { [LEGACY_URL_ALIAS_TYPE]: '0.1.2' },
coreMigrationVersion: '1.0.0',
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '0.1.2',
},
]);
});
});
describe('correctly applies reference and migration transforms', () => {
describe('correctly applies core, reference, and migration transforms', () => {
const migrator = new DocumentMigrator({
...testOpts(),
typeRegistry: createRegistry(
@ -1037,9 +1047,9 @@ describe('DocumentMigrator', () => {
id: 'sleepy',
type: 'dog',
attributes: { name: 'Patches', age: '11', color: 'tri-color' },
migrationVersion: { dog: '2.0.0' },
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',
type: 'dog',
attributes: { name: 'Patches', age: '11', color: 'tri-color' },
migrationVersion: { dog: '2.0.0' },
references: [{ id: 'uuidv5', type: 'toy', name: 'BALL!' }], // changed
coreMigrationVersion: '1.0.0',
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '2.0.0',
namespace: 'foo-namespace',
},
]);
@ -1069,7 +1079,7 @@ describe('DocumentMigrator', () => {
name: 'dog',
namespaceType: 'multiple',
migrations: {
'1.0.0': setAttr('migrationVersion.dog', '2.0.0'),
'1.0.0': setAttr('typeMigrationVersion', '2.0.0'),
'2.0.0': (doc) => doc, // noop
},
convertToMultiNamespaceTypeVersion: '1.0.0', // the conversion transform occurs before the migration transform above
@ -1080,7 +1090,8 @@ describe('DocumentMigrator', () => {
id: 'hungry',
type: 'dog',
attributes: { name: 'Remy' },
migrationVersion: {},
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '',
};
it('in the default space', () => {
@ -1091,8 +1102,8 @@ describe('DocumentMigrator', () => {
id: 'hungry',
type: 'dog',
attributes: { name: 'Remy' },
migrationVersion: { dog: '2.0.0' },
coreMigrationVersion: '1.0.0',
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '2.0.0',
namespaces: ['default'],
},
]);
@ -1107,8 +1118,8 @@ describe('DocumentMigrator', () => {
id: 'uuidv5',
type: 'dog',
attributes: { name: 'Remy' },
migrationVersion: { dog: '2.0.0' },
coreMigrationVersion: '1.0.0',
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '2.0.0',
namespaces: ['foo-namespace'],
originId: 'hungry',
},
@ -1122,14 +1133,14 @@ describe('DocumentMigrator', () => {
targetId: 'uuidv5',
purpose: 'savedObjectConversion',
},
migrationVersion: { [LEGACY_URL_ALIAS_TYPE]: '0.1.2' },
coreMigrationVersion: '1.0.0',
coreMigrationVersion: '8.8.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({
...testOpts(),
typeRegistry: createRegistry(
@ -1137,7 +1148,7 @@ describe('DocumentMigrator', () => {
name: 'dog',
namespaceType: 'multiple',
migrations: {
'1.0.0': setAttr('migrationVersion.dog', '2.0.0'),
'1.0.0': setAttr('typeMigrationVersion', '2.0.0'),
'2.0.0': (doc) => doc, // noop
},
convertToMultiNamespaceTypeVersion: '1.0.0',
@ -1162,9 +1173,9 @@ describe('DocumentMigrator', () => {
id: 'pretty',
type: 'dog',
attributes: { name: 'Sasha' },
migrationVersion: { dog: '2.0.0' },
references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }], // no change
coreMigrationVersion: '1.0.0',
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '2.0.0',
namespaces: ['default'],
},
]);
@ -1190,9 +1201,9 @@ describe('DocumentMigrator', () => {
id: 'uuidv5',
type: 'dog',
attributes: { name: 'Sasha' },
migrationVersion: { dog: '2.0.0' },
references: [{ id: 'uuidv5', type: 'toy', name: 'BALL!' }], // changed
coreMigrationVersion: '1.0.0',
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '2.0.0',
namespaces: ['foo-namespace'],
originId: 'pretty',
},
@ -1206,13 +1217,117 @@ describe('DocumentMigrator', () => {
targetId: 'uuidv5',
purpose: 'savedObjectConversion',
},
migrationVersion: { [LEGACY_URL_ALIAS_TYPE]: '0.1.2' },
coreMigrationVersion: '1.0.0',
coreMigrationVersion: '8.8.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.
*/
import Boom from '@hapi/boom';
import { set } from '@kbn/safer-lodash-set';
import _ from 'lodash';
import Semver from 'semver';
import type { Logger } from '@kbn/logging';
import type { SavedObjectsMigrationVersion } from '@kbn/core-saved-objects-common';
import type {
SavedObjectUnsanitizedDoc,
ISavedObjectTypeRegistry,
} from '@kbn/core-saved-objects-server';
import type { ActiveMigrations, TransformResult } from './types';
import type { ActiveMigrations } from './types';
import { maxVersion } from './utils';
import { buildActiveMigrations } from './build_active_migrations';
import { DocumentMigratorPipeline } from './document_migrator_pipeline';
export type MigrateFn = (doc: SavedObjectUnsanitizedDoc) => SavedObjectUnsanitizedDoc;
export type MigrateAndConvertFn = (doc: SavedObjectUnsanitizedDoc) => SavedObjectUnsanitizedDoc[];
type ApplyTransformsFn = (
doc: SavedObjectUnsanitizedDoc,
options?: TransformOptions
) => TransformResult;
interface TransformOptions {
convertNamespaceTypes?: boolean;
}
@ -90,7 +83,6 @@ export interface VersionedTransformer {
export class DocumentMigrator implements VersionedTransformer {
private documentMigratorOptions: DocumentMigratorOptions;
private migrations?: ActiveMigrations;
private transformDoc?: ApplyTransformsFn;
/**
* Creates an instance of DocumentMigrator.
@ -143,12 +135,33 @@ export class DocumentMigrator implements VersionedTransformer {
log,
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.
*
@ -157,16 +170,9 @@ export class DocumentMigrator implements VersionedTransformer {
* @memberof DocumentMigrator
*/
public migrate = (doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc => {
if (!this.migrations || !this.transformDoc) {
throw new Error('Migrations are not ready. Make sure prepareMigrations is called first.');
}
const { document } = this.transform(doc);
// 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 { transformedDoc } = this.transformDoc(clonedDoc);
return transformedDoc;
return document;
};
/**
@ -178,364 +184,8 @@ export class DocumentMigrator implements VersionedTransformer {
* @memberof DocumentMigrator
*/
public migrateAndConvert = (doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc[] => {
if (!this.migrations || !this.transformDoc) {
throw new Error('Migrations are not ready. Make sure prepareMigrations is called first.');
}
const { document, additionalDocs } = this.transform(doc, { convertNamespaceTypes: true });
// 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 { transformedDoc, additionalDocs } = this.transformDoc(clonedDoc, {
convertNamespaceTypes: true,
});
return [transformedDoc, ...additionalDocs];
return [document, ...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,
LegacyUrlAlias,
} from '@kbn/core-saved-objects-base-server-internal';
import { migrations as coreMigrationsMap } from './migrations';
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.
*/

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 {
/** Derived from the related transforms */
latestVersion: Record<TransformType, string>;
/** List of transforms registered for the type **/
/** Ordered list of transforms registered for the type **/
transforms: Transform[];
}
@ -37,20 +37,29 @@ export interface Transform {
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 {
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',
/**
* 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',
}

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 { 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
* 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
* first, 'convert' transforms always run second, and 'migrate' transforms always run last. This is because:
* Transforms are sorted in ascending order by version depending on their type:
* - `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.
* 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.
* 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.One version may contain multiple transforms.
*/
export function transformComparator(a: Transform, b: Transform) {
const semver = Semver.compare(a.version, b.version);
if (semver !== 0) {
return semver;
} else if (a.transformType !== b.transformType) {
if (a.transformType === TransformType.Migrate) {
return 1;
} else if (b.transformType === TransformType.Migrate) {
return -1;
} else if (a.transformType === TransformType.Convert) {
return 1;
} else if (b.transformType === TransformType.Convert) {
return -1;
}
const aPriority = TRANSFORM_PRIORITY.indexOf(a.transformType);
const bPriority = TRANSFORM_PRIORITY.indexOf(b.transformType);
if (
aPriority !== bPriority &&
(a.transformType === TransformType.Core || b.transformType === TransformType.Core)
) {
return aPriority - bPriority;
}
return 0;
return Semver.compare(a.version, b.version) || aPriority - bPriority;
}
export function maxVersion(a?: string, b?: string) {

View file

@ -369,41 +369,107 @@ describe('createInitialState', () => {
logger: mockLogger.get(),
}).outdatedDocumentsQuery
).toMatchInlineSnapshot(`
Object {
"bool": Object {
"should": Array [
Object {
"bool": Object {
"must": Object {
Object {
"bool": Object {
"should": Array [
Object {
"bool": Object {
"must": Array [
Object {
"term": Object {
"type": "my_dashboard",
},
},
"must_not": Object {
"term": Object {
"migrationVersion.my_dashboard": "7.10.1",
Object {
"bool": Object {
"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 {
"must": Object {
},
Object {
"bool": Object {
"must": Array [
Object {
"term": Object {
"type": "my_viz",
},
},
"must_not": Object {
"term": Object {
"migrationVersion.my_viz": "8.0.0",
Object {
"bool": Object {
"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', () => {

View file

@ -7,6 +7,7 @@
*/
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 { Logger } from '@kbn/logging';
import type { SavedObjectsMigrationVersion } from '@kbn/core-saved-objects-common';
@ -44,12 +45,33 @@ export const createInitialState = ({
docLinks: DocLinksServiceStart;
logger: Logger;
}): InitState => {
const outdatedDocumentsQuery = {
const outdatedDocumentsQuery: QueryDslQueryContainer = {
bool: {
should: Object.entries(migrationVersionPerType).map(([type, latestVersion]) => ({
bool: {
must: { term: { type } },
must_not: { term: { [`migrationVersion.${type}`]: latestVersion } },
must: [
{ 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()),
migrationVersion: schema.maybe(schema.recordOf(schema.string(), schema.string())),
coreMigrationVersion: schema.maybe(schema.string()),
typeMigrationVersion: schema.maybe(schema.string()),
references: schema.maybe(
schema.arrayOf(
schema.object({

View file

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

View file

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

View file

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

View file

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

View file

@ -118,7 +118,7 @@ describe('migration v2', () => {
await root.preboot();
await root.setup();
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(
@ -131,7 +131,7 @@ describe('migration v2', () => {
expect(
records.find((rec) =>
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();

View file

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

View file

@ -196,7 +196,7 @@ describe('migration v2', () => {
migratedDocs.forEach((doc, i) => {
expect(doc.id).toBe(`foo:${i}`);
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) => {
expect(doc.id).toBe(`foo:${i}`);
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) => {
expect(doc.id).toBe(`foo:${i}`);
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) => {
expect(doc.id).toBe(`foo:${i}`);
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);
const [doc] = migratedDocs;
expect(doc._source.migrationVersion.foo).toBe('7.14.0');
expect(doc._source.coreMigrationVersion).toBe('8.0.0');
expect(doc._source.coreMigrationVersion).toBe('8.8.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 migrationsKeys = migrationsMap ? Object.keys(migrationsMap) : [];
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);
}
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,
expectedVersions: Record<string, string | undefined>
) => {
const migrationVersions = doc._source.migrationVersion;
const type = doc._source.type;
expect(migrationVersions ? migrationVersions[type] : undefined).toEqual(expectedVersions[type]);
expect(doc._source.typeMigrationVersion).toEqual(expectedVersions[type]);
};
const stopServers = async () => {

View file

@ -191,7 +191,7 @@ describe('migration v2', () => {
migratedFooDocs.forEach((doc, i) => {
expect(doc.id).toBe(`foo:${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');
@ -199,7 +199,7 @@ describe('migration v2', () => {
migratedBarDocs.forEach((doc, i) => {
expect(doc.id).toBe(`bar:${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' },
references: [],
namespaces: ['default'],
migrationVersion: { foo: '8.0.0' },
coreMigrationVersion: '8.0.0',
coreMigrationVersion: expect.any(String),
typeMigrationVersion: '8.0.0',
},
{
id: `foo:${newFooId}`,
@ -197,8 +197,8 @@ describe('migration v2', () => {
references: [],
namespaces: ['spacex'],
originId: '1',
migrationVersion: { foo: '8.0.0' },
coreMigrationVersion: '8.0.0',
coreMigrationVersion: expect.any(String),
typeMigrationVersion: '8.0.0',
},
{
// new object for spacex:foo:1
@ -211,9 +211,9 @@ describe('migration v2', () => {
targetType: 'foo',
purpose: 'savedObjectConversion',
},
migrationVersion: { 'legacy-url-alias': '8.2.0' },
references: [],
coreMigrationVersion: '8.0.0',
coreMigrationVersion: expect.any(String),
typeMigrationVersion: '8.2.0',
},
{
id: 'bar:1',
@ -221,8 +221,8 @@ describe('migration v2', () => {
bar: { nomnom: 1 },
references: [{ type: 'foo', id: '1', name: 'Foo 1 default' }],
namespaces: ['default'],
migrationVersion: { bar: '8.0.0' },
coreMigrationVersion: '8.0.0',
coreMigrationVersion: expect.any(String),
typeMigrationVersion: '8.0.0',
},
{
id: `bar:${newBarId}`,
@ -231,8 +231,8 @@ describe('migration v2', () => {
references: [{ type: 'foo', id: newFooId, name: 'Foo 1 spacex' }],
namespaces: ['spacex'],
originId: '1',
migrationVersion: { bar: '8.0.0' },
coreMigrationVersion: '8.0.0',
coreMigrationVersion: expect.any(String),
typeMigrationVersion: '8.0.0',
},
{
// new object for spacex:bar:1
@ -245,9 +245,9 @@ describe('migration v2', () => {
targetType: 'bar',
purpose: 'savedObjectConversion',
},
migrationVersion: { 'legacy-url-alias': '8.2.0' },
references: [],
coreMigrationVersion: '8.0.0',
coreMigrationVersion: expect.any(String),
typeMigrationVersion: '8.2.0',
},
].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).toHaveBeenCalledWith(
[expect.objectContaining({ migrationVersion: {} })],
[expect.objectContaining({ typeMigrationVersion: '' })],
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).toHaveBeenCalledWith(
[expect.objectContaining({ migrationVersion: {} })],
[expect.objectContaining({ typeMigrationVersion: '' })],
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).toHaveBeenCalledWith(
[{ type, id, attributes, migrationVersion: {} }],
[{ type, id, attributes, typeMigrationVersion: '' }],
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).toHaveBeenCalledWith(
[{ type, id, attributes, migrationVersion: {} }],
[{ type, id, attributes, typeMigrationVersion: '' }],
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).toHaveBeenCalledWith(
[{ type, id, attributes, references, migrationVersion: {} }],
[{ type, id, attributes, references, typeMigrationVersion: '' }],
expect.objectContaining({ overwrite: undefined })
);
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).toHaveBeenCalledWith(
[{ type, id, attributes, references, migrationVersion: {} }],
[{ type, id, attributes, references, typeMigrationVersion: '' }],
expect.objectContaining({ overwrite: undefined })
);
expect(savedObjectsClient.bulkGet).not.toHaveBeenCalled();

View file

@ -25,11 +25,9 @@ export const getSavedObjects = (): SavedObject[] => [
name: 'Kibana Sample Data eCommerce',
typeMeta: '{}',
},
coreMigrationVersion: '8.0.0',
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
migrationVersion: {
'index-pattern': '7.11.0',
},
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '7.11.0',
references: [],
type: 'index-pattern',
updated_at: '2021-08-05T12:23:57.577Z',
@ -49,11 +47,9 @@ export const getSavedObjects = (): SavedObject[] => [
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"}}',
},
coreMigrationVersion: '8.0.0',
id: '45e07720-b890-11e8-a6d9-e546fe2bba5f',
migrationVersion: {
visualization: '7.14.0',
},
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '7.14.0',
references: [
{
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
@ -84,11 +80,9 @@ export const getSavedObjects = (): SavedObject[] => [
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"}}',
},
coreMigrationVersion: '8.0.0',
id: 'b80e6540-b891-11e8-a6d9-e546fe2bba5f',
migrationVersion: {
visualization: '7.14.0',
},
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '7.14.0',
references: [
{
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
@ -122,11 +116,9 @@ export const getSavedObjects = (): SavedObject[] => [
}),
version: 1,
},
coreMigrationVersion: '8.0.0',
id: '3ba638e0-b894-11e8-a6d9-e546fe2bba5f',
migrationVersion: {
search: '7.9.3',
},
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '7.9.3',
references: [
{
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
@ -143,8 +135,8 @@ export const getSavedObjects = (): SavedObject[] => [
type: 'visualization',
updated_at: '2021-10-28T15:07:24.077Z',
version: '1',
coreMigrationVersion: '8.0.0',
migrationVersion: { visualization: '8.0.0' },
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '8.0.0',
attributes: {
title: i18n.translate('home.sampleData.ecommerceSpec.salesCountMapTitle', {
defaultMessage: '[eCommerce] Sales Count Map',
@ -179,11 +171,9 @@ export const getSavedObjects = (): SavedObject[] => [
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":[]}',
},
coreMigrationVersion: '8.0.0',
id: 'c00d1f90-f5ea-11eb-a78e-83aac3c38a60',
migrationVersion: {
visualization: '7.14.0',
},
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '7.14.0',
references: [],
type: 'visualization',
updated_at: '2021-08-05T12:43:35.817Z',
@ -328,11 +318,9 @@ export const getSavedObjects = (): SavedObject[] => [
title: '% of target revenue ($10k)',
visualizationType: 'lnsXY',
},
coreMigrationVersion: '8.0.0',
id: 'c762b7a0-f5ea-11eb-a78e-83aac3c38a60',
migrationVersion: {
lens: '7.14.0',
},
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '7.14.0',
references: [
{
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
@ -386,11 +374,9 @@ export const getSavedObjects = (): SavedObject[] => [
title: 'Sum of revenue',
visualizationType: 'lnsMetric',
},
coreMigrationVersion: '8.0.0',
id: 'ce02e260-f5ea-11eb-a78e-83aac3c38a60',
migrationVersion: {
lens: '7.14.0',
},
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '7.14.0',
references: [
{
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
@ -444,11 +430,9 @@ export const getSavedObjects = (): SavedObject[] => [
title: 'Median spending',
visualizationType: 'lnsMetric',
},
coreMigrationVersion: '8.0.0',
id: 'd5f90030-f5ea-11eb-a78e-83aac3c38a60',
migrationVersion: {
lens: '7.14.0',
},
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '7.14.0',
references: [
{
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
@ -600,11 +584,9 @@ export const getSavedObjects = (): SavedObject[] => [
title: 'Transactions per day',
visualizationType: 'lnsXY',
},
coreMigrationVersion: '8.0.0',
id: 'dde978b0-f5ea-11eb-a78e-83aac3c38a60',
migrationVersion: {
lens: '7.14.0',
},
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '7.14.0',
references: [
{
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
@ -666,11 +648,9 @@ export const getSavedObjects = (): SavedObject[] => [
title: 'Avg. items sold',
visualizationType: 'lnsMetric',
},
coreMigrationVersion: '8.0.0',
id: 'e3902840-f5ea-11eb-a78e-83aac3c38a60',
migrationVersion: {
lens: '7.14.0',
},
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '7.14.0',
references: [
{
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
@ -793,11 +773,9 @@ export const getSavedObjects = (): SavedObject[] => [
title: 'Breakdown by category',
visualizationType: 'lnsXY',
},
coreMigrationVersion: '8.0.0',
id: 'eddf7850-f5ea-11eb-a78e-83aac3c38a60',
migrationVersion: {
lens: '7.14.0',
},
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '7.14.0',
references: [
{
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
@ -993,11 +971,9 @@ export const getSavedObjects = (): SavedObject[] => [
title: 'Daily comparison',
visualizationType: 'lnsDatatable',
},
coreMigrationVersion: '8.0.0',
id: 'ff6a21b0-f5ea-11eb-a78e-83aac3c38a60',
migrationVersion: {
lens: '7.14.0',
},
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '7.14.0',
references: [
{
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
@ -1109,11 +1085,9 @@ export const getSavedObjects = (): SavedObject[] => [
title: 'Top products this week',
visualizationType: 'lnsXY',
},
coreMigrationVersion: '8.0.0',
id: '03071e90-f5eb-11eb-a78e-83aac3c38a60',
migrationVersion: {
lens: '7.14.0',
},
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '7.14.0',
references: [
{
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
@ -1225,11 +1199,9 @@ export const getSavedObjects = (): SavedObject[] => [
title: 'Top products last week',
visualizationType: 'lnsXY',
},
coreMigrationVersion: '8.0.0',
id: '06379e00-f5eb-11eb-a78e-83aac3c38a60',
migrationVersion: {
lens: '7.14.0',
},
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '7.14.0',
references: [
{
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
@ -1370,9 +1342,7 @@ export const getSavedObjects = (): SavedObject[] => [
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
},
],
migrationVersion: {
dashboard: '8.5.0',
},
coreMigrationVersion: '8.6.0',
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '8.5.0',
},
];

View file

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

View file

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

View file

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

View file

@ -71,8 +71,8 @@ export default function ({ getService }: FtrProviderContext) {
kibanaSavedObjectMeta:
resp.body.saved_objects[0].attributes.kibanaSavedObjectMeta,
},
migrationVersion: resp.body.saved_objects[0].migrationVersion,
coreMigrationVersion: '7.14.0',
coreMigrationVersion: '8.8.0',
typeMigrationVersion: resp.body.saved_objects[0].typeMigrationVersion,
namespaces: ['default'],
references: [
{
@ -102,13 +102,13 @@ export default function ({ getService }: FtrProviderContext) {
defaultIndex: '91200a00-9efd-11e7-acb3-3dab96693fab',
},
namespaces: ['default'],
migrationVersion: resp.body.saved_objects[2].migrationVersion,
coreMigrationVersion: '7.14.0',
coreMigrationVersion: '8.8.0',
typeMigrationVersion: resp.body.saved_objects[2].typeMigrationVersion,
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({
id: resp.body.id,
type: 'visualization',
migrationVersion: resp.body.migrationVersion,
coreMigrationVersion: '8.0.0',
coreMigrationVersion: '8.8.0',
typeMigrationVersion: resp.body.typeMigrationVersion,
updated_at: resp.body.updated_at,
created_at: resp.body.created_at,
version: resp.body.version,
@ -60,7 +60,7 @@ export default function ({ getService }: FtrProviderContext) {
references: [],
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: {
title: 'My favorite vis',
},
coreMigrationVersion: '1.2.3',
coreMigrationVersion: '8.8.0',
})
.expect(200)
.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,
},
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
migrationVersion: objects[0].migrationVersion,
coreMigrationVersion: '7.14.0',
coreMigrationVersion: '8.8.0',
typeMigrationVersion: objects[0].typeMigrationVersion,
references: [
{
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
@ -362,7 +362,7 @@ export default function ({ getService }: FtrProviderContext) {
created_at: objects[0].created_at,
version: objects[0].version,
});
expect(objects[0].migrationVersion).to.be.ok();
expect(objects[0].typeMigrationVersion).to.be.ok();
expect(() =>
JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON)
).not.to.throwError();
@ -409,8 +409,8 @@ export default function ({ getService }: FtrProviderContext) {
version: 1,
},
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
migrationVersion: objects[0].migrationVersion,
coreMigrationVersion: '7.14.0',
coreMigrationVersion: '8.8.0',
typeMigrationVersion: objects[0].typeMigrationVersion,
references: [
{
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
@ -423,7 +423,7 @@ export default function ({ getService }: FtrProviderContext) {
created_at: objects[0].created_at,
version: objects[0].version,
});
expect(objects[0].migrationVersion).to.be.ok();
expect(objects[0].typeMigrationVersion).to.be.ok();
expect(() =>
JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON)
).not.to.throwError();
@ -475,8 +475,8 @@ export default function ({ getService }: FtrProviderContext) {
version: 1,
},
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
migrationVersion: objects[0].migrationVersion,
coreMigrationVersion: '7.14.0',
coreMigrationVersion: '8.8.0',
typeMigrationVersion: objects[0].typeMigrationVersion,
references: [
{
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
@ -489,7 +489,7 @@ export default function ({ getService }: FtrProviderContext) {
created_at: objects[0].updated_at,
version: objects[0].version,
});
expect(objects[0].migrationVersion).to.be.ok();
expect(objects[0].typeMigrationVersion).to.be.ok();
expect(() =>
JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON)
).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([
'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', () => {
@ -124,7 +124,7 @@ export default function ({ getService }: FtrProviderContext) {
namespaces: so.namespaces,
}))
).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,
created_at: resp.body.created_at,
version: resp.body.version,
migrationVersion: resp.body.migrationVersion,
coreMigrationVersion: '7.14.0',
coreMigrationVersion: '8.8.0',
typeMigrationVersion: resp.body.typeMigrationVersion,
attributes: {
title: 'Count of requests',
description: '',
@ -56,7 +56,7 @@ export default function ({ getService }: FtrProviderContext) {
],
namespaces: ['default'],
});
expect(resp.body.migrationVersion).to.be.ok();
expect(resp.body.typeMigrationVersion).to.be.ok();
}));
describe('doc does not exist', () => {

View file

@ -41,8 +41,8 @@ export default function ({ getService }: FtrProviderContext) {
updated_at: '2015-01-01T00:00:00.000Z',
created_at: '2015-01-01T00:00:00.000Z',
version: resp.body.saved_object.version,
migrationVersion: resp.body.saved_object.migrationVersion,
coreMigrationVersion: '7.14.0',
coreMigrationVersion: '8.8.0',
typeMigrationVersion: resp.body.saved_object.typeMigrationVersion,
attributes: {
title: 'Count of requests',
description: '',
@ -63,7 +63,7 @@ export default function ({ getService }: FtrProviderContext) {
},
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', () => {

View file

@ -754,11 +754,11 @@ export class DataRecognizer {
.map((o) => o.savedObject!);
if (filteredSavedObjects.length) {
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.
filteredSavedObjects.map((doc) => ({
...doc,
migrationVersion: {},
typeMigrationVersion: '',
}))
);
}

View file

@ -45,7 +45,7 @@ export default function ({ getService }) {
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);
});