[Migrations] Add support of deferred migrations (#153117)

* Add deferred migrations parameter.
* Update outdated documents query to take into account deferred migrations.
* Update outdated documents query to take into account the core migration version.
* Update read operations in the saved objects repository to perform deferred migrations.
This commit is contained in:
Michael Dokolin 2023-05-22 11:17:41 +02:00 committed by GitHub
parent 633444e615
commit a65cd356aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
75 changed files with 2141 additions and 834 deletions

View file

@ -254,6 +254,38 @@ the error should be verbose and informative so that the corrupt document can be
**WARNING:** Do not attempt to change the `typeMigrationVersion`, `id`, or `type` fields within a migration function, this is not supported.
### Deferred Migrations
Usually, migrations run during the upgrade process, and sometimes that may block it if there is a huge amount of outdated objects.
In this case, it is recommended to mark some of the migrations to defer their execution.
```ts
export const dashboardVisualization: SavedObjectsType = {
name: 'dashboard_visualization', [1]
/** ... */
migrations: {
// Takes a pre 1.1.0 doc, and converts it to 1.1.0
'1.1.0': {
deferred: true,
transform: migrateDashboardVisualization110,
},
},
};
```
By default, all the migrations are not deferred, and in order to make them so, the `deferred` flag should be explicitly set to `true`.
In this case, the documents with only pending deferred migrations will not be migrated during the upgrade process.
But whenever they are accessed via Saved Object API or repository, all the migrations will be applied to them on the fly:
- On read operations, the stored objects remain untouched and only transformed before returning the result.
If there are some failures during the migration, an exception or 500 server error will be thrown,
so that it is guaranteed that all the returned objects will be up to date.
- On write operations, the objects will be migrated to the latest version before writing them.
In other words, this flag postpones the write operation until the objects are explicitly modified.
One important notice: if there is a few pending migrations for a document and not all of them can be deferred,
the document will be migrated during the upgrade process, and all pending migrations will be applied.
### Testing Migrations
Bugs in a migration function cause downtime for our users and therefore have a very high impact. Follow the <DocLink id="kibDevTutorialTestingPlugins" section="saved-objects-migrations" text="Saved Object migrations section in the plugin testing guide"/>.

View file

@ -46,7 +46,15 @@ type ExpectedBulkGetResult = Either<
export const performBulkGet = async <T>(
{ objects, options }: PerformBulkGetParams<T>,
{ helpers, allowedTypes, client, serializer, registry, extensions = {} }: ApiExecutionContext
{
helpers,
allowedTypes,
client,
migrator,
serializer,
registry,
extensions = {},
}: ApiExecutionContext
): Promise<SavedObjectsBulkResponse<T>> => {
const {
common: commonHelper,
@ -192,9 +200,12 @@ export const performBulkGet = async <T>(
}
// @ts-expect-error MultiGetHit._source is optional
return getSavedObjectFromSource(registry, type, id, doc, {
const document = getSavedObjectFromSource(registry, type, id, doc, {
migrationVersionCompatibility,
});
const migrated = migrator.migrateDocument(document);
return migrated;
}),
};

View file

@ -32,6 +32,7 @@ export const performBulkResolve = async <T>(
helpers,
allowedTypes,
client,
migrator,
serializer,
extensions = {},
} = apiExecutionContext;
@ -43,6 +44,7 @@ export const performBulkResolve = async <T>(
registry,
allowedTypes,
client,
migrator,
serializer,
getIndexForType: commonHelper.getIndexForType.bind(commonHelper),
incrementCounterInternal: (type, id, counterFields, opts = {}) =>

View file

@ -13,6 +13,7 @@ import {
SavedObjectsErrorHelpers,
type SavedObjectsRawDoc,
CheckAuthorizationResult,
type SavedObject,
SavedObjectsRawDocSource,
} from '@kbn/core-saved-objects-server';
import {
@ -48,7 +49,6 @@ export const performFind = async <T = unknown, A = unknown>(
allowedTypes: rawAllowedTypes,
mappings,
client,
serializer,
migrator,
extensions = {},
}: ApiExecutionContext
@ -229,22 +229,32 @@ export const performFind = async <T = unknown, A = unknown>(
return SavedObjectsUtils.createEmptyFindResponse<T, A>(options);
}
const result = {
...(body.aggregations ? { aggregations: body.aggregations as unknown as A } : {}),
page,
per_page: perPage,
total: body.hits.total,
saved_objects: body.hits.hits.map(
(hit: estypes.SearchHit<SavedObjectsRawDocSource>): SavedObjectsFindResult => ({
...serializerHelper.rawToSavedObject(hit as SavedObjectsRawDoc, {
migrationVersionCompatibility,
}),
score: hit._score!,
sort: hit.sort,
})
),
pit_id: body.pit_id,
} as SavedObjectsFindResponse<T, A>;
let result: SavedObjectsFindResponse<T, A>;
try {
result = {
...(body.aggregations ? { aggregations: body.aggregations as unknown as A } : {}),
page,
per_page: perPage,
total: body.hits.total,
saved_objects: body.hits.hits.map(
(hit: estypes.SearchHit<SavedObjectsRawDocSource>): SavedObjectsFindResult => ({
...(migrator.migrateDocument(
serializerHelper.rawToSavedObject(hit as SavedObjectsRawDoc, {
migrationVersionCompatibility,
})
) as SavedObject),
score: hit._score!,
sort: hit.sort,
})
),
pit_id: body.pit_id,
} as typeof result;
} catch (error) {
throw SavedObjectsErrorHelpers.decorateGeneralError(
error,
'Failed to migrate document to the latest version.'
);
}
if (disableExtensions) {
return result;

View file

@ -24,7 +24,15 @@ export interface PerformGetParams {
export const performGet = async <T>(
{ type, id, options }: PerformGetParams,
{ registry, helpers, allowedTypes, client, serializer, extensions = {} }: ApiExecutionContext
{
registry,
helpers,
allowedTypes,
client,
migrator,
serializer,
extensions = {},
}: ApiExecutionContext
): Promise<SavedObject<T>> => {
const { common: commonHelper, encryption: encryptionHelper } = helpers;
const { securityExtension } = extensions;
@ -68,12 +76,22 @@ export const performGet = async <T>(
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}
const result = getSavedObjectFromSource<T>(registry, type, id, body, {
const document = getSavedObjectFromSource<T>(registry, type, id, body, {
migrationVersionCompatibility,
});
let migrated: SavedObject<T>;
try {
migrated = migrator.migrateDocument(document) as SavedObject<T>;
} catch (error) {
throw SavedObjectsErrorHelpers.decorateGeneralError(
error,
'Failed to migrate document to the latest version.'
);
}
return encryptionHelper.optionallyDecryptAndRedactSingleResult(
result,
migrated,
authorizationResult?.typeMap
);
};

View file

@ -31,6 +31,7 @@ import {
type ISavedObjectTypeRegistry,
type SavedObject,
SavedObjectsErrorHelpers,
type SavedObjectUnsanitizedDoc,
} from '@kbn/core-saved-objects-server';
import {
enforceError,
@ -38,6 +39,7 @@ import {
setupAuthorizeAndRedactInternalBulkResolveSuccess,
} from '../../../test_helpers/repository.test.common';
import { savedObjectsExtensionsMock } from '../../../mocks/saved_objects_extensions.mock';
import { kibanaMigratorMock } from '../../../mocks';
const VERSION_PROPS = { _seq_no: 1, _primary_term: 1 };
const OBJ_TYPE = 'obj-type';
@ -57,6 +59,7 @@ beforeEach(() => {
describe('internalBulkResolve', () => {
let client: ReturnType<typeof elasticsearchClientMock.createElasticsearchClient>;
let migrator: ReturnType<typeof kibanaMigratorMock.create>;
let serializer: SavedObjectsSerializer;
let incrementCounterInternal: jest.Mock<any, any>;
let registry: jest.Mocked<ISavedObjectTypeRegistry>;
@ -72,11 +75,13 @@ describe('internalBulkResolve', () => {
): InternalBulkResolveParams {
registry = typeRegistryMock.create();
client = elasticsearchClientMock.createElasticsearchClient();
migrator = kibanaMigratorMock.create();
serializer = new SavedObjectsSerializer(registry);
incrementCounterInternal = jest.fn().mockRejectedValue(new Error('increment error')); // mock error to implicitly test that it is caught and swallowed
return {
registry,
allowedTypes: [OBJ_TYPE, ENCRYPTED_TYPE],
migrator,
client,
serializer,
getIndexForType: (type: string) => `index-for-${type}`,
@ -223,7 +228,7 @@ describe('internalBulkResolve', () => {
};
}
for (const namespace of [undefined, 'default', 'space-x']) {
describe.each([undefined, 'default', 'space-x'])(`with namespace '%s'`, (namespace) => {
const expectedNamespaceString = SavedObjectsUtils.namespaceIdToString(namespace);
it('throws if mget call results in non-ES-originated 404 error', async () => {
@ -362,7 +367,29 @@ describe('internalBulkResolve', () => {
expectConflictResult({ id: '7', alias_target_id: '7-newId', alias_purpose: 'y' }),
]);
});
}
it('migrates the resolved objects', async () => {
const objects = [
{ type: OBJ_TYPE, id: '1' },
{ type: OBJ_TYPE, id: '2' },
];
const params = setup(objects, { namespace });
mockBulkResults({ found: false }, { found: false });
mockMgetResults({ found: true }, { found: true });
migrator.migrateDocument.mockImplementation(
(doc) => `migrated-${doc}` as unknown as SavedObjectUnsanitizedDoc
);
await expect(internalBulkResolve(params)).resolves.toHaveProperty('resolved_objects', [
expect.objectContaining({ saved_object: 'migrated-mock-obj-for-1' }),
expect.objectContaining({ saved_object: 'migrated-mock-obj-for-2' }),
]);
expect(migrator.migrateDocument).toHaveBeenCalledTimes(2);
expect(migrator.migrateDocument).nthCalledWith(1, 'mock-obj-for-1');
expect(migrator.migrateDocument).nthCalledWith(2, 'mock-obj-for-2');
});
});
describe('with encryption extension', () => {
const namespace = 'foo';

View file

@ -27,6 +27,7 @@ import {
SavedObjectsErrorHelpers,
} from '@kbn/core-saved-objects-server';
import {
type IKibanaMigrator,
LEGACY_URL_ALIAS_TYPE,
type LegacyUrlAlias,
} from '@kbn/core-saved-objects-base-server-internal';
@ -59,6 +60,7 @@ const MAX_CONCURRENT_RESOLVE = 10;
*/
export interface InternalBulkResolveParams {
registry: ISavedObjectTypeRegistry;
migrator: IKibanaMigrator;
allowedTypes: string[];
client: RepositoryEsClient;
serializer: ISavedObjectsSerializer;
@ -98,6 +100,7 @@ export async function internalBulkResolve<T>(
): Promise<InternalSavedObjectsBulkResolveResponse<T>> {
const {
registry,
migrator,
allowedTypes,
client,
serializer,
@ -184,10 +187,12 @@ export async function internalBulkResolve<T>(
const object = getSavedObjectFromSource<T>(registry, objectType, objectId, doc, {
migrationVersionCompatibility,
});
if (!encryptionExtension?.isEncryptableType(object.type)) {
return object;
const migrated = migrator.migrateDocument(object) as SavedObject<T>;
if (!encryptionExtension?.isEncryptableType(migrated.type)) {
return migrated;
}
return encryptionExtension.decryptOrStripResponseAttributes(object);
return encryptionExtension.decryptOrStripResponseAttributes(migrated);
}
// map function for pMap below
@ -211,28 +216,39 @@ export async function internalBulkResolve<T>(
const { type, id } = either.value;
let result: SavedObjectsResolveResponse<T> | null = null;
if (foundExactMatch && foundAliasMatch) {
result = {
saved_object: await getSavedObject(type, id, exactMatchDoc!),
outcome: 'conflict',
alias_target_id: aliasInfo!.targetId,
alias_purpose: aliasInfo!.purpose,
try {
if (foundExactMatch && foundAliasMatch) {
result = {
saved_object: await getSavedObject(type, id, exactMatchDoc!),
outcome: 'conflict',
alias_target_id: aliasInfo!.targetId,
alias_purpose: aliasInfo!.purpose,
};
resolveCounter.recordOutcome(REPOSITORY_RESOLVE_OUTCOME_STATS.CONFLICT);
} else if (foundExactMatch) {
result = {
saved_object: await getSavedObject(type, id, exactMatchDoc!),
outcome: 'exactMatch',
};
resolveCounter.recordOutcome(REPOSITORY_RESOLVE_OUTCOME_STATS.EXACT_MATCH);
} else if (foundAliasMatch) {
result = {
saved_object: await getSavedObject(type, aliasInfo!.targetId, aliasMatchDoc!),
outcome: 'aliasMatch',
alias_target_id: aliasInfo!.targetId,
alias_purpose: aliasInfo!.purpose,
};
resolveCounter.recordOutcome(REPOSITORY_RESOLVE_OUTCOME_STATS.ALIAS_MATCH);
}
} catch (error) {
return {
id,
type,
error: SavedObjectsErrorHelpers.decorateGeneralError(
error,
'Failed to migrate document to the latest version.'
),
};
resolveCounter.recordOutcome(REPOSITORY_RESOLVE_OUTCOME_STATS.CONFLICT);
} else if (foundExactMatch) {
result = {
saved_object: await getSavedObject(type, id, exactMatchDoc!),
outcome: 'exactMatch',
};
resolveCounter.recordOutcome(REPOSITORY_RESOLVE_OUTCOME_STATS.EXACT_MATCH);
} else if (foundAliasMatch) {
result = {
saved_object: await getSavedObject(type, aliasInfo!.targetId, aliasMatchDoc!),
outcome: 'aliasMatch',
alias_target_id: aliasInfo!.targetId,
alias_purpose: aliasInfo!.purpose,
};
resolveCounter.recordOutcome(REPOSITORY_RESOLVE_OUTCOME_STATS.ALIAS_MATCH);
}
if (result !== null) {

View file

@ -29,6 +29,7 @@ export const performResolve = async <T>(
helpers,
allowedTypes,
client,
migrator,
serializer,
extensions = {},
} = apiExecutionContext;
@ -39,6 +40,7 @@ export const performResolve = async <T>(
registry,
allowedTypes,
client,
migrator,
serializer,
getIndexForType: commonHelper.getIndexForType.bind(commonHelper),
incrementCounterInternal: (t, i, counterFields, opts = {}) =>

View file

@ -742,6 +742,9 @@ describe('SavedObjectsRepository Security Extension', () => {
attributes: doc._source![doc._source!.type],
references: [],
namespaces: doc._source!.type === NAMESPACE_AGNOSTIC_TYPE ? undefined : [namespace],
coreMigrationVersion: expect.any(String),
typeMigrationVersion: expect.any(String),
managed: expect.any(Boolean),
});
});
});
@ -795,6 +798,9 @@ describe('SavedObjectsRepository Security Extension', () => {
attributes: doc._source![doc._source!.type],
references: [],
namespaces: doc._source!.type === NAMESPACE_AGNOSTIC_TYPE ? undefined : [namespace],
coreMigrationVersion: expect.any(String),
typeMigrationVersion: expect.any(String),
managed: expect.any(Boolean),
});
});
});
@ -849,6 +855,9 @@ describe('SavedObjectsRepository Security Extension', () => {
attributes: doc._source![doc._source!.type],
references: [],
namespaces: doc._source!.type === NAMESPACE_AGNOSTIC_TYPE ? undefined : [namespace],
coreMigrationVersion: expect.any(String),
typeMigrationVersion: expect.any(String),
managed: expect.any(Boolean),
});
});
});

View file

@ -1316,7 +1316,9 @@ describe('SavedObjectsRepository', () => {
version: encodeHitVersion(doc),
attributes: doc._source![type],
references: doc._source!.references || [],
migrationVersion: doc._source!.migrationVersion,
coreMigrationVersion: expect.any(String),
typeMigrationVersion: expect.any(String),
managed: expect.any(Boolean),
});
it(`returns early for empty objects argument`, async () => {
@ -1389,6 +1391,24 @@ describe('SavedObjectsRepository', () => {
],
});
});
it('migrates the fetched documents', async () => {
const response = getMockMgetResponse(registry, [obj1, obj2]);
client.mget.mockResolvedValueOnce(
elasticsearchClientMock.createSuccessTransportRequestPromise(response)
);
migrator.migrateDocument.mockReturnValue(
'migrated' as unknown as ReturnType<typeof migrator.migrateDocument>
);
await expect(bulkGet(repository, [obj1, obj2])).resolves.toHaveProperty('saved_objects', [
'migrated',
'migrated',
]);
expect(migrator.migrateDocument).toHaveBeenCalledTimes(2);
expect(migrator.migrateDocument).nthCalledWith(1, expect.objectContaining({ id: obj1.id }));
expect(migrator.migrateDocument).nthCalledWith(2, expect.objectContaining({ id: obj2.id }));
});
});
});
@ -3910,6 +3930,9 @@ describe('SavedObjectsRepository', () => {
attributes: doc._source![doc._source!.type],
references: [],
namespaces: doc._source!.type === NAMESPACE_AGNOSTIC_TYPE ? undefined : ['default'],
coreMigrationVersion: expect.any(String),
typeMigrationVersion: expect.any(String),
managed: expect.any(Boolean),
});
});
});
@ -3937,6 +3960,9 @@ describe('SavedObjectsRepository', () => {
attributes: doc._source![doc._source!.type],
references: [],
namespaces: doc._source!.type === NAMESPACE_AGNOSTIC_TYPE ? undefined : [namespace],
coreMigrationVersion: expect.any(String),
typeMigrationVersion: expect.any(String),
managed: expect.any(Boolean),
});
});
});
@ -3952,6 +3978,30 @@ describe('SavedObjectsRepository', () => {
await test(HIDDEN_TYPE);
await test(['unknownType', HIDDEN_TYPE]);
});
it('migrates the found document', async () => {
const noNamespaceSearchResults = generateIndexPatternSearchResults();
client.search.mockResolvedValueOnce(
elasticsearchClientMock.createSuccessTransportRequestPromise(noNamespaceSearchResults)
);
migrator.migrateDocument.mockImplementationOnce((doc) => ({ ...doc, migrated: true }));
await expect(repository.find({ type })).resolves.toHaveProperty(
'saved_objects.0.migrated',
true
);
expect(migrator.migrateDocument).toHaveBeenCalledTimes(
noNamespaceSearchResults.hits.hits.length
);
expect(migrator.migrateDocument).toHaveBeenCalledWith(
expect.objectContaining({
type,
id: noNamespaceSearchResults.hits.hits[0]._id.replace(
/(index-pattern|config|globalType)\:/,
''
),
})
);
});
});
describe('search dsl', () => {
@ -4272,6 +4322,9 @@ describe('SavedObjectsRepository', () => {
},
references: [],
namespaces: ['default'],
coreMigrationVersion: expect.any(String),
typeMigrationVersion: expect.any(String),
managed: expect.any(Boolean),
});
});
@ -4300,6 +4353,20 @@ describe('SavedObjectsRepository', () => {
expect(result).toMatchObject({ originId });
});
});
it('migrates the fetched document', async () => {
migrator.migrateDocument.mockReturnValueOnce(
'migrated' as unknown as ReturnType<typeof migrator.migrateDocument>
);
await expect(getSuccess(client, repository, registry, type, id)).resolves.toBe('migrated');
expect(migrator.migrateDocument).toHaveBeenCalledTimes(1);
expect(migrator.migrateDocument).toHaveBeenCalledWith(
expect.objectContaining({
id,
type,
})
);
});
});
describe('#resolve', () => {

View file

@ -582,6 +582,9 @@ export const expectBulkGetResult = (
attributes: doc._source![type],
references: doc._source!.references || [],
migrationVersion: doc._source!.migrationVersion,
managed: expect.any(Boolean),
coreMigrationVersion: expect.any(String),
typeMigrationVersion: expect.any(String),
});
export const getMockBulkCreateResponse = (

View file

@ -0,0 +1,175 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`getOutdatedDocumentsQuery should not select documents if there are no migrations 1`] = `
Object {
"bool": Object {
"should": Array [],
},
}
`;
exports[`getOutdatedDocumentsQuery should select documents with outdated both core and type migration versions 1`] = `
Object {
"bool": Object {
"should": Array [
Object {
"bool": Object {
"must": Array [
Object {
"term": Object {
"type": "dashboard",
},
},
Object {
"bool": Object {
"should": Array [
Object {
"range": Object {
"coreMigrationVersion": Object {
"lt": "8.8.0",
},
},
},
Object {
"bool": Object {
"must_not": Array [
Object {
"exists": Object {
"field": "typeMigrationVersion",
},
},
Object {
"exists": Object {
"field": "migrationVersion.dashboard",
},
},
],
},
},
Object {
"bool": Object {
"must": Object {
"exists": Object {
"field": "migrationVersion",
},
},
"must_not": Object {
"term": Object {
"migrationVersion.dashboard": "7.7.0",
},
},
},
},
Object {
"range": Object {
"typeMigrationVersion": Object {
"lt": "7.7.0",
},
},
},
],
},
},
],
},
},
],
},
}
`;
exports[`getOutdatedDocumentsQuery should select documents with outdated core migration version 1`] = `
Object {
"bool": Object {
"should": Array [
Object {
"bool": Object {
"must": Array [
Object {
"term": Object {
"type": "dashboard",
},
},
Object {
"bool": Object {
"should": Array [
Object {
"range": Object {
"coreMigrationVersion": Object {
"lt": "8.8.0",
},
},
},
],
},
},
],
},
},
],
},
}
`;
exports[`getOutdatedDocumentsQuery should select documents with outdated type migration version 1`] = `
Object {
"bool": Object {
"should": Array [
Object {
"bool": Object {
"must": Array [
Object {
"term": Object {
"type": "dashboard",
},
},
Object {
"bool": Object {
"should": Array [
Object {
"bool": Object {
"must_not": Array [
Object {
"exists": Object {
"field": "typeMigrationVersion",
},
},
Object {
"exists": Object {
"field": "migrationVersion.dashboard",
},
},
],
},
},
Object {
"bool": Object {
"must": Object {
"exists": Object {
"field": "migrationVersion",
},
},
"must_not": Object {
"term": Object {
"migrationVersion.dashboard": "7.7.0",
},
},
},
},
Object {
"range": Object {
"typeMigrationVersion": Object {
"lt": "7.7.0",
},
},
},
],
},
},
],
},
},
],
},
}
`;

View file

@ -39,17 +39,20 @@ describe('buildActiveMigrations', () => {
...parts,
});
const transform = (type: TransformType, version: string): Transform => ({
const transform = (type: TransformType, version: string, deferred?: boolean): Transform => ({
...(deferred != null ? { deferred } : {}),
version,
transformType: type,
transform: jest.fn(),
});
const expectTransform = (type: TransformType, version: string): Transform => ({
version,
transformType: type,
transform: expect.any(Function),
});
const expectTransform = (type: TransformType, version: string, deferred?: boolean): Transform =>
expect.objectContaining({
...(deferred != null ? { deferred } : {}),
version,
transformType: type,
transform: expect.any(Function),
});
const addType = (parts: Partial<SavedObjectsType>) => {
typeRegistry.registerType(createType(parts));
@ -123,7 +126,14 @@ describe('buildActiveMigrations', () => {
migrations: {
'7.12.0': jest.fn(),
'7.16.0': jest.fn(),
'8.3.0': jest.fn(),
'8.3.0': {
transform: jest.fn(),
},
'8.4.0': {
// @ts-expect-error
deferred: true,
transform: jest.fn(),
},
},
});
@ -131,9 +141,10 @@ describe('buildActiveMigrations', () => {
expect(Object.keys(migrations).sort()).toEqual(['foo']);
expect(migrations.foo.transforms).toEqual([
expectTransform(TransformType.Migrate, '7.12.0'),
expectTransform(TransformType.Migrate, '7.16.0'),
expectTransform(TransformType.Migrate, '8.3.0'),
expectTransform(TransformType.Migrate, '7.12.0', false),
expectTransform(TransformType.Migrate, '7.16.0', false),
expectTransform(TransformType.Migrate, '8.3.0', false),
expectTransform(TransformType.Migrate, '8.4.0', true),
]);
});
});
@ -339,4 +350,71 @@ describe('buildActiveMigrations', () => {
]);
});
});
describe('versions', () => {
it('returns the latest migrations versions', () => {
addType({
name: 'foo',
migrations: {
'7.12.0': jest.fn(),
'7.16.0': jest.fn(),
'8.3.0': jest.fn(),
},
});
getCoreTransformsMock.mockReturnValue([
transform(TransformType.Core, '8.8.0'),
transform(TransformType.Core, '8.9.0'),
]);
getReferenceTransformsMock.mockReturnValue([
transform(TransformType.Reference, '7.12.0'),
transform(TransformType.Reference, '7.17.3'),
]);
getConversionTransformsMock.mockReturnValue([
transform(TransformType.Convert, '7.14.0'),
transform(TransformType.Convert, '7.15.0'),
]);
expect(buildMigrations()).toHaveProperty(
'foo.latestVersion',
expect.objectContaining({
[TransformType.Convert]: '7.15.0',
[TransformType.Core]: '8.9.0',
[TransformType.Migrate]: '8.3.0',
[TransformType.Reference]: '7.17.3',
})
);
});
it('returns the latest not deferred migrations versions', () => {
addType({
name: 'foo',
migrations: {
'7.12.0': {
// @ts-expect-error
deferred: true,
transform: jest.fn(),
},
'7.16.0': jest.fn(),
'8.3.0': {
// @ts-expect-error
deferred: true,
transform: jest.fn(),
},
},
});
getCoreTransformsMock.mockReturnValue([
transform(TransformType.Core, '8.7.0', true),
transform(TransformType.Core, '8.8.0'),
transform(TransformType.Core, '8.9.0', true),
]);
expect(buildMigrations()).toHaveProperty(
'foo.immediateVersion',
expect.objectContaining({
[TransformType.Core]: '8.8.0',
[TransformType.Migrate]: '7.16.0',
})
);
});
});
});

View file

@ -36,7 +36,6 @@ export function buildActiveMigrations({
convertVersion?: string;
log: Logger;
}): ActiveMigrations {
const coreTransforms = getCoreTransforms();
const referenceTransforms = getReferenceTransforms(typeRegistry);
return typeRegistry.getAllTypes().reduce((migrations, type) => {
@ -46,7 +45,6 @@ export function buildActiveMigrations({
type,
log,
kibanaVersion,
coreTransforms,
referenceTransforms,
});
@ -64,13 +62,11 @@ export function buildActiveMigrations({
const buildTypeTransforms = ({
type,
log,
coreTransforms,
referenceTransforms,
}: {
type: SavedObjectsType;
kibanaVersion: string;
log: Logger;
coreTransforms: Transform[];
referenceTransforms: Transform[];
}): TypeTransforms => {
const migrationsMap =
@ -79,11 +75,13 @@ const buildTypeTransforms = ({
const migrationTransforms = Object.entries(migrationsMap ?? {}).map<Transform>(
([version, transform]) => ({
version,
deferred: !_.isFunction(transform) && !!transform.deferred,
transform: convertMigrationFunction(version, type, transform, log),
transformType: TransformType.Migrate,
})
);
const coreTransforms = getCoreTransforms({ log, type });
const modelVersionTransforms = getModelVersionTransforms({ log, typeDefinition: type });
const conversionTransforms = getConversionTransforms(type);
@ -96,6 +94,16 @@ const buildTypeTransforms = ({
].sort(transformComparator);
return {
immediateVersion: _.chain(transforms)
.groupBy('transformType')
.mapValues((items) =>
_.chain(items)
.filter(({ deferred }) => !deferred)
.last()
.get('version')
.value()
)
.value() as Record<TransformType, string>,
latestVersion: _.chain(transforms)
.groupBy('transformType')
.mapValues((items) => _.last(items)?.version)

View file

@ -635,7 +635,7 @@ describe('DocumentMigrator', () => {
),
});
migrator.prepareMigrations();
expect(migrator.migrationVersion).toEqual({
expect(migrator.getMigrationVersion()).toEqual({
aaa: '10.4.0',
bbb: '3.2.3',
ccc: '11.0.0',
@ -643,6 +643,47 @@ describe('DocumentMigrator', () => {
});
});
test('extracts the latest non-deferred migration version info', () => {
const migrator = new DocumentMigrator({
...testOpts(),
typeRegistry: createRegistry({
name: 'aaa',
migrations: {
'1.2.3': (doc: SavedObjectUnsanitizedDoc) => doc,
'2.2.1': (doc: SavedObjectUnsanitizedDoc) => doc,
'10.4.0': {
// @ts-expect-error
deferred: true,
transform: (doc: SavedObjectUnsanitizedDoc) => doc,
},
},
}),
});
migrator.prepareMigrations();
expect(migrator.getMigrationVersion({ includeDeferred: false })).toHaveProperty(
'aaa',
'2.2.1'
);
});
test('extracts the latest core migration version info', () => {
const migrator = new DocumentMigrator({
...testOpts(),
typeRegistry: createRegistry({
name: 'aaa',
migrations: {
'1.2.3': (doc: SavedObjectUnsanitizedDoc) => doc,
'2.2.1': (doc: SavedObjectUnsanitizedDoc) => doc,
},
}),
});
migrator.prepareMigrations();
expect(migrator.getMigrationVersion({ migrationType: 'core' })).toHaveProperty(
'aaa',
'8.8.0'
);
});
describe('conversion to multi-namespace type', () => {
it('assumes documents w/ undefined typeMigrationVersion and correct coreMigrationVersion are up to date', () => {
const migrator = new DocumentMigrator({

View file

@ -28,6 +28,20 @@ interface DocumentMigratorOptions {
log: Logger;
}
interface MigrationVersionParams {
/**
* Include deferred migrations in the migrationVersion.
* @default true
*/
includeDeferred?: boolean;
/**
* Migration type to use in the migrationVersion.
* @default 'type'
*/
migrationType?: 'core' | 'type';
}
/**
* Manages transformations of individual documents.
*/
@ -35,19 +49,19 @@ export interface VersionedTransformer {
/**
* Migrates a document to its latest version.
*/
migrate: (doc: SavedObjectUnsanitizedDoc) => SavedObjectUnsanitizedDoc;
migrate(doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc;
/**
* Migrates a document to the latest version and applies type conversions if applicable.
* Also returns any additional document(s) that may have been created during the transformation process.
*/
migrateAndConvert: (doc: SavedObjectUnsanitizedDoc) => SavedObjectUnsanitizedDoc[];
migrateAndConvert(doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc[];
/**
* Converts a document down to the specified version.
*/
transformDown: (
transformDown(
doc: SavedObjectUnsanitizedDoc,
options: { targetTypeVersion: string }
) => SavedObjectUnsanitizedDoc;
): SavedObjectUnsanitizedDoc;
}
/**
@ -68,30 +82,39 @@ export class DocumentMigrator implements VersionedTransformer {
*/
constructor(options: DocumentMigratorOptions) {
this.options = options;
this.migrate = (...args) => this.constructor.prototype.migrate.apply(this, args);
this.migrateAndConvert = (...args) =>
this.constructor.prototype.migrateAndConvert.apply(this, args);
}
/**
* Gets the latest version of each migrate-able property.
* Gets the latest pending version of each type.
* Some migration objects won't have a latest migration version (they only contain reference transforms that are applied from other types).
*/
public get migrationVersion(): SavedObjectsMigrationVersion {
public getMigrationVersion({
includeDeferred = true,
migrationType = 'type',
}: MigrationVersionParams = {}): SavedObjectsMigrationVersion {
if (!this.migrations) {
throw new Error('Migrations are not ready. Make sure prepareMigrations is called first.');
}
return Object.entries(this.migrations).reduce((acc, [prop, { latestVersion }]) => {
// some migration objects won't have a latest migration version (they only contain reference transforms that are applied from other types)
const latestMigrationVersion = maxVersion(latestVersion.migrate, latestVersion.convert);
if (latestMigrationVersion) {
return { ...acc, [prop]: latestMigrationVersion };
}
return acc;
}, {});
return Object.entries(this.migrations).reduce(
(acc, [type, { latestVersion, immediateVersion }]) => {
const version = includeDeferred ? latestVersion : immediateVersion;
const latestMigrationVersion =
migrationType === 'core' ? version.core : maxVersion(version.migrate, version.convert);
return latestMigrationVersion ? { ...acc, [type]: latestMigrationVersion } : acc;
},
{}
);
}
/**
* Prepares active migrations and document transformer function.
*/
public prepareMigrations = () => {
public prepareMigrations() {
const { typeRegistry, kibanaVersion, log, convertVersion } = this.options;
this.migrations = buildActiveMigrations({
typeRegistry,
@ -99,31 +122,31 @@ export class DocumentMigrator implements VersionedTransformer {
log,
convertVersion,
});
};
}
/**
* Migrates a document to the latest version.
*/
public migrate = (doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc => {
public migrate(doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc {
const { document } = this.transform(doc);
return document;
};
}
/**
* Migrates a document to the latest version and applies type conversions if applicable. Also returns any additional document(s) that may
* have been created during the transformation process.
*/
public migrateAndConvert = (doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc[] => {
public migrateAndConvert(doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc[] {
const { document, additionalDocs } = this.transform(doc, { convertNamespaceTypes: true });
return [document, ...additionalDocs];
};
}
public transformDown = (
public transformDown(
doc: SavedObjectUnsanitizedDoc,
options: { targetTypeVersion: string }
): SavedObjectUnsanitizedDoc => {
): SavedObjectUnsanitizedDoc {
if (!this.migrations) {
throw new Error('Migrations are not ready. Make sure prepareMigrations is called first.');
}
@ -136,7 +159,7 @@ export class DocumentMigrator implements VersionedTransformer {
});
const { document } = pipeline.run();
return document;
};
}
private transform(
doc: SavedObjectUnsanitizedDoc,

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
import { isFunction } from 'lodash';
import {
ISavedObjectTypeRegistry,
SavedObjectsType,
@ -16,16 +17,25 @@ import {
LEGACY_URL_ALIAS_TYPE,
LegacyUrlAlias,
} from '@kbn/core-saved-objects-base-server-internal';
import { Logger } from '@kbn/logging';
import { migrations as coreMigrationsMap } from './migrations';
import { type Transform, TransformType } from './types';
import { convertMigrationFunction } from './utils';
/**
* Returns all available core transforms for all object types.
*/
export function getCoreTransforms(): Transform[] {
export function getCoreTransforms({
type,
log,
}: {
type: SavedObjectsType;
log: Logger;
}): Transform[] {
return Object.entries(coreMigrationsMap).map<Transform>(([version, transform]) => ({
version,
transform,
deferred: !isFunction(transform) && !!transform.deferred,
transform: convertMigrationFunction(version, type, transform, log),
transformType: TransformType.Core,
}));
}

View file

@ -6,17 +6,11 @@
* Side Public License, v 1.
*/
import { flow } from 'lodash';
import get from 'lodash/fp/get';
import { TransformFn } from '../types';
import type { SavedObjectMigrationMap } from '@kbn/core-saved-objects-server';
import { mergeSavedObjectMigrations } from '@kbn/core-saved-objects-utils-server';
import { transformMigrationVersion } from './transform_migration_version';
import { transformSetManagedDefault } from './transform_set_managed_default';
export const migrations = {
'8.8.0': flow(
transformMigrationVersion,
// extract transformedDoc from TransformResult as input to next transform
get('transformedDoc'),
transformSetManagedDefault
),
} as Record<string, TransformFn>;
export const migrations: SavedObjectMigrationMap = {
'8.8.0': mergeSavedObjectMigrations(transformMigrationVersion, transformSetManagedDefault),
};

View file

@ -6,12 +6,16 @@
* Side Public License, v 1.
*/
import { unary } from 'lodash';
import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server';
import { transformMigrationVersion } from './transform_migration_version';
const transform = unary(SavedObjectsUtils.getMigrationFunction(transformMigrationVersion));
describe('transformMigrationVersion', () => {
it('should extract the correct version from the `migrationVersion` property', () => {
expect(
transformMigrationVersion({
transform({
id: 'a',
attributes: {},
type: 'something',
@ -20,12 +24,12 @@ describe('transformMigrationVersion', () => {
previous: '2.0.0',
},
})
).toHaveProperty('transformedDoc.typeMigrationVersion', '1.0.0');
).toHaveProperty('typeMigrationVersion', '1.0.0');
});
it('should remove the original `migrationVersion` property', () => {
expect(
transformMigrationVersion({
transform({
id: 'a',
attributes: {},
type: 'something',
@ -34,27 +38,27 @@ describe('transformMigrationVersion', () => {
previous: '2.0.0',
},
})
).not.toHaveProperty('transformedDoc.migrationVersion');
).not.toHaveProperty('migrationVersion');
});
it('should not add `typeMigrationVersion` if there is no `migrationVersion`', () => {
expect(
transformMigrationVersion({
transform({
id: 'a',
attributes: {},
type: 'something',
})
).not.toHaveProperty('transformedDoc.typeMigrationVersion');
).not.toHaveProperty('typeMigrationVersion');
});
it('should add empty `typeMigrationVersion` if there is no related value in `migrationVersion`', () => {
expect(
transformMigrationVersion({
transform({
id: 'a',
attributes: {},
type: 'something',
migrationVersion: {},
})
).toHaveProperty('transformedDoc.typeMigrationVersion', '');
).toHaveProperty('typeMigrationVersion', '');
});
});

View file

@ -6,14 +6,14 @@
* Side Public License, v 1.
*/
import { TransformFn } from '../types';
import type { SavedObjectMigration } from '@kbn/core-saved-objects-server';
export const transformMigrationVersion: TransformFn = ({ migrationVersion, ...doc }) => {
return {
transformedDoc: {
...doc,
...(migrationVersion ? { typeMigrationVersion: migrationVersion[doc.type] ?? '' } : {}),
},
additionalDocs: [],
};
export const transformMigrationVersion: SavedObjectMigration = {
// @todo Remove when deferred migrations are publicly available.
// @ts-expect-error
deferred: true,
transform: ({ migrationVersion, ...doc }) => ({
...doc,
...(migrationVersion ? { typeMigrationVersion: migrationVersion[doc.type] ?? '' } : {}),
}),
};

View file

@ -6,35 +6,39 @@
* Side Public License, v 1.
*/
import { unary } from 'lodash';
import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server';
import { transformSetManagedDefault } from './transform_set_managed_default';
const transform = unary(SavedObjectsUtils.getMigrationFunction(transformSetManagedDefault));
describe('transformAddManaged', () => {
it('should add managed if not defined', () => {
expect(
transformSetManagedDefault({
transform({
id: 'a',
attributes: {},
type: 'something',
})
).toHaveProperty('transformedDoc.managed');
).toHaveProperty('managed');
});
it('should not change managed if already defined', () => {
const docWithManagedFalse = transformSetManagedDefault({
const docWithManagedFalse = transform({
id: 'a',
attributes: {},
type: 'something',
managed: false,
});
const docWithManagedTrue = transformSetManagedDefault({
const docWithManagedTrue = transform({
id: 'a',
attributes: {},
type: 'something',
managed: true,
});
[docWithManagedFalse, docWithManagedTrue].forEach((doc) => {
expect(doc.transformedDoc.managed).toBeDefined();
expect(doc.managed).toBeDefined();
});
expect(docWithManagedFalse.transformedDoc.managed).not.toBeTruthy();
expect(docWithManagedTrue.transformedDoc.managed).toBeTruthy();
expect(docWithManagedFalse.managed).not.toBeTruthy();
expect(docWithManagedTrue.managed).toBeTruthy();
});
});

View file

@ -6,9 +6,11 @@
* Side Public License, v 1.
*/
import { SavedObjectUnsanitizedDoc } from '@kbn/core-saved-objects-server';
import type { SavedObjectMigration } from '@kbn/core-saved-objects-server';
export const transformSetManagedDefault = (doc: SavedObjectUnsanitizedDoc) => ({
transformedDoc: { ...doc, managed: doc.managed ?? false },
additionalDocs: [],
});
export const transformSetManagedDefault: SavedObjectMigration = {
// @todo Remove when deferred migrations are publicly available.
// @ts-expect-error
deferred: true,
transform: ({ managed, ...doc }) => ({ ...doc, managed: managed ?? false }),
};

View file

@ -46,6 +46,7 @@ describe('DocumentMigratorPipeline', () => {
return {
transforms,
immediateVersion: latestVersions(versions),
latestVersion: latestVersions(versions),
};
};

View file

@ -47,6 +47,7 @@ describe('DocumentMigratorPipeline', () => {
return {
transforms,
immediateVersion: latestVersions(versions),
latestVersion: latestVersions(versions),
};
};

View file

@ -19,7 +19,15 @@ export interface ActiveMigrations {
* Structure containing all the required info to perform a type's conversion
*/
export interface TypeTransforms {
/** Derived from the related transforms */
/**
* Latest non-deferred version for each transform type.
* This is the version that will be used to query outdated documents.
*/
immediateVersion: Record<TransformType, string>;
/**
* Latest version for each transform type, including deferred transforms.
* This is the version that will be used to perform the migration.
*/
latestVersion: Record<TransformType, string>;
/** Ordered list of transforms registered for the type **/
transforms: Transform[];
@ -37,6 +45,8 @@ export interface Transform {
transform: TransformFn;
/** The (optional) downward transformation function */
transformDown?: TransformFn;
/** Whether this transform is deferred */
deferred?: boolean;
}
export enum TransformType {

View file

@ -6,9 +6,10 @@
* Side Public License, v 1.
*/
import { isFunction } from 'lodash';
import Semver from 'semver';
import {
SavedObjectMigrationFn,
import type {
SavedObjectMigration,
SavedObjectsType,
SavedObjectUnsanitizedDoc,
} from '@kbn/core-saved-objects-server';
@ -31,7 +32,7 @@ const TRANSFORM_PRIORITY = [
export function convertMigrationFunction(
version: string,
type: SavedObjectsType,
migrationFn: SavedObjectMigrationFn,
migration: SavedObjectMigration,
log: Logger
): TransformFn {
const context = Object.freeze({
@ -43,7 +44,8 @@ export function convertMigrationFunction(
return function tryTransformDoc(doc: SavedObjectUnsanitizedDoc) {
try {
const result = migrationFn(doc, context);
const transformFn = isFunction(migration) ? migration : migration.transform;
const result = transformFn(doc, context);
// A basic check to help migration authors detect basic errors
// (e.g. forgetting to return the transformed doc)

View file

@ -70,15 +70,41 @@ describe('validateTypeMigrations', () => {
);
});
it('validates the migration function', () => {
it('throws on the invalid migration type', () => {
const type = createType({
name: 'foo',
convertToMultiNamespaceTypeVersion: '3.1.1',
namespaceType: 'multiple',
migrations: { '1.2.3': 23 as any },
});
expect(() => validate({ type })).toThrow(/expected a function, but got 23/i);
expect(() => validate({ type })).toThrow(/expected a function or an object/i);
});
it('throws on the invalid migration object', () => {
const type = createType({
name: 'foo',
migrations: {
'1.2.3': {
deferred: false,
transform: 23 as any,
},
},
});
expect(() => validate({ type })).toThrow(/expected a function or an object/i);
});
it('validates the migration object', () => {
const type = createType({
name: 'foo',
migrations: {
'1.2.3': {
deferred: false,
transform: jest.fn(),
},
},
});
expect(() => validate({ type })).not.toThrow();
});
describe('when switchToModelVersionAt is specified', () => {

View file

@ -49,9 +49,9 @@ export function validateTypeMigrations({
`Migrations map for type ${type.name} should be an object like { '2.0.0': (doc) => doc }.`
);
Object.entries(migrationMap).forEach(([version, fn]) => {
Object.entries(migrationMap).forEach(([version, migration]) => {
assertValidSemver(kibanaVersion, version, type.name);
assertValidTransform(fn, version, type.name);
assertValidTransform(migration, version, type.name);
if (type.switchToModelVersionAt && Semver.gte(version, type.switchToModelVersionAt)) {
throw new Error(
`Migration for type ${type.name} for version ${version} registered after switchToModelVersionAt (${type.switchToModelVersionAt})`
@ -169,9 +169,14 @@ const assertValidConvertToMultiNamespaceType = (
}
};
const assertValidTransform = (fn: any, version: string, type: string) => {
if (typeof fn !== 'function') {
throw new Error(`Invalid migration ${type}.${version}: expected a function, but got ${fn}.`);
const assertValidTransform = (migration: any, version: string, type: string) => {
if (
!(typeof migration === 'object' && typeof migration.transform === 'function') &&
typeof migration !== 'function'
) {
throw new Error(
`Invalid migration ${type}.${version}: expected a function or an object, but got ${migration}.`
);
}
};

View file

@ -0,0 +1,26 @@
/*
* 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 { getOutdatedDocumentsQuery } from './get_outdated_documents_query';
describe('getOutdatedDocumentsQuery', () => {
it.each`
coreMigrationVersionPerType | migrationVersionPerType | case
${{}} | ${{}} | ${'should not select documents if there are no migrations'}
${{ dashboard: '8.8.0' }} | ${{}} | ${'should select documents with outdated core migration version'}
${{}} | ${{ dashboard: '7.7.0' }} | ${'should select documents with outdated type migration version'}
${{ dashboard: '8.8.0' }} | ${{ dashboard: '7.7.0' }} | ${'should select documents with outdated both core and type migration versions'}
`('$case', ({ coreMigrationVersionPerType, migrationVersionPerType }) => {
expect(
getOutdatedDocumentsQuery({
coreMigrationVersionPerType,
migrationVersionPerType,
})
).toMatchSnapshot();
});
});

View file

@ -0,0 +1,76 @@
/*
* 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { SavedObjectsMigrationVersion } from '@kbn/core-saved-objects-common';
export interface OutdatedDocumentsQueryParams {
coreMigrationVersionPerType: SavedObjectsMigrationVersion;
migrationVersionPerType: SavedObjectsMigrationVersion;
}
export function getOutdatedDocumentsQuery({
coreMigrationVersionPerType,
migrationVersionPerType,
}: OutdatedDocumentsQueryParams): QueryDslQueryContainer {
const types = [
...new Set([
...Object.keys(coreMigrationVersionPerType),
...Object.keys(migrationVersionPerType),
]).values(),
];
return {
bool: {
should: types.map((type) => ({
bool: {
must: [
{ term: { type } },
{
bool: {
should: [
...(coreMigrationVersionPerType[type]
? [
{
range: {
coreMigrationVersion: { lt: coreMigrationVersionPerType[type] },
},
},
]
: []),
...(migrationVersionPerType[type]
? [
{
bool: {
must_not: [
{ exists: { field: 'typeMigrationVersion' } },
{ exists: { field: `migrationVersion.${type}` } },
],
},
},
{
bool: {
must: { exists: { field: 'migrationVersion' } },
must_not: {
term: { [`migrationVersion.${type}`]: migrationVersionPerType[type] },
},
},
},
{
range: { typeMigrationVersion: { lt: migrationVersionPerType[type] } },
},
]
: []),
],
},
},
],
},
})),
},
};
}

View file

@ -18,6 +18,8 @@ import {
import type { Logger } from '@kbn/logging';
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
import { createInitialState, type CreateInitialStateParams } from './initial_state';
import * as getOutdatedDocumentsQueryModule from './get_outdated_documents_query';
import { getOutdatedDocumentsQuery } from './get_outdated_documents_query';
const mockLogger = loggingSystemMock.create();
@ -40,6 +42,7 @@ const createInitialStateCommonParams = {
dynamic: 'strict',
properties: { my_type: { properties: { title: { type: 'text' } } } },
} as IndexMapping,
coreMigrationVersionPerType: {},
migrationVersionPerType: {},
indexPrefix: '.kibana_task_manager',
migrationsConfig,
@ -347,10 +350,13 @@ describe('createInitialState', () => {
).toEqual(true);
});
it('returns state with an outdatedDocumentsQuery', () => {
jest.spyOn(getOutdatedDocumentsQueryModule, 'getOutdatedDocumentsQuery');
expect(
createInitialState({
...createInitialStateParams,
preMigrationScript: "ctx._id = ctx._source.type + ':' + ctx._id",
coreMigrationVersionPerType: {},
migrationVersionPerType: { my_dashboard: '7.10.1', my_viz: '8.0.0' },
}).outdatedDocumentsQuery
).toMatchInlineSnapshot(`
@ -368,6 +374,22 @@ describe('createInitialState', () => {
Object {
"bool": Object {
"should": Array [
Object {
"bool": Object {
"must_not": Array [
Object {
"exists": Object {
"field": "typeMigrationVersion",
},
},
Object {
"exists": Object {
"field": "migrationVersion.my_dashboard",
},
},
],
},
},
Object {
"bool": Object {
"must": Object {
@ -383,19 +405,10 @@ describe('createInitialState', () => {
},
},
Object {
"bool": Object {
"must_not": Array [
Object {
"exists": Object {
"field": "migrationVersion",
},
},
Object {
"term": Object {
"typeMigrationVersion": "7.10.1",
},
},
],
"range": Object {
"typeMigrationVersion": Object {
"lt": "7.10.1",
},
},
},
],
@ -415,6 +428,22 @@ describe('createInitialState', () => {
Object {
"bool": Object {
"should": Array [
Object {
"bool": Object {
"must_not": Array [
Object {
"exists": Object {
"field": "typeMigrationVersion",
},
},
Object {
"exists": Object {
"field": "migrationVersion.my_viz",
},
},
],
},
},
Object {
"bool": Object {
"must": Object {
@ -430,19 +459,10 @@ describe('createInitialState', () => {
},
},
Object {
"bool": Object {
"must_not": Array [
Object {
"exists": Object {
"field": "migrationVersion",
},
},
Object {
"term": Object {
"typeMigrationVersion": "8.0.0",
},
},
],
"range": Object {
"typeMigrationVersion": Object {
"lt": "8.0.0",
},
},
},
],
@ -455,6 +475,10 @@ describe('createInitialState', () => {
},
}
`);
expect(getOutdatedDocumentsQuery).toHaveBeenCalledWith({
coreMigrationVersionPerType: {},
migrationVersionPerType: { my_dashboard: '7.10.1', my_viz: '8.0.0' },
});
});
it('initializes the `discardUnknownObjects` flag to false if the flag is not provided in the config', () => {

View file

@ -7,28 +7,29 @@
*/
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';
import type { ISavedObjectTypeRegistry } from '@kbn/core-saved-objects-server';
import type {
IndexMapping,
IndexTypesMap,
SavedObjectsMigrationConfigType,
} from '@kbn/core-saved-objects-base-server-internal';
import {
getOutdatedDocumentsQuery,
type OutdatedDocumentsQueryParams,
} from './get_outdated_documents_query';
import type { InitState } from './state';
import { excludeUnusedTypesQuery } from './core';
import { getTempIndexName } from './model/helpers';
export interface CreateInitialStateParams {
export interface CreateInitialStateParams extends OutdatedDocumentsQueryParams {
kibanaVersion: string;
waitForMigrationCompletion: boolean;
mustRelocateDocuments: boolean;
indexTypesMap: IndexTypesMap;
targetMappings: IndexMapping;
preMigrationScript?: string;
migrationVersionPerType: SavedObjectsMigrationVersion;
indexPrefix: string;
migrationsConfig: SavedObjectsMigrationConfigType;
typeRegistry: ISavedObjectTypeRegistry;
@ -46,6 +47,7 @@ export const createInitialState = ({
indexTypesMap,
targetMappings,
preMigrationScript,
coreMigrationVersionPerType,
migrationVersionPerType,
indexPrefix,
migrationsConfig,
@ -53,37 +55,10 @@ export const createInitialState = ({
docLinks,
logger,
}: CreateInitialStateParams): InitState => {
const outdatedDocumentsQuery: QueryDslQueryContainer = {
bool: {
should: Object.entries(migrationVersionPerType).map(([type, latestVersion]) => ({
bool: {
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 } },
],
},
},
],
},
},
],
},
})),
},
};
const outdatedDocumentsQuery = getOutdatedDocumentsQuery({
coreMigrationVersionPerType,
migrationVersionPerType,
});
const reindexTargetMappings: IndexMapping = {
dynamic: false,

View file

@ -18,26 +18,9 @@ import { DocumentMigrator } from './document_migrator';
import { ByteSizeValue } from '@kbn/config-schema';
import { docLinksServiceMock } from '@kbn/core-doc-links-server-mocks';
import { lastValueFrom } from 'rxjs';
import * as runResilientMigratorModule from './run_resilient_migrator';
import { runResilientMigrator } from './run_resilient_migrator';
jest.mock('./run_resilient_migrator', () => {
const actual = jest.requireActual('./run_resilient_migrator');
return {
runResilientMigrator: jest.fn(actual.runResilientMigrator),
};
});
jest.mock('./document_migrator', () => {
return {
// Create a mock for spying on the constructor
DocumentMigrator: jest.fn().mockImplementation((...args) => {
const { DocumentMigrator: RealDocMigrator } = jest.requireActual('./document_migrator');
return new RealDocMigrator(args[0]);
}),
};
});
const mappingsResponseWithoutIndexTypesMap: estypes.IndicesGetMappingResponse = {
'.kibana_8.7.0_001': {
mappings: {
@ -70,8 +53,8 @@ const createRegistry = (types: Array<Partial<SavedObjectsType>>) => {
describe('KibanaMigrator', () => {
beforeEach(() => {
(DocumentMigrator as jest.Mock).mockClear();
(runResilientMigrator as jest.MockedFunction<typeof runResilientMigrator>).mockClear();
jest.restoreAllMocks();
jest.spyOn(runResilientMigratorModule, 'runResilientMigrator');
});
describe('getActiveMappings', () => {
it('returns full index mappings w/ core properties', () => {
@ -110,13 +93,11 @@ describe('KibanaMigrator', () => {
it('calls documentMigrator.migrate', () => {
const options = mockOptions();
const kibanaMigrator = new KibanaMigrator(options);
const mockDocumentMigrator = { migrate: jest.fn() };
// @ts-expect-error `documentMigrator` is readonly.
kibanaMigrator.documentMigrator = mockDocumentMigrator;
jest.spyOn(DocumentMigrator.prototype, 'migrate').mockImplementation((doc) => doc);
const doc = {} as any;
expect(() => kibanaMigrator.migrateDocument(doc)).not.toThrowError();
expect(mockDocumentMigrator.migrate).toBeCalledTimes(1);
expect(DocumentMigrator.prototype.migrate).toBeCalledTimes(1);
});
});

View file

@ -176,7 +176,7 @@ export class KibanaMigrator implements IKibanaMigrator {
});
this.log.debug('Applying registered migrations for the following saved object types:');
Object.entries(this.documentMigrator.migrationVersion)
Object.entries(this.documentMigrator.getMigrationVersion())
.sort(([t1, v1], [t2, v2]) => {
return Semver.compare(v1, v2);
})
@ -243,7 +243,13 @@ export class KibanaMigrator implements IKibanaMigrator {
migrateDoc: this.documentMigrator.migrateAndConvert,
rawDocs,
}),
migrationVersionPerType: this.documentMigrator.migrationVersion,
coreMigrationVersionPerType: this.documentMigrator.getMigrationVersion({
includeDeferred: false,
migrationType: 'core',
}),
migrationVersionPerType: this.documentMigrator.getMigrationVersion({
includeDeferred: false,
}),
indexPrefix: indexName,
migrationsConfig: this.soMigrationsConfig,
typeRegistry: this.typeRegistry,

View file

@ -44,6 +44,7 @@ describe('migrationsStateActionMachine', () => {
'.kibana_cases': ['typeD', 'typeE'],
},
targetMappings: { properties: {} },
coreMigrationVersionPerType: {},
migrationVersionPerType: {},
indexPrefix: '.my-so-index',
migrationsConfig: {

View file

@ -57,6 +57,7 @@ export async function runResilientMigrator({
readyToReindex,
doneReindexing,
transformRawDocs,
coreMigrationVersionPerType,
migrationVersionPerType,
indexPrefix,
migrationsConfig,
@ -74,6 +75,7 @@ export async function runResilientMigrator({
doneReindexing: Defer<any>;
logger: Logger;
transformRawDocs: TransformRawDocs;
coreMigrationVersionPerType: SavedObjectsMigrationVersion;
migrationVersionPerType: SavedObjectsMigrationVersion;
indexPrefix: string;
migrationsConfig: SavedObjectsMigrationConfigType;
@ -87,6 +89,7 @@ export async function runResilientMigrator({
indexTypesMap,
targetMappings,
preMigrationScript,
coreMigrationVersionPerType,
migrationVersionPerType,
indexPrefix,
migrationsConfig,

View file

@ -41,8 +41,10 @@ export type {
SavedObjectsMappingProperties,
} from './src/mapping_definition';
export type {
SavedObjectMigration,
SavedObjectMigrationMap,
SavedObjectMigrationContext,
SavedObjectMigrationParams,
SavedObjectsMigrationLogger,
SavedObjectMigrationFn,
} from './src/migration';

View file

@ -47,6 +47,33 @@ export type SavedObjectMigrationFn<InputAttributes = unknown, MigratedAttributes
context: SavedObjectMigrationContext
) => SavedObjectUnsanitizedDoc<MigratedAttributes>;
/**
* Saved Objects migration with parameters.
* @public
*/
export interface SavedObjectMigrationParams<
InputAttributes = unknown,
MigratedAttributes = unknown
> {
/**
* A flag that can defer the migration until either an object is accessed (read) or if there is another non-deferred migration with a higher version.
* @default false
*/
deferred?: false;
/** {@inheritDoc SavedObjectMigrationFn} */
transform: SavedObjectMigrationFn<InputAttributes, MigratedAttributes>;
}
/**
* Saved Objects migration.
* It can be either a {@link SavedObjectMigrationFn | migration function} or a {@link SavedObjectMigrationParams | migration object}.
* @public
*/
export type SavedObjectMigration<InputAttributes = unknown, MigratedAttributes = unknown> =
| SavedObjectMigrationFn<InputAttributes, MigratedAttributes>
| SavedObjectMigrationParams<InputAttributes, MigratedAttributes>;
/** @public */
export interface SavedObjectsMigrationLogger {
debug: (msg: string) => void;
@ -81,7 +108,7 @@ export interface SavedObjectMigrationContext {
}
/**
* A map of {@link SavedObjectMigrationFn | migration functions} to be used for a given type.
* A map of {@link SavedObjectMigration | migrations} to be used for a given type.
* The map's keys must be valid semver versions, and they cannot exceed the current Kibana version.
*
* For a given document, only migrations with a higher version number than that of the document will be applied.
@ -98,5 +125,5 @@ export interface SavedObjectMigrationContext {
* @public
*/
export interface SavedObjectMigrationMap {
[version: string]: SavedObjectMigrationFn<any, any>;
[version: string]: SavedObjectMigration<any, any>;
}

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
export { mergeSavedObjectMigrationMaps } from './src/merge_migration_maps';
export { mergeSavedObjectMigrations, mergeSavedObjectMigrationMaps } from './src/merge_migrations';
export {
SavedObjectsUtils,
ALL_NAMESPACES_STRING,

View file

@ -1,73 +0,0 @@
/*
* 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 {
SavedObjectMigrationContext,
SavedObjectMigrationMap,
SavedObjectUnsanitizedDoc,
} from '@kbn/core-saved-objects-server';
import { mergeSavedObjectMigrationMaps } from './merge_migration_maps';
describe('mergeSavedObjectMigrationMaps', () => {
const obj1: SavedObjectMigrationMap = {
'7.12.1': (doc, context) => {
context.log.info('');
return {
...doc,
attributes: { ...doc.attributes, counter: doc.attributes.counter + 1 },
};
},
'7.12.2': (doc, context) => {
context.log.info('');
return {
...doc,
attributes: { ...doc.attributes, counter: doc.attributes.counter + 2 },
};
},
};
const obj2: SavedObjectMigrationMap = {
'7.12.0': (doc, context) => {
context.log.info('');
return {
...doc,
attributes: { ...doc.attributes, counter: doc.attributes.counter - 2 },
};
},
'7.12.2': (doc, context) => {
context.log.info('');
return {
...doc,
attributes: { ...doc.attributes, counter: doc.attributes.counter + 2 },
};
},
};
test('correctly merges two saved object migration maps', () => {
const context = { log: { info: jest.fn() } } as unknown as SavedObjectMigrationContext;
const result = mergeSavedObjectMigrationMaps(obj1, obj2);
expect(
result['7.12.0']({ attributes: { counter: 5 } } as SavedObjectUnsanitizedDoc, context)
.attributes.counter
).toEqual(3);
expect(context.log.info).toHaveBeenCalledTimes(1);
expect(
result['7.12.1']({ attributes: { counter: 5 } } as SavedObjectUnsanitizedDoc, context)
.attributes.counter
).toEqual(6);
expect(context.log.info).toHaveBeenCalledTimes(2);
expect(
result['7.12.2']({ attributes: { counter: 5 } } as SavedObjectUnsanitizedDoc, context)
.attributes.counter
).toEqual(9);
expect(context.log.info).toHaveBeenCalledTimes(4);
});
});

View file

@ -1,48 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { mergeWith } from 'lodash';
import type {
SavedObjectMigrationContext,
SavedObjectMigrationFn,
SavedObjectMigrationMap,
SavedObjectUnsanitizedDoc,
} from '@kbn/core-saved-objects-server';
/**
* Merges two saved object migration maps.
*
* If there is a migration for a given version on only one of the maps,
* that migration function will be used:
*
* mergeSavedObjectMigrationMaps({ '1.2.3': f }, { '4.5.6': g }) -> { '1.2.3': f, '4.5.6': g }
*
* If there is a migration for a given version on both maps, the migrations will be composed:
*
* mergeSavedObjectMigrationMaps({ '1.2.3': f }, { '1.2.3': g }) -> { '1.2.3': (doc, context) => f(g(doc, context), context) }
*
* @public
*
* @param map1 - The first map to merge
* @param map2 - The second map to merge
* @returns The merged map {@link SavedObjectMigrationMap}
*/
export const mergeSavedObjectMigrationMaps = (
map1: SavedObjectMigrationMap,
map2: SavedObjectMigrationMap
): SavedObjectMigrationMap => {
const customizer = (objValue: SavedObjectMigrationFn, srcValue: SavedObjectMigrationFn) => {
if (!srcValue || !objValue) {
return srcValue || objValue;
}
return (state: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext) =>
objValue(srcValue(state, context), context);
};
return mergeWith({ ...map1 }, map2, customizer);
};

View file

@ -0,0 +1,133 @@
/*
* 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 {
SavedObjectMigrationContext,
SavedObjectMigrationMap,
SavedObjectMigrationFn,
SavedObjectUnsanitizedDoc,
} from '@kbn/core-saved-objects-server';
import { mergeSavedObjectMigrations, mergeSavedObjectMigrationMaps } from './merge_migrations';
describe('mergeSavedObjectMigrations', () => {
test('merges migration parameters with a migration function', () => {
// @ts-expect-error
expect(mergeSavedObjectMigrations({ deferred: true, transform: jest.fn() }, jest.fn())).toEqual(
{
deferred: false,
transform: expect.any(Function),
}
);
});
test('returns a function on merging two functions', () => {
expect(mergeSavedObjectMigrations(jest.fn(), jest.fn())).toBeInstanceOf(Function);
});
test('merges two deferred migrations', () => {
expect(
mergeSavedObjectMigrations(
// @ts-expect-error
{ deferred: true, transform: jest.fn() },
{ deferred: true, transform: jest.fn() }
)
).toEqual({
deferred: true,
transform: expect.any(Function),
});
});
test('merges two non-deferred migrations', () => {
expect(
mergeSavedObjectMigrations(
{ deferred: false, transform: jest.fn() },
{ deferred: false, transform: jest.fn() }
)
).toEqual({
deferred: false,
transform: expect.any(Function),
});
});
test('merges deferred and non-deferred migrations', () => {
expect(
mergeSavedObjectMigrations(
// @ts-expect-error
{ deferred: true, transform: jest.fn() },
{ deferred: false, transform: jest.fn() }
)
).toEqual({
deferred: false,
transform: expect.any(Function),
});
});
});
describe('mergeSavedObjectMigrationMaps', () => {
test('correctly merges two saved object migration maps', () => {
const obj1: SavedObjectMigrationMap = {
'7.12.1': (doc, context) => {
context.log.info('');
return {
...doc,
attributes: { ...doc.attributes, counter: doc.attributes.counter + 1 },
};
},
'7.12.2': (doc, context) => {
context.log.info('');
return {
...doc,
attributes: { ...doc.attributes, counter: doc.attributes.counter + 2 },
};
},
};
const obj2: SavedObjectMigrationMap = {
'7.12.0': (doc, context) => {
context.log.info('');
return {
...doc,
attributes: { ...doc.attributes, counter: doc.attributes.counter - 2 },
};
},
'7.12.2': (doc, context) => {
context.log.info('');
return {
...doc,
attributes: { ...doc.attributes, counter: doc.attributes.counter + 2 },
};
},
};
const context = { log: { info: jest.fn() } } as unknown as SavedObjectMigrationContext;
const result = mergeSavedObjectMigrationMaps(obj1, obj2);
expect(
(result['7.12.0'] as SavedObjectMigrationFn)(
{ attributes: { counter: 5 } } as SavedObjectUnsanitizedDoc,
context
)
).toHaveProperty('attributes.counter', 3);
expect(context.log.info).toHaveBeenCalledTimes(1);
expect(
(result['7.12.1'] as SavedObjectMigrationFn)(
{ attributes: { counter: 5 } } as SavedObjectUnsanitizedDoc,
context
)
).toHaveProperty('attributes.counter', 6);
expect(context.log.info).toHaveBeenCalledTimes(2);
expect(
(result['7.12.2'] as SavedObjectMigrationFn)(
{ attributes: { counter: 5 } } as SavedObjectUnsanitizedDoc,
context
)
).toHaveProperty('attributes.counter', 9);
expect(context.log.info).toHaveBeenCalledTimes(4);
});
});

View file

@ -0,0 +1,84 @@
/*
* 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 { ary, isFunction, mergeWith } from 'lodash';
import type {
SavedObjectMigrationContext,
SavedObjectMigration,
SavedObjectMigrationMap,
SavedObjectUnsanitizedDoc,
} from '@kbn/core-saved-objects-server';
/**
* Composes two migrations into a single migration.
* ```
* mergeSavedObjectMigrations(outer, inner) -> (doc, context) => outer(inner(doc, context), context) }
* ```
*
* If at least one of the migrations is not deferred, the composed migration will not be deferred.
*
* @public
* @param outer Wrapping migration.
* @param inner Wrapped migration.
* @param rest Additional wrapped migrations to compose.
* @returns The composed migration can be either a function or an object depending on the input migrations.
*/
export function mergeSavedObjectMigrations(
outer: SavedObjectMigration,
inner: SavedObjectMigration,
...rest: SavedObjectMigration[]
): SavedObjectMigration {
if (rest.length) {
return mergeSavedObjectMigrations(
mergeSavedObjectMigrations(outer, inner),
...(rest as [SavedObjectMigration, ...SavedObjectMigration[]])
);
}
if (!inner || !outer) {
return inner || outer;
}
const innerMigration = isFunction(inner) ? { transform: inner } : inner;
const outerMigration = isFunction(outer) ? { transform: outer } : outer;
const merged = {
deferred: !!(innerMigration.deferred && outerMigration.deferred),
transform: (state: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext) =>
outerMigration.transform(innerMigration.transform(state, context), context),
};
if (isFunction(inner) && isFunction(outer)) {
return merged.transform;
}
return merged as SavedObjectMigration;
}
/**
* Merges two saved object migration maps.
*
* If there is a migration for a given version on only one of the maps,
* that migration function will be used:
*
* mergeSavedObjectMigrationMaps({ '1.2.3': f }, { '4.5.6': g }) -> { '1.2.3': f, '4.5.6': g }
*
* If there is a migration for a given version on both maps, the migrations will be composed:
*
* mergeSavedObjectMigrationMaps({ '1.2.3': f }, { '1.2.3': g }) -> { '1.2.3': (doc, context) => f(g(doc, context), context) }
*
* @public
*
* @param map1 - The first map to merge
* @param map2 - The second map to merge
* @returns The merged map {@link SavedObjectMigrationMap}
*/
export function mergeSavedObjectMigrationMaps(
map1: SavedObjectMigrationMap,
map2: SavedObjectMigrationMap
): SavedObjectMigrationMap {
return mergeWith({ ...map1 }, map2, ary(mergeSavedObjectMigrations, 2));
}

View file

@ -107,4 +107,18 @@ describe('SavedObjectsUtils', () => {
expect(mockUuidv5).toHaveBeenCalledWith('namespace:type:oldId', 'DNSUUID');
});
});
describe('#getMigrationFunction', () => {
it('should return the migration function when it is a function', () => {
const migration = jest.fn();
expect(SavedObjectsUtils.getMigrationFunction(migration)).toBe(migration);
});
it('should return the migration function when it is a migration object', () => {
const migration = { transform: jest.fn() };
expect(SavedObjectsUtils.getMigrationFunction(migration)).toBe(migration.transform);
});
});
});

View file

@ -6,11 +6,13 @@
* Side Public License, v 1.
*/
import { isFunction } from 'lodash';
import { v1 as uuidv1, v5 as uuidv5 } from 'uuid';
import type {
SavedObjectsFindOptions,
SavedObjectsFindResponse,
} from '@kbn/core-saved-objects-api-server';
import type { SavedObjectMigration, SavedObjectMigrationFn } from '@kbn/core-saved-objects-server';
export const DEFAULT_NAMESPACE_STRING = 'default';
export const ALL_NAMESPACES_STRING = '*';
@ -95,4 +97,15 @@ export class SavedObjectsUtils {
}
return uuidv5(`${namespace}:${type}:${id}`, uuidv5.DNS); // The uuidv5 namespace constant (uuidv5.DNS) is arbitrary.
}
/**
* Gets the transform function from a migration object.
* @param migration Migration object or a migration function.
* @returns A migration function.
*/
public static getMigrationFunction<InputAttributes, MigratedAttributes>(
migration: SavedObjectMigration<InputAttributes, MigratedAttributes>
): SavedObjectMigrationFn<InputAttributes, MigratedAttributes> {
return isFunction(migration) ? migration : migration.transform;
}
}

View file

@ -0,0 +1,166 @@
/*
* 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 { TestElasticsearchUtils } from '@kbn/core-test-helpers-kbn-server';
import type {
SavedObjectsRawDocSource,
SavedObjectsType,
SavedObjectUnsanitizedDoc,
} from '@kbn/core-saved-objects-server';
import { delay } from '../test_utils';
import '../jest_matchers';
import {
clearLog,
defaultKibanaIndex,
startElasticsearch,
KibanaMigratorTestKit,
getKibanaMigratorTestKit,
} from '../kibana_migrator_test_kit';
describe('deferred migrations', () => {
let client: KibanaMigratorTestKit['client'];
let runMigrations: KibanaMigratorTestKit['runMigrations'];
let savedObjectsRepository: KibanaMigratorTestKit['savedObjectsRepository'];
let server: TestElasticsearchUtils['es'];
let type: SavedObjectsType;
beforeAll(async () => {
server = await startElasticsearch();
});
afterAll(async () => {
await server?.stop();
await delay(10);
});
beforeEach(async () => {
const noop = (doc: SavedObjectUnsanitizedDoc) => doc;
type = {
name: 'some-type',
hidden: false,
namespaceType: 'agnostic',
mappings: {
properties: {
name: { type: 'keyword' },
},
},
migrations: {
'1.0.0': jest.fn(noop),
'2.0.0': jest.fn(noop),
'3.0.0': {
// @ts-expect-error
deferred: true,
transform: jest.fn(noop),
},
'4.0.0': jest.fn(noop),
'5.0.0': {
// @ts-expect-error
deferred: true,
transform: jest.fn(noop),
},
'6.0.0': {
// @ts-expect-error
deferred: true,
transform: jest.fn(noop),
},
},
};
({ client, runMigrations, savedObjectsRepository } = await getKibanaMigratorTestKit({
kibanaIndex: defaultKibanaIndex,
types: [type],
}));
await clearLog();
});
describe.each`
source | expected
${'1.0.0'} | ${'6.0.0'}
${'2.0.0'} | ${'6.0.0'}
${'3.0.0'} | ${'6.0.0'}
${'4.0.0'} | ${'4.0.0'}
${'5.0.0'} | ${'5.0.0'}
${'6.0.0'} | ${'6.0.0'}
`("when source document version is '$source'", ({ source, expected }) => {
let id: string;
let latestVersion: string;
beforeEach(async () => {
id = `${type.name}:test-document-${source}`;
latestVersion = Object.keys(type.migrations!).sort().pop()!;
await client.create<SavedObjectsRawDocSource>({
id: `${type.name}:${id}`,
index: defaultKibanaIndex,
refresh: 'wait_for',
document: {
type: type.name,
references: [],
typeMigrationVersion: source,
coreMigrationVersion: '7.0.0',
},
});
await runMigrations();
});
afterEach(async () => {
await client.delete({
id: `${type.name}:${id}`,
index: defaultKibanaIndex,
refresh: 'wait_for',
});
});
it(`should migrate to '${expected}'`, async () => {
await expect(
client.get({
id: `${type.name}:${id}`,
index: defaultKibanaIndex,
})
).resolves.toHaveProperty('_source.typeMigrationVersion', expected);
});
it('should return the latest version via `repository.get`', async () => {
await expect(savedObjectsRepository.get(type.name, id)).resolves.toHaveProperty(
'typeMigrationVersion',
latestVersion
);
});
it('should return the latest version via `repository.bulkGet`', async () => {
await expect(
savedObjectsRepository.bulkGet([{ id, type: type.name }])
).resolves.toHaveProperty('saved_objects.0.typeMigrationVersion', latestVersion);
});
it('should return the latest version via `repository.resolve`', async () => {
await expect(savedObjectsRepository.resolve(type.name, id)).resolves.toHaveProperty(
'saved_object.typeMigrationVersion',
latestVersion
);
});
it('should return the latest version via `repository.bulkResolve`', async () => {
await expect(
savedObjectsRepository.bulkResolve([{ id, type: type.name }])
).resolves.toHaveProperty(
'resolved_objects.0.saved_object.typeMigrationVersion',
latestVersion
);
});
it('should return the latest version via `repository.find`', async () => {
await expect(savedObjectsRepository.find({ type: type.name })).resolves.toHaveProperty(
'saved_objects.0.typeMigrationVersion',
latestVersion
);
});
});
});

View file

@ -164,9 +164,11 @@ export const getKibanaMigratorTestKit = async ({
}
hasRun = true;
migrator.prepareMigrations();
const migrationResults = await migrator.runMigrations();
await loggingSystem.stop();
return migrationResults;
try {
return await migrator.runMigrations();
} finally {
await loggingSystem.stop();
}
};
const savedObjectsRepository = SavedObjectsRepository.createRepository(

View file

@ -97,7 +97,7 @@ describe('ZDT upgrades - basic downgrade', () => {
it('migrates the documents', async () => {
await createBaseline();
const { runMigrations, client, savedObjectsRepository } = await getKibanaMigratorTestKit({
const { runMigrations, client } = await getKibanaMigratorTestKit({
...getBaseMigratorParams(),
logFilePath,
types: [typeV1],
@ -129,10 +129,9 @@ describe('ZDT upgrades - basic downgrade', () => {
sample_type: '10.2.0',
});
const { saved_objects: sampleDocs } = await savedObjectsRepository.find({
type: 'sample_type',
});
expect(sampleDocs).toHaveLength(5);
await expect(
client.count({ index: '.kibana_1', query: { term: { type: 'sample_type' } } })
).resolves.toHaveProperty('count', 5);
const records = await parseLogFile(logFilePath);

View file

@ -63,7 +63,7 @@ describe('ZDT upgrades - encountering conversion failures', () => {
describe('when discardCorruptObjects is false', () => {
it('fails the migration with an explicit message and keep the documents', async () => {
const { runMigrations, savedObjectsRepository } = await prepareScenario({
const { client, runMigrations } = await prepareScenario({
discardCorruptObjects: false,
});
@ -84,15 +84,13 @@ describe('ZDT upgrades - encountering conversion failures', () => {
const records = await parseLogFile(logFilePath);
expect(records).toContainLogEntry('OUTDATED_DOCUMENTS_SEARCH_READ -> FATAL');
const { saved_objects: sampleADocs } = await savedObjectsRepository.find({
type: 'sample_a',
});
const { saved_objects: sampleBDocs } = await savedObjectsRepository.find({
type: 'sample_b',
});
expect(sampleADocs).toHaveLength(5);
expect(sampleBDocs).toHaveLength(5);
const { kibanaIndex: index } = getBaseMigratorParams();
await expect(
client.count({ index, query: { term: { type: 'sample_a' } } })
).resolves.toHaveProperty('count', 5);
await expect(
client.count({ index, query: { term: { type: 'sample_b' } } })
).resolves.toHaveProperty('count', 5);
});
});

View file

@ -10,6 +10,7 @@ import { SerializableRecord } from '@kbn/utility-types';
import { savedObjectsServiceMock } from '@kbn/core/server/mocks';
import { createEmbeddableSetupMock } from '@kbn/embeddable-plugin/server/mocks';
import { SavedObjectReference, SavedObjectUnsanitizedDoc } from '@kbn/core/server';
import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server';
import { createExtract, createInject } from '../../../common';
import { EmbeddableStateWithType } from '@kbn/embeddable-plugin/common';
@ -47,7 +48,7 @@ const contextMock = savedObjectsServiceMock.createMigrationContext();
describe('dashboard', () => {
describe('7.0.0', () => {
const migration = migrations['7.0.0'];
const migration = SavedObjectsUtils.getMigrationFunction(migrations['7.0.0']);
test('skips error on empty object', () => {
expect(migration({} as SavedObjectUnsanitizedDoc, contextMock)).toMatchInlineSnapshot(`
@ -472,7 +473,7 @@ describe('dashboard', () => {
});
describe('7.10.0 - hidden panel titles', () => {
const migration = migrations['7.17.3'];
const migration = SavedObjectsUtils.getMigrationFunction(migrations['7.17.3']);
const doc: DashboardDoc730ToLatest = {
attributes: {
description: '',
@ -579,7 +580,7 @@ describe('dashboard', () => {
});
describe('7.11.0 - embeddable persistable state extraction', () => {
const migration = migrations['7.11.0'];
const migration = SavedObjectsUtils.getMigrationFunction(migrations['7.11.0']);
const doc: DashboardDoc730ToLatest = {
attributes: {
description: '',
@ -703,7 +704,10 @@ describe('dashboard', () => {
embeddable: newEmbeddableSetupMock,
});
expect(migrationsList['7.13.0']).toBeDefined();
const migratedDoc = migrationsList['7.13.0'](originalDoc, contextMock);
const migratedDoc = SavedObjectsUtils.getMigrationFunction(migrationsList['7.13.0'])(
originalDoc,
contextMock
);
expect(migratedDoc.attributes.panelsJSON).toMatchInlineSnapshot(
`"[{\\"version\\":\\"7.9.3\\",\\"gridData\\":{\\"x\\":0,\\"y\\":0,\\"w\\":24,\\"h\\":15,\\"i\\":\\"0\\"},\\"panelIndex\\":\\"0\\",\\"embeddableConfig\\":{}},{\\"version\\":\\"7.13.0\\",\\"gridData\\":{\\"x\\":24,\\"y\\":0,\\"w\\":24,\\"h\\":15,\\"i\\":\\"1\\"},\\"panelIndex\\":\\"1\\",\\"embeddableConfig\\":{\\"attributes\\":{\\"byValueThing\\":\\"ThisIsByValue\\"},\\"superCoolKey\\":\\"ONLY 4 BY VALUE EMBEDDABLES THANK YOU VERY MUCH\\"}}]"`
);

View file

@ -8,6 +8,7 @@
import { savedObjectsServiceMock } from '@kbn/core/server/mocks';
import { createEmbeddableSetupMock } from '@kbn/embeddable-plugin/server/mocks';
import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server';
import {
DashboardDocPre700,
@ -87,8 +88,8 @@ test('dashboard migration 7.3.0 migrates filters to query on search source when
},
};
const doc700 = migrations['7.0.0'](doc, mockContext);
const newDoc = migrations['7.3.0'](doc700, mockContext);
const doc700 = SavedObjectsUtils.getMigrationFunction(migrations['7.0.0'])(doc, mockContext);
const newDoc = SavedObjectsUtils.getMigrationFunction(migrations['7.3.0'])(doc700, mockContext);
const parsedSearchSource = JSON.parse(newDoc.attributes.kibanaSavedObjectMeta.searchSourceJSON);
expect(parsedSearchSource.filter.length).toBe(0);
@ -119,8 +120,8 @@ test('dashboard migration works when panelsJSON is missing panelIndex', () => {
},
};
const doc700 = migrations['7.0.0'](doc, mockContext);
const newDoc = migrations['7.3.0'](doc700, mockContext);
const doc700 = SavedObjectsUtils.getMigrationFunction(migrations['7.0.0'])(doc, mockContext);
const newDoc = SavedObjectsUtils.getMigrationFunction(migrations['7.3.0'])(doc700, mockContext);
const parsedSearchSource = JSON.parse(newDoc.attributes.kibanaSavedObjectMeta.searchSourceJSON);
expect(parsedSearchSource.filter.length).toBe(0);

View file

@ -57,6 +57,7 @@
"@kbn/saved-objects-management-plugin",
"@kbn/shared-ux-button-toolbar",
"@kbn/core-saved-objects-server",
"@kbn/core-saved-objects-utils-server",
],
"exclude": [
"target/**/*",

View file

@ -16,6 +16,7 @@ import {
import { SavedObject } from '@kbn/core/types';
import { SEARCH_SESSION_TYPE, SearchSessionStatus, SearchStatus } from '../../../common';
import { SavedObjectMigrationContext } from '@kbn/core/server';
import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server';
describe('7.12.0 -> 7.13.0', () => {
const mockCompletedSessionSavedObject: SavedObject<SearchSessionSavedObjectAttributesPre$7$13$0> =
@ -66,7 +67,9 @@ describe('7.12.0 -> 7.13.0', () => {
references: [],
};
const migration = searchSessionSavedObjectMigrations['7.13.0'];
const migration = SavedObjectsUtils.getMigrationFunction(
searchSessionSavedObjectMigrations['7.13.0']
);
test('"completed" is populated from "touched" for completed session', () => {
const migratedCompletedSession = migration(
mockCompletedSessionSavedObject,
@ -138,7 +141,9 @@ describe('7.13.0 -> 7.14.0', () => {
references: [],
};
const migration = searchSessionSavedObjectMigrations['7.14.0'];
const migration = SavedObjectsUtils.getMigrationFunction(
searchSessionSavedObjectMigrations['7.14.0']
);
test('version is populated', () => {
const migratedSession = migration(mockSessionSavedObject, {} as SavedObjectMigrationContext);
@ -169,7 +174,9 @@ describe('7.13.0 -> 7.14.0', () => {
});
describe('7.14.0 -> 8.0.0', () => {
const migration = searchSessionSavedObjectMigrations['8.0.0'];
const migration = SavedObjectsUtils.getMigrationFunction(
searchSessionSavedObjectMigrations['8.0.0']
);
test('Discover app URL generator migrates to locator', () => {
const mockSessionSavedObject: SavedObject<SearchSessionSavedObjectAttributesPre$8$0$0> = {
@ -359,7 +366,9 @@ describe('7.14.0 -> 8.0.0', () => {
});
describe('8.0.0 -> 8.6.0', () => {
const migration = searchSessionSavedObjectMigrations['8.6.0'];
const migration = SavedObjectsUtils.getMigrationFunction(
searchSessionSavedObjectMigrations['8.6.0']
);
const mockSessionSavedObject: SavedObject<SearchSessionSavedObjectAttributesPre$8$6$0> = {
id: 'id',

View file

@ -48,6 +48,7 @@
"@kbn/config-schema",
"@kbn/core-application-browser",
"@kbn/core-saved-objects-server",
"@kbn/core-saved-objects-utils-server",
],
"exclude": [
"target/**/*",

View file

@ -7,6 +7,7 @@
*/
import { SavedObjectMigrationContext, SavedObjectUnsanitizedDoc } from '@kbn/core/server';
import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server';
import { getAllMigrations, searchMigrations } from './search_migrations';
const savedObjectMigrationContext = null as unknown as SavedObjectMigrationContext;
@ -369,7 +370,12 @@ Object {
[versionToTest]: (state) => ({ ...state, migrated: true }),
});
expect(migrations[versionToTest](savedSearch, {} as SavedObjectMigrationContext)).toEqual({
expect(
SavedObjectsUtils.getMigrationFunction(migrations[versionToTest])(
savedSearch,
{} as SavedObjectMigrationContext
)
).toEqual({
attributes: {
kibanaSavedObjectMeta: {
searchSourceJSON: JSON.stringify({
@ -395,7 +401,12 @@ Object {
[versionToTest]: (state) => ({ ...state, migrated: true }),
});
expect(migrations[versionToTest](savedSearch, {} as SavedObjectMigrationContext)).toEqual({
expect(
SavedObjectsUtils.getMigrationFunction(migrations[versionToTest])(
savedSearch,
{} as SavedObjectMigrationContext
)
).toEqual({
attributes: {
kibanaSavedObjectMeta: {
searchSourceJSON: '5',

View file

@ -18,6 +18,7 @@
"@kbn/i18n",
"@kbn/config-schema",
"@kbn/core-saved-objects-server",
"@kbn/core-saved-objects-utils-server",
],
"exclude": [
"target/**/*",

View file

@ -12,6 +12,7 @@ import type {
SavedObjectsType,
SavedObjectUnsanitizedDoc,
} from '@kbn/core/server';
import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server';
import { ServerShortUrlClientFactory } from '..';
import { UrlService, LocatorDefinition } from '../../../common/url_service';
import { LegacyShortUrlLocatorDefinition } from '../../../common/url_service/locators/legacy_short_url_locator';
@ -101,9 +102,9 @@ describe('migrations', () => {
service.locators.create(new FooLocatorDefinition());
const migrationFunction = (type.migrations as () => SavedObjectMigrationMap)()['8.0.0'];
expect(typeof migrationFunction).toBe('function');
const migrationFunction = SavedObjectsUtils.getMigrationFunction(
(type.migrations as () => SavedObjectMigrationMap)()['8.0.0']
);
const doc1: SavedObjectUnsanitizedDoc<ShortUrlSavedObjectAttributes> = {
id: 'foo',

View file

@ -13,6 +13,7 @@
"@kbn/i18n-react",
"@kbn/config-schema",
"@kbn/core-custom-branding-browser",
"@kbn/core-saved-objects-utils-server",
],
"exclude": [
"target/**/*",

View file

@ -12,6 +12,7 @@ import {
SavedObjectMigrationFn,
SavedObjectUnsanitizedDoc,
} from '@kbn/core/server';
import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server';
const savedObjectMigrationContext = null as unknown as SavedObjectMigrationContext;
@ -64,7 +65,7 @@ describe('migration visualization', () => {
describe('6.7.2', () => {
const migrate = (doc: any) =>
visualizationSavedObjectTypeMigrations['6.7.2'](
SavedObjectsUtils.getMigrationFunction(visualizationSavedObjectTypeMigrations['6.7.2'])(
doc as Parameters<SavedObjectMigrationFn>[0],
savedObjectMigrationContext
);
@ -198,7 +199,7 @@ describe('migration visualization', () => {
describe('7.0.0', () => {
const migrate = (doc: any) =>
visualizationSavedObjectTypeMigrations['7.0.0'](
SavedObjectsUtils.getMigrationFunction(visualizationSavedObjectTypeMigrations['7.0.0'])(
doc as Parameters<SavedObjectMigrationFn>[0],
savedObjectMigrationContext
);
@ -783,7 +784,7 @@ describe('migration visualization', () => {
describe('7.2.0', () => {
describe('date histogram custom interval removal', () => {
const migrate = (doc: any) =>
visualizationSavedObjectTypeMigrations['7.2.0'](
SavedObjectsUtils.getMigrationFunction(visualizationSavedObjectTypeMigrations['7.2.0'])(
doc as Parameters<SavedObjectMigrationFn>[0],
savedObjectMigrationContext
);
@ -935,7 +936,7 @@ describe('migration visualization', () => {
} as unknown as SavedObjectMigrationContext;
const migrate = (doc: any) =>
visualizationSavedObjectTypeMigrations['7.3.0'](
SavedObjectsUtils.getMigrationFunction(visualizationSavedObjectTypeMigrations['7.3.0'])(
doc as Parameters<SavedObjectMigrationFn>[0],
logger
);
@ -1126,7 +1127,7 @@ describe('migration visualization', () => {
describe('7.3.0 tsvb', () => {
const migrate = (doc: any) =>
visualizationSavedObjectTypeMigrations['7.3.0'](
SavedObjectsUtils.getMigrationFunction(visualizationSavedObjectTypeMigrations['7.3.0'])(
doc as Parameters<SavedObjectMigrationFn>[0],
savedObjectMigrationContext
);
@ -1222,7 +1223,7 @@ describe('migration visualization', () => {
describe('7.3.1', () => {
const migrate = (doc: any) =>
visualizationSavedObjectTypeMigrations['7.3.1'](
SavedObjectsUtils.getMigrationFunction(visualizationSavedObjectTypeMigrations['7.3.1'])(
doc as Parameters<SavedObjectMigrationFn>[0],
savedObjectMigrationContext
);
@ -1266,7 +1267,7 @@ describe('migration visualization', () => {
describe('7.4.2 tsvb split_filters migration', () => {
const migrate = (doc: any) =>
visualizationSavedObjectTypeMigrations['7.4.2'](
SavedObjectsUtils.getMigrationFunction(visualizationSavedObjectTypeMigrations['7.4.2'])(
doc as Parameters<SavedObjectMigrationFn>[0],
savedObjectMigrationContext
);
@ -1399,7 +1400,7 @@ describe('migration visualization', () => {
describe('7.7.0 tsvb opperator typo migration', () => {
const migrate = (doc: any) =>
visualizationSavedObjectTypeMigrations['7.7.0'](
SavedObjectsUtils.getMigrationFunction(visualizationSavedObjectTypeMigrations['7.7.0'])(
doc as Parameters<SavedObjectMigrationFn>[0],
savedObjectMigrationContext
);
@ -1506,7 +1507,7 @@ describe('migration visualization', () => {
describe('7.9.3', () => {
const migrate = (doc: any) =>
visualizationSavedObjectTypeMigrations['7.9.3'](
SavedObjectsUtils.getMigrationFunction(visualizationSavedObjectTypeMigrations['7.9.3'])(
doc as Parameters<SavedObjectMigrationFn>[0],
savedObjectMigrationContext
);
@ -1518,7 +1519,7 @@ describe('migration visualization', () => {
describe('7.8.0 tsvb split_color_mode', () => {
const migrate = (doc: any) =>
visualizationSavedObjectTypeMigrations['7.8.0'](
SavedObjectsUtils.getMigrationFunction(visualizationSavedObjectTypeMigrations['7.8.0'])(
doc as Parameters<SavedObjectMigrationFn>[0],
savedObjectMigrationContext
);
@ -1576,7 +1577,7 @@ describe('migration visualization', () => {
describe('7.10.0 tsvb filter_ratio migration', () => {
const migrate = (doc: any) =>
visualizationSavedObjectTypeMigrations['7.10.0'](
SavedObjectsUtils.getMigrationFunction(visualizationSavedObjectTypeMigrations['7.10.0'])(
doc as Parameters<SavedObjectMigrationFn>[0],
savedObjectMigrationContext
);
@ -1610,7 +1611,7 @@ describe('migration visualization', () => {
describe('7.10.0 remove tsvb search source', () => {
const migrate = (doc: any) =>
visualizationSavedObjectTypeMigrations['7.10.0'](
SavedObjectsUtils.getMigrationFunction(visualizationSavedObjectTypeMigrations['7.10.0'])(
doc as Parameters<SavedObjectMigrationFn>[0],
savedObjectMigrationContext
);
@ -1650,7 +1651,7 @@ describe('migration visualization', () => {
describe('7.11.0 Data table vis - enable toolbar', () => {
const migrate = (doc: any) =>
visualizationSavedObjectTypeMigrations['7.11.0'](
SavedObjectsUtils.getMigrationFunction(visualizationSavedObjectTypeMigrations['7.11.0'])(
doc as Parameters<SavedObjectMigrationFn>[0],
savedObjectMigrationContext
);
@ -1672,7 +1673,7 @@ describe('migration visualization', () => {
describe('7.12.0 update vislib visualization defaults', () => {
const migrate = (doc: any) =>
visualizationSavedObjectTypeMigrations['7.12.0'](
SavedObjectsUtils.getMigrationFunction(visualizationSavedObjectTypeMigrations['7.12.0'])(
doc as Parameters<SavedObjectMigrationFn>[0],
savedObjectMigrationContext
);
@ -1817,7 +1818,7 @@ describe('migration visualization', () => {
describe('7.12.0 update "schema" in aggregations', () => {
const migrate = (doc: any) =>
visualizationSavedObjectTypeMigrations['7.12.0'](
SavedObjectsUtils.getMigrationFunction(visualizationSavedObjectTypeMigrations['7.12.0'])(
doc as Parameters<SavedObjectMigrationFn>[0],
savedObjectMigrationContext
);
@ -1958,7 +1959,7 @@ describe('migration visualization', () => {
describe('7.13.0 tsvb hide Last value indicator by default', () => {
const migrate = (doc: any) =>
visualizationSavedObjectTypeMigrations['7.13.0'](
SavedObjectsUtils.getMigrationFunction(visualizationSavedObjectTypeMigrations['7.13.0'])(
doc as Parameters<SavedObjectMigrationFn>[0],
savedObjectMigrationContext
);
@ -1990,7 +1991,7 @@ describe('migration visualization', () => {
describe('7.13.0 tsvb - remove default_index_pattern and default_timefield from Model', () => {
const migrate = (doc: any) =>
visualizationSavedObjectTypeMigrations['7.13.0'](
SavedObjectsUtils.getMigrationFunction(visualizationSavedObjectTypeMigrations['7.13.0'])(
doc as Parameters<SavedObjectMigrationFn>[0],
savedObjectMigrationContext
);
@ -2014,13 +2015,13 @@ describe('migration visualization', () => {
describe('7.13.0 and 7.13.1 tsvb migrations can run twice', () => {
const migrate = (doc: any) =>
visualizationSavedObjectTypeMigrations['7.13.0'](
SavedObjectsUtils.getMigrationFunction(visualizationSavedObjectTypeMigrations['7.13.0'])(
doc as Parameters<SavedObjectMigrationFn>[0],
savedObjectMigrationContext
);
const migrateAgain = (doc: any) =>
visualizationSavedObjectTypeMigrations['7.13.1'](
SavedObjectsUtils.getMigrationFunction(visualizationSavedObjectTypeMigrations['7.13.1'])(
doc as Parameters<SavedObjectMigrationFn>[0],
savedObjectMigrationContext
);
@ -2054,7 +2055,7 @@ describe('migration visualization', () => {
describe('7.14.0 tsvb - add empty value rule to savedObjects with less and greater then zero rules', () => {
const migrate = (doc: any) =>
visualizationSavedObjectTypeMigrations['7.14.0'](
SavedObjectsUtils.getMigrationFunction(visualizationSavedObjectTypeMigrations['7.14.0'])(
doc as Parameters<SavedObjectMigrationFn>[0],
savedObjectMigrationContext
);
@ -2151,7 +2152,7 @@ describe('migration visualization', () => {
describe('7.14.0 tsvb - add drop last bucket into TSVB model', () => {
const migrate = (doc: any) =>
visualizationSavedObjectTypeMigrations['7.14.0'](
SavedObjectsUtils.getMigrationFunction(visualizationSavedObjectTypeMigrations['7.14.0'])(
doc as Parameters<SavedObjectMigrationFn>[0],
savedObjectMigrationContext
);
@ -2232,7 +2233,7 @@ describe('migration visualization', () => {
describe('7.14.0 update pie visualization defaults', () => {
const migrate = (doc: any) =>
visualizationSavedObjectTypeMigrations['7.14.0'](
SavedObjectsUtils.getMigrationFunction(visualizationSavedObjectTypeMigrations['7.14.0'])(
doc as Parameters<SavedObjectMigrationFn>[0],
savedObjectMigrationContext
);
@ -2280,7 +2281,7 @@ describe('migration visualization', () => {
describe('7.14.0 replaceIndexPatternReference', () => {
const migrate = (doc: any) =>
visualizationSavedObjectTypeMigrations['7.14.0'](
SavedObjectsUtils.getMigrationFunction(visualizationSavedObjectTypeMigrations['7.14.0'])(
doc as Parameters<SavedObjectMigrationFn>[0],
savedObjectMigrationContext
);
@ -2310,7 +2311,7 @@ describe('migration visualization', () => {
describe('7.14.0 update tagcloud defaults', () => {
const migrate = (doc: any) =>
visualizationSavedObjectTypeMigrations['7.14.0'](
SavedObjectsUtils.getMigrationFunction(visualizationSavedObjectTypeMigrations['7.14.0'])(
doc as Parameters<SavedObjectMigrationFn>[0],
savedObjectMigrationContext
);
@ -2351,7 +2352,7 @@ describe('migration visualization', () => {
describe('8.0.0 removeMarkdownLessFromTSVB', () => {
const migrate = (doc: any) =>
visualizationSavedObjectTypeMigrations['8.0.0'](
SavedObjectsUtils.getMigrationFunction(visualizationSavedObjectTypeMigrations['8.0.0'])(
doc as Parameters<SavedObjectMigrationFn>[0],
savedObjectMigrationContext
);
@ -2383,13 +2384,13 @@ describe('migration visualization', () => {
describe('7.17.0 tsvb - add drop last bucket into TSVB model', () => {
const migrate = (doc: any) =>
visualizationSavedObjectTypeMigrations['7.14.0'](
SavedObjectsUtils.getMigrationFunction(visualizationSavedObjectTypeMigrations['7.14.0'])(
doc as Parameters<SavedObjectMigrationFn>[0],
savedObjectMigrationContext
);
const migrateAgain = (doc: any) =>
visualizationSavedObjectTypeMigrations['7.17.0'](
SavedObjectsUtils.getMigrationFunction(visualizationSavedObjectTypeMigrations['7.17.0'])(
doc as Parameters<SavedObjectMigrationFn>[0],
savedObjectMigrationContext
);
@ -2457,7 +2458,10 @@ describe('migration visualization', () => {
});
expect(
visMigrations[versionToTest](visualizationDoc, {} as SavedObjectMigrationContext)
SavedObjectsUtils.getMigrationFunction(visMigrations[versionToTest])(
visualizationDoc,
{} as SavedObjectMigrationContext
)
).toEqual({
attributes: {
kibanaSavedObjectMeta: {
@ -2485,7 +2489,10 @@ describe('migration visualization', () => {
});
expect(
visMigrations[versionToTest](visualizationDoc, {} as SavedObjectMigrationContext)
SavedObjectsUtils.getMigrationFunction(visMigrations[versionToTest])(
visualizationDoc,
{} as SavedObjectMigrationContext
)
).toEqual({
attributes: {
kibanaSavedObjectMeta: {
@ -2543,7 +2550,7 @@ describe('migration visualization', () => {
},
});
const migrate = (doc: any) =>
visualizationSavedObjectTypeMigrations['8.1.0'](
SavedObjectsUtils.getMigrationFunction(visualizationSavedObjectTypeMigrations['8.1.0'])(
doc as Parameters<SavedObjectMigrationFn>[0],
savedObjectMigrationContext
);
@ -2580,7 +2587,7 @@ describe('migration visualization', () => {
},
});
const migrate = (doc: any) =>
visualizationSavedObjectTypeMigrations['8.3.0'](
SavedObjectsUtils.getMigrationFunction(visualizationSavedObjectTypeMigrations['8.3.0'])(
doc as Parameters<SavedObjectMigrationFn>[0],
savedObjectMigrationContext
);
@ -2626,7 +2633,7 @@ describe('migration visualization', () => {
describe('8.5.0 tsvb - remove exclamation circle icon', () => {
const migrate = (doc: any) =>
visualizationSavedObjectTypeMigrations['8.5.0'](
SavedObjectsUtils.getMigrationFunction(visualizationSavedObjectTypeMigrations['8.5.0'])(
doc as Parameters<SavedObjectMigrationFn>[0],
savedObjectMigrationContext
);

View file

@ -56,7 +56,8 @@
"@kbn/content-management-plugin",
"@kbn/core-saved-objects-api-server",
"@kbn/object-versioning",
"@kbn/core-saved-objects-server"
"@kbn/core-saved-objects-server",
"@kbn/core-saved-objects-utils-server"
],
"exclude": [
"target/**/*",

View file

@ -6,12 +6,14 @@
* Side Public License, v 1.
*/
import { MAIN_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server';
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const kibanaServer = getService('kibanaServer');
const es = getService('es');
const BULK_REQUESTS = [
{
@ -265,5 +267,31 @@ export default function ({ getService }: FtrProviderContext) {
expect(resp.body.saved_objects[2].managed).not.to.be.ok();
expect(resp.body.saved_objects[3].managed).to.be.ok();
}));
it('should migrate saved object before returning', async () => {
await es.update({
index: MAIN_SAVED_OBJECT_INDEX,
id: 'config:7.0.0-alpha1',
doc: {
coreMigrationVersion: '7.0.0',
typeMigrationVersion: '7.0.0',
},
});
const { body } = await supertest
.post(`/api/saved_objects/_bulk_get`)
.send([
{
type: 'config',
id: '7.0.0-alpha1',
},
])
.expect(200);
expect(body.saved_objects[0].coreMigrationVersion).to.be.ok();
expect(body.saved_objects[0].coreMigrationVersion).not.to.be('7.0.0');
expect(body.saved_objects[0].typeMigrationVersion).to.be.ok();
expect(body.saved_objects[0].typeMigrationVersion).not.to.be('7.0.0');
});
});
}

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
import { MAIN_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server';
import expect from '@kbn/expect';
import { SavedObject } from '@kbn/core/server';
import { FtrProviderContext } from '../../ftr_provider_context';
@ -13,6 +14,7 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const kibanaServer = getService('kibanaServer');
const es = getService('es');
const SPACE_ID = 'ftr-so-find';
const UUID_PATTERN = new RegExp(
/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i
@ -52,6 +54,27 @@ export default function ({ getService }: FtrProviderContext) {
expect(resp.body.saved_objects[0].typeMigrationVersion).to.be.ok();
}));
it('should migrate saved object before returning', async () => {
await es.update({
index: MAIN_SAVED_OBJECT_INDEX,
id: `${SPACE_ID}:config:7.0.0-alpha1`,
doc: {
coreMigrationVersion: '7.0.0',
typeMigrationVersion: '7.0.0',
},
});
const { body } = await supertest
.get(`/s/${SPACE_ID}/api/saved_objects/_find?type=config`)
.expect(200);
expect(body.saved_objects.map((so: { id: string }) => so.id)).to.eql(['7.0.0-alpha1']);
expect(body.saved_objects[0].coreMigrationVersion).to.be.ok();
expect(body.saved_objects[0].coreMigrationVersion).not.to.be('7.0.0');
expect(body.saved_objects[0].typeMigrationVersion).to.be.ok();
expect(body.saved_objects[0].typeMigrationVersion).not.to.be('7.0.0');
});
describe('unknown type', () => {
it('should return 200 with empty response', async () =>
await supertest

View file

@ -6,10 +6,12 @@
* Side Public License, v 1.
*/
import { MAIN_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server';
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
const es = getService('es');
const supertest = getService('supertest');
const kibanaServer = getService('kibanaServer');
@ -113,6 +115,24 @@ export default function ({ getService }: FtrProviderContext) {
});
});
it('should migrate saved object before returning', async () => {
await es.update({
index: MAIN_SAVED_OBJECT_INDEX,
id: 'config:7.0.0-alpha1',
doc: {
coreMigrationVersion: '7.0.0',
typeMigrationVersion: '7.0.0',
},
});
const { body } = await supertest.get(`/api/saved_objects/config/7.0.0-alpha1`).expect(200);
expect(body.coreMigrationVersion).to.be.ok();
expect(body.coreMigrationVersion).not.to.be('7.0.0');
expect(body.typeMigrationVersion).to.be.ok();
expect(body.typeMigrationVersion).not.to.be('7.0.0');
});
describe('doc does not exist', () => {
it('should return same generic error as when index does not exist', async () =>
await supertest

View file

@ -6,10 +6,12 @@
* Side Public License, v 1.
*/
import { MAIN_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server';
import expect from '@kbn/expect';
import type { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
const es = getService('es');
const supertest = getService('supertest');
const kibanaServer = getService('kibanaServer');
@ -70,6 +72,26 @@ export default function ({ getService }: FtrProviderContext) {
expect(resp.body.saved_object.managed).to.not.be.ok();
}));
it('should migrate saved object before returning', async () => {
await es.update({
index: MAIN_SAVED_OBJECT_INDEX,
id: 'config:7.0.0-alpha1',
doc: {
coreMigrationVersion: '7.0.0',
typeMigrationVersion: '7.0.0',
},
});
const { body } = await supertest
.get(`/api/saved_objects/resolve/config/7.0.0-alpha1`)
.expect(200);
expect(body.saved_object.coreMigrationVersion).to.be.ok();
expect(body.saved_object.coreMigrationVersion).not.to.be('7.0.0');
expect(body.saved_object.typeMigrationVersion).to.be.ok();
expect(body.saved_object.typeMigrationVersion).not.to.be('7.0.0');
});
describe('doc does not exist', () => {
it('should return same generic error as when index does not exist', async () =>
await supertest

View file

@ -14,6 +14,7 @@ import { ActionTaskParams } from '../types';
import { SavedObjectReference, SavedObjectUnsanitizedDoc } from '@kbn/core/server';
import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
import { migrationMocks } from '@kbn/core/server/mocks';
import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server';
const context = migrationMocks.createContext();
const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup();
@ -38,10 +39,9 @@ describe('successful migrations', () => {
describe('7.16.0', () => {
test('adds actionId to references array if actionId is not preconfigured', () => {
const migration716 = getActionTaskParamsMigrations(
encryptedSavedObjectsSetup,
preconfiguredActions
)['7.16.0'];
const migration716 = SavedObjectsUtils.getMigrationFunction(
getActionTaskParamsMigrations(encryptedSavedObjectsSetup, preconfiguredActions)['7.16.0']
);
const actionTaskParam = getMockData();
const migratedActionTaskParam = migration716(actionTaskParam, context);
expect(migratedActionTaskParam).toEqual({
@ -57,10 +57,9 @@ describe('successful migrations', () => {
});
test('does not add actionId to references array if actionId is preconfigured', () => {
const migration716 = getActionTaskParamsMigrations(
encryptedSavedObjectsSetup,
preconfiguredActions
)['7.16.0'];
const migration716 = SavedObjectsUtils.getMigrationFunction(
getActionTaskParamsMigrations(encryptedSavedObjectsSetup, preconfiguredActions)['7.16.0']
);
const actionTaskParam = getMockData({ actionId: 'my-slack1' });
const migratedActionTaskParam = migration716(actionTaskParam, context);
expect(migratedActionTaskParam).toEqual({
@ -70,10 +69,9 @@ describe('successful migrations', () => {
});
test('handles empty relatedSavedObjects array', () => {
const migration716 = getActionTaskParamsMigrations(
encryptedSavedObjectsSetup,
preconfiguredActions
)['7.16.0'];
const migration716 = SavedObjectsUtils.getMigrationFunction(
getActionTaskParamsMigrations(encryptedSavedObjectsSetup, preconfiguredActions)['7.16.0']
);
const actionTaskParam = getMockData({ relatedSavedObjects: [] });
const migratedActionTaskParam = migration716(actionTaskParam, context);
expect(migratedActionTaskParam).toEqual({
@ -93,10 +91,9 @@ describe('successful migrations', () => {
});
test('adds actionId and relatedSavedObjects to references array', () => {
const migration716 = getActionTaskParamsMigrations(
encryptedSavedObjectsSetup,
preconfiguredActions
)['7.16.0'];
const migration716 = SavedObjectsUtils.getMigrationFunction(
getActionTaskParamsMigrations(encryptedSavedObjectsSetup, preconfiguredActions)['7.16.0']
);
const actionTaskParam = getMockData({
relatedSavedObjects: [
{
@ -137,10 +134,9 @@ describe('successful migrations', () => {
});
test('only adds relatedSavedObjects to references array if action is preconfigured', () => {
const migration716 = getActionTaskParamsMigrations(
encryptedSavedObjectsSetup,
preconfiguredActions
)['7.16.0'];
const migration716 = SavedObjectsUtils.getMigrationFunction(
getActionTaskParamsMigrations(encryptedSavedObjectsSetup, preconfiguredActions)['7.16.0']
);
const actionTaskParam = getMockData({
actionId: 'my-slack1',
relatedSavedObjects: [
@ -177,10 +173,9 @@ describe('successful migrations', () => {
});
test('adds actionId and multiple relatedSavedObjects to references array', () => {
const migration716 = getActionTaskParamsMigrations(
encryptedSavedObjectsSetup,
preconfiguredActions
)['7.16.0'];
const migration716 = SavedObjectsUtils.getMigrationFunction(
getActionTaskParamsMigrations(encryptedSavedObjectsSetup, preconfiguredActions)['7.16.0']
);
const actionTaskParam = getMockData({
relatedSavedObjects: [
{
@ -236,10 +231,9 @@ describe('successful migrations', () => {
});
test('does not overwrite existing references', () => {
const migration716 = getActionTaskParamsMigrations(
encryptedSavedObjectsSetup,
preconfiguredActions
)['7.16.0'];
const migration716 = SavedObjectsUtils.getMigrationFunction(
getActionTaskParamsMigrations(encryptedSavedObjectsSetup, preconfiguredActions)['7.16.0']
);
const actionTaskParam = getMockData(
{
relatedSavedObjects: [
@ -294,10 +288,9 @@ describe('successful migrations', () => {
});
test('does not overwrite existing references if relatedSavedObjects is undefined', () => {
const migration716 = getActionTaskParamsMigrations(
encryptedSavedObjectsSetup,
preconfiguredActions
)['7.16.0'];
const migration716 = SavedObjectsUtils.getMigrationFunction(
getActionTaskParamsMigrations(encryptedSavedObjectsSetup, preconfiguredActions)['7.16.0']
);
const actionTaskParam = getMockData({}, [
{
id: 'existing-ref-id',
@ -324,10 +317,9 @@ describe('successful migrations', () => {
});
test('does not overwrite existing references if relatedSavedObjects is empty', () => {
const migration716 = getActionTaskParamsMigrations(
encryptedSavedObjectsSetup,
preconfiguredActions
)['7.16.0'];
const migration716 = SavedObjectsUtils.getMigrationFunction(
getActionTaskParamsMigrations(encryptedSavedObjectsSetup, preconfiguredActions)['7.16.0']
);
const actionTaskParam = getMockData({ relatedSavedObjects: [] }, [
{
id: 'existing-ref-id',
@ -360,7 +352,9 @@ describe('successful migrations', () => {
describe('8.0.0', () => {
test('no op migration for rules SO', () => {
const migration800 = getActionTaskParamsMigrations(encryptedSavedObjectsSetup, [])['8.0.0'];
const migration800 = SavedObjectsUtils.getMigrationFunction(
getActionTaskParamsMigrations(encryptedSavedObjectsSetup, [])['8.0.0']
);
const actionTaskParam = getMockData();
expect(migration800(actionTaskParam, context)).toEqual(actionTaskParam);
});
@ -377,10 +371,9 @@ describe('handles errors during migrations', () => {
describe('7.16.0 throws if migration fails', () => {
test('should show the proper exception', () => {
const migration716 = getActionTaskParamsMigrations(
encryptedSavedObjectsSetup,
preconfiguredActions
)['7.16.0'];
const migration716 = SavedObjectsUtils.getMigrationFunction(
getActionTaskParamsMigrations(encryptedSavedObjectsSetup, preconfiguredActions)['7.16.0']
);
const actionTaskParam = getMockData();
expect(() => {
migration716(actionTaskParam, context);

View file

@ -11,6 +11,7 @@ import { RawAction } from '../types';
import { SavedObjectUnsanitizedDoc } from '@kbn/core/server';
import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
import { migrationMocks } from '@kbn/core/server/mocks';
import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server';
const context = migrationMocks.createContext();
const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup();
@ -23,7 +24,9 @@ describe('successful migrations', () => {
describe('7.10.0', () => {
test('add hasAuth config property for .email actions', () => {
const migration710 = getActionsMigrations(encryptedSavedObjectsSetup)['7.10.0'];
const migration710 = SavedObjectsUtils.getMigrationFunction(
getActionsMigrations(encryptedSavedObjectsSetup)['7.10.0']
);
const action = getMockDataForEmail({});
const migratedAction = migration710(action, context);
expect(migratedAction.attributes.config).toEqual({
@ -41,7 +44,9 @@ describe('successful migrations', () => {
});
test('rename cases configuration object', () => {
const migration710 = getActionsMigrations(encryptedSavedObjectsSetup)['7.10.0'];
const migration710 = SavedObjectsUtils.getMigrationFunction(
getActionsMigrations(encryptedSavedObjectsSetup)['7.10.0']
);
const action = getCasesMockData({});
const migratedAction = migration710(action, context);
expect(migratedAction.attributes.config).toEqual({
@ -61,7 +66,9 @@ describe('successful migrations', () => {
describe('7.11.0', () => {
test('add hasAuth = true for .webhook actions with user and password', () => {
const migration711 = getActionsMigrations(encryptedSavedObjectsSetup)['7.11.0'];
const migration711 = SavedObjectsUtils.getMigrationFunction(
getActionsMigrations(encryptedSavedObjectsSetup)['7.11.0']
);
const action = getMockDataForWebhook({}, true);
expect(migration711(action, context)).toMatchObject({
...action,
@ -75,7 +82,9 @@ describe('successful migrations', () => {
});
test('add hasAuth = false for .webhook actions without user and password', () => {
const migration711 = getActionsMigrations(encryptedSavedObjectsSetup)['7.11.0'];
const migration711 = SavedObjectsUtils.getMigrationFunction(
getActionsMigrations(encryptedSavedObjectsSetup)['7.11.0']
);
const action = getMockDataForWebhook({}, false);
expect(migration711(action, context)).toMatchObject({
...action,
@ -88,7 +97,9 @@ describe('successful migrations', () => {
});
});
test('remove cases mapping object', () => {
const migration711 = getActionsMigrations(encryptedSavedObjectsSetup)['7.11.0'];
const migration711 = SavedObjectsUtils.getMigrationFunction(
getActionsMigrations(encryptedSavedObjectsSetup)['7.11.0']
);
const action = getMockData({
config: { incidentConfiguration: { mapping: [] }, isCaseOwned: true, another: 'value' },
});
@ -106,7 +117,9 @@ describe('successful migrations', () => {
describe('7.14.0', () => {
test('add isMissingSecrets property for actions', () => {
const migration714 = getActionsMigrations(encryptedSavedObjectsSetup)['7.14.0'];
const migration714 = SavedObjectsUtils.getMigrationFunction(
getActionsMigrations(encryptedSavedObjectsSetup)['7.14.0']
);
const action = getMockData({ isMissingSecrets: undefined });
const migratedAction = migration714(action, context);
expect(migratedAction).toEqual({
@ -121,7 +134,9 @@ describe('successful migrations', () => {
describe('7.16.0', () => {
test('set service config property for .email connectors if service is undefined', () => {
const migration716 = getActionsMigrations(encryptedSavedObjectsSetup)['7.16.0'];
const migration716 = SavedObjectsUtils.getMigrationFunction(
getActionsMigrations(encryptedSavedObjectsSetup)['7.16.0']
);
const action = getMockDataForEmail({ config: { service: undefined } });
const migratedAction = migration716(action, context);
expect(migratedAction.attributes.config).toEqual({
@ -139,7 +154,9 @@ describe('successful migrations', () => {
});
test('set service config property for .email connectors if service is null', () => {
const migration716 = getActionsMigrations(encryptedSavedObjectsSetup)['7.16.0'];
const migration716 = SavedObjectsUtils.getMigrationFunction(
getActionsMigrations(encryptedSavedObjectsSetup)['7.16.0']
);
const action = getMockDataForEmail({ config: { service: null } });
const migratedAction = migration716(action, context);
expect(migratedAction.attributes.config).toEqual({
@ -157,7 +174,9 @@ describe('successful migrations', () => {
});
test('skips migrating .email connectors if service is defined, even if value is nonsense', () => {
const migration716 = getActionsMigrations(encryptedSavedObjectsSetup)['7.16.0'];
const migration716 = SavedObjectsUtils.getMigrationFunction(
getActionsMigrations(encryptedSavedObjectsSetup)['7.16.0']
);
const action = getMockDataForEmail({ config: { service: 'gobbledygook' } });
const migratedAction = migration716(action, context);
expect(migratedAction.attributes.config).toEqual({
@ -167,7 +186,9 @@ describe('successful migrations', () => {
});
test('set usesTableApi config property for .servicenow', () => {
const migration716 = getActionsMigrations(encryptedSavedObjectsSetup)['7.16.0'];
const migration716 = SavedObjectsUtils.getMigrationFunction(
getActionsMigrations(encryptedSavedObjectsSetup)['7.16.0']
);
const action = getMockDataForServiceNow716({ usesTableApi: true });
const migratedAction = migration716(action, context);
@ -184,7 +205,9 @@ describe('successful migrations', () => {
});
test('set usesTableApi config property for .servicenow-sir', () => {
const migration716 = getActionsMigrations(encryptedSavedObjectsSetup)['7.16.0'];
const migration716 = SavedObjectsUtils.getMigrationFunction(
getActionsMigrations(encryptedSavedObjectsSetup)['7.16.0']
);
const action = getMockDataForServiceNow716({ actionTypeId: '.servicenow-sir' });
const migratedAction = migration716(action, context);
@ -201,7 +224,9 @@ describe('successful migrations', () => {
});
test('it does not set usesTableApi config for other connectors', () => {
const migration716 = getActionsMigrations(encryptedSavedObjectsSetup)['7.16.0'];
const migration716 = SavedObjectsUtils.getMigrationFunction(
getActionsMigrations(encryptedSavedObjectsSetup)['7.16.0']
);
const action = getMockData();
const migratedAction = migration716(action, context);
expect(migratedAction).toEqual(action);
@ -210,7 +235,9 @@ describe('successful migrations', () => {
describe('8.0.0', () => {
test('no op migration for rules SO', () => {
const migration800 = getActionsMigrations(encryptedSavedObjectsSetup)['8.0.0'];
const migration800 = SavedObjectsUtils.getMigrationFunction(
getActionsMigrations(encryptedSavedObjectsSetup)['8.0.0']
);
const action = getMockData({});
expect(migration800(action, context)).toEqual(action);
});
@ -218,7 +245,9 @@ describe('successful migrations', () => {
describe('8.3.0', () => {
test('set isOAuth config property for .servicenow', () => {
const migration830 = getActionsMigrations(encryptedSavedObjectsSetup)['8.3.0'];
const migration830 = SavedObjectsUtils.getMigrationFunction(
getActionsMigrations(encryptedSavedObjectsSetup)['8.3.0']
);
const action = getMockDataForServiceNow83();
const migratedAction = migration830(action, context);
@ -230,7 +259,9 @@ describe('successful migrations', () => {
});
test('set isOAuth config property for .servicenow-sir', () => {
const migration830 = getActionsMigrations(encryptedSavedObjectsSetup)['8.3.0'];
const migration830 = SavedObjectsUtils.getMigrationFunction(
getActionsMigrations(encryptedSavedObjectsSetup)['8.3.0']
);
const action = getMockDataForServiceNow83({ actionTypeId: '.servicenow-sir' });
const migratedAction = migration830(action, context);
@ -242,7 +273,9 @@ describe('successful migrations', () => {
});
test('set isOAuth config property for .servicenow-itom', () => {
const migration830 = getActionsMigrations(encryptedSavedObjectsSetup)['8.3.0'];
const migration830 = SavedObjectsUtils.getMigrationFunction(
getActionsMigrations(encryptedSavedObjectsSetup)['8.3.0']
);
const action = getMockDataForServiceNow83({ actionTypeId: '.servicenow-itom' });
const migratedAction = migration830(action, context);
@ -254,7 +287,9 @@ describe('successful migrations', () => {
});
test('it does not set isOAuth config for other connectors', () => {
const migration830 = getActionsMigrations(encryptedSavedObjectsSetup)['8.3.0'];
const migration830 = SavedObjectsUtils.getMigrationFunction(
getActionsMigrations(encryptedSavedObjectsSetup)['8.3.0']
);
const action = getMockData();
const migratedAction = migration830(action, context);
@ -273,7 +308,9 @@ describe('handles errors during migrations', () => {
describe('7.10.0 throws if migration fails', () => {
test('should show the proper exception', () => {
const migration710 = getActionsMigrations(encryptedSavedObjectsSetup)['7.10.0'];
const migration710 = SavedObjectsUtils.getMigrationFunction(
getActionsMigrations(encryptedSavedObjectsSetup)['7.10.0']
);
const action = getMockDataForEmail({});
expect(() => {
migration710(action, context);
@ -291,7 +328,9 @@ describe('handles errors during migrations', () => {
describe('7.11.0 throws if migration fails', () => {
test('should show the proper exception', () => {
const migration711 = getActionsMigrations(encryptedSavedObjectsSetup)['7.11.0'];
const migration711 = SavedObjectsUtils.getMigrationFunction(
getActionsMigrations(encryptedSavedObjectsSetup)['7.11.0']
);
const action = getMockDataForEmail({});
expect(() => {
migration711(action, context);
@ -309,7 +348,9 @@ describe('handles errors during migrations', () => {
describe('7.14.0 throws if migration fails', () => {
test('should show the proper exception', () => {
const migration714 = getActionsMigrations(encryptedSavedObjectsSetup)['7.14.0'];
const migration714 = SavedObjectsUtils.getMigrationFunction(
getActionsMigrations(encryptedSavedObjectsSetup)['7.14.0']
);
const action = getMockDataForEmail({});
expect(() => {
migration714(action, context);

View file

@ -35,6 +35,7 @@
"@kbn/safer-lodash-set",
"@kbn/core-http-server-mocks",
"@kbn/tinymath",
"@kbn/core-saved-objects-utils-server",
],
"exclude": [
"target/**/*",

View file

@ -27,6 +27,7 @@ import type {
SavedObjectUnsanitizedDoc,
} from '@kbn/core/server';
import { mergeSavedObjectMigrationMaps } from '@kbn/core/server';
import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server';
import type { MigrateFunction, MigrateFunctionsObject } from '@kbn/kibana-utils-plugin/common';
import type { SerializableRecord } from '@kbn/utility-types';
import { GENERATED_ALERT, SUB_CASE_SAVED_OBJECT } from './constants';
@ -245,7 +246,10 @@ describe('comments migrations', () => {
});
expect(migrations['7.14.0']).toBeDefined();
const result = migrations['7.14.0'](caseComment, contextMock);
const result = SavedObjectsUtils.getMigrationFunction(migrations['7.14.0'])(
caseComment,
contextMock
);
const parsedComment = parseCommentString(result.attributes.comment);
const lensVisualizations = getLensVisualizations(
@ -330,7 +334,7 @@ describe('comments migrations', () => {
};
const mergedFunctions = mergeSavedObjectMigrationMaps(migrationObj1, migrationObj2);
mergedFunctions['1.0.0'](caseComment, contextMock);
SavedObjectsUtils.getMigrationFunction(mergedFunctions['1.0.0'])(caseComment, contextMock);
const log = contextMock.log as jest.Mocked<SavedObjectsMigrationLogger>;
expect(log.error.mock.calls[0]).toMatchInlineSnapshot(`
@ -360,7 +364,9 @@ describe('comments migrations', () => {
const mergedFunctions = mergeSavedObjectMigrationMaps(migrationObj1, migrationObj2);
expect(() => mergedFunctions['2.0.0'](caseComment, contextMock)).toThrow();
expect(() =>
SavedObjectsUtils.getMigrationFunction(mergedFunctions['2.0.0'])(caseComment, contextMock)
).toThrow();
const log = contextMock.log as jest.Mocked<SavedObjectsMigrationLogger>;
expect(log.error).not.toHaveBeenCalled();
@ -586,7 +592,7 @@ describe('comments migrations', () => {
});
it('migrates a persistable state attachment correctly', () => {
const migrationFn = migrations['8.4.0'];
const migrationFn = SavedObjectsUtils.getMigrationFunction(migrations['8.4.0']);
const res = migrationFn(
{
id: '123',
@ -617,7 +623,7 @@ describe('comments migrations', () => {
});
it('should not change any other attribute expect persistableStateAttachmentState or put excess attributes', () => {
const migrationFn = migrations['8.4.0'];
const migrationFn = SavedObjectsUtils.getMigrationFunction(migrations['8.4.0']);
const res = migrationFn(
{
id: '123',
@ -655,7 +661,7 @@ describe('comments migrations', () => {
* combined along with the persistable state attachment
* migrations
*/
const migrationFn = migrations['7.14.0'];
const migrationFn = SavedObjectsUtils.getMigrationFunction(migrations['7.14.0']);
const res = migrationFn(
{
id: '123',
@ -688,7 +694,7 @@ describe('comments migrations', () => {
});
it('does not run persistable state migration on other attachments', () => {
const migrationFn = migrations['8.4.0'];
const migrationFn = SavedObjectsUtils.getMigrationFunction(migrations['8.4.0']);
const res = migrationFn(
{
id: '123',

View file

@ -13,6 +13,7 @@ import {
SavedObjectMigrationFn,
SavedObjectUnsanitizedDoc,
} from '@kbn/core/server';
import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server';
import {
LensDocShape715,
LensDocShape810,
@ -132,12 +133,12 @@ describe('Lens migrations', () => {
visualizationType: 'lnsMetric',
},
};
const result = migrations['7.7.0'](target, context);
const result = SavedObjectsUtils.getMigrationFunction(migrations['7.7.0'])(target, context);
expect(result).toEqual(target);
});
it('should handle missing layers', () => {
const result = migrations['7.7.0'](
const result = SavedObjectsUtils.getMigrationFunction(migrations['7.7.0'])(
{
...example,
attributes: {
@ -169,7 +170,7 @@ describe('Lens migrations', () => {
});
it('should remove only missing accessors', () => {
const result = migrations['7.7.0'](example, context);
const result = SavedObjectsUtils.getMigrationFunction(migrations['7.7.0'])(example, context);
expect(result.attributes.state.visualization.layers).toEqual([
{
@ -285,7 +286,7 @@ describe('Lens migrations', () => {
};
it('should remove the lens_auto_date expression', () => {
const result = migrations['7.8.0'](example, context);
const result = SavedObjectsUtils.getMigrationFunction(migrations['7.8.0'])(example, context);
expect(result.attributes.expression).toContain(`timeFields=\"products.created_on\"`);
});
@ -302,7 +303,7 @@ describe('Lens migrations', () => {
| xyVis xTitle="products.created_on" yTitle="Count of records" legend={legendConfig isVisible=true position="right"} layers={}`,
},
};
const result = migrations['7.8.0'](input, context);
const result = SavedObjectsUtils.getMigrationFunction(migrations['7.8.0'])(input, context);
expect(result).toEqual(input);
});
});
@ -454,12 +455,12 @@ describe('Lens migrations', () => {
};
it('should remove expression', () => {
const result = migrations['7.10.0'](example, context);
const result = SavedObjectsUtils.getMigrationFunction(migrations['7.10.0'])(example, context);
expect(result.attributes.expression).toBeUndefined();
});
it('should list references for layers', () => {
const result = migrations['7.10.0'](example, context);
const result = SavedObjectsUtils.getMigrationFunction(migrations['7.10.0'])(example, context);
expect(
result.references?.find(
(ref) => ref.name === 'indexpattern-datasource-layer-3b7791e9-326e-40d5-a787-b7594e48d906'
@ -473,7 +474,7 @@ describe('Lens migrations', () => {
});
it('should remove index pattern ids from layers', () => {
const result = migrations['7.10.0'](example, context);
const result = SavedObjectsUtils.getMigrationFunction(migrations['7.10.0'])(example, context);
expect(
result.attributes.state.datasourceStates.indexpattern.layers[
'3b7791e9-326e-40d5-a787-b7594e48d906'
@ -487,12 +488,12 @@ describe('Lens migrations', () => {
});
it('should remove datsource meta data', () => {
const result = migrations['7.10.0'](example, context);
const result = SavedObjectsUtils.getMigrationFunction(migrations['7.10.0'])(example, context);
expect(result.attributes.state.datasourceMetaData).toBeUndefined();
});
it('should list references for filters', () => {
const result = migrations['7.10.0'](example, context);
const result = SavedObjectsUtils.getMigrationFunction(migrations['7.10.0'])(example, context);
expect(result.references?.find((ref) => ref.name === 'filter-index-pattern-0')?.id).toEqual(
'90943e30-9a47-11e8-b64d-95841ca0b247'
);
@ -502,7 +503,7 @@ describe('Lens migrations', () => {
});
it('should remove index pattern ids from filters', () => {
const result = migrations['7.10.0'](example, context);
const result = SavedObjectsUtils.getMigrationFunction(migrations['7.10.0'])(example, context);
expect(result.attributes.state.filters[0].meta.index).toBeUndefined();
expect(result.attributes.state.filters[0].meta.indexRefName).toEqual(
'filter-index-pattern-0'
@ -514,7 +515,7 @@ describe('Lens migrations', () => {
});
it('should list reference for current index pattern', () => {
const result = migrations['7.10.0'](example, context);
const result = SavedObjectsUtils.getMigrationFunction(migrations['7.10.0'])(example, context);
expect(
result.references?.find(
(ref) => ref.name === 'indexpattern-datasource-current-indexpattern'
@ -523,14 +524,14 @@ describe('Lens migrations', () => {
});
it('should remove current index pattern id from datasource state', () => {
const result = migrations['7.10.0'](example, context);
const result = SavedObjectsUtils.getMigrationFunction(migrations['7.10.0'])(example, context);
expect(
result.attributes.state.datasourceStates.indexpattern.currentIndexPatternId
).toBeUndefined();
});
it('should produce a valid document', () => {
const result = migrations['7.10.0'](example, context);
const result = SavedObjectsUtils.getMigrationFunction(migrations['7.10.0'])(example, context);
// changes to the outcome of this are critical - this test is a safe guard to not introduce changes accidentally
// if this test fails, make extra sure it's expected
expect(result).toMatchSnapshot();
@ -608,9 +609,10 @@ describe('Lens migrations', () => {
};
it('should remove the suggested priority from all columns', () => {
const result = migrations['7.11.0'](example, context) as ReturnType<
SavedObjectMigrationFn<LensDocShape, LensDocShape>
>;
const result = SavedObjectsUtils.getMigrationFunction(migrations['7.11.0'])(
example,
context
) as ReturnType<SavedObjectMigrationFn<LensDocShape, LensDocShape>>;
const resultLayers = result.attributes.state.datasourceStates.indexpattern.layers;
const layersWithSuggestedPriority = Object.values(resultLayers).reduce(
(count, layer) =>
@ -657,16 +659,18 @@ describe('Lens migrations', () => {
...example,
attributes: { ...example.attributes, visualizationType: 'xy' },
};
const result = migrations['7.12.0'](xyChart, context) as ReturnType<
SavedObjectMigrationFn<LensDocShape, LensDocShape>
>;
const result = SavedObjectsUtils.getMigrationFunction(migrations['7.12.0'])(
xyChart,
context
) as ReturnType<SavedObjectMigrationFn<LensDocShape, LensDocShape>>;
expect(result).toBe(xyChart);
});
it('should remove layer array and reshape state', () => {
const result = migrations['7.12.0'](example, context) as ReturnType<
SavedObjectMigrationFn<LensDocShape, LensDocShape>
>;
const result = SavedObjectsUtils.getMigrationFunction(migrations['7.12.0'])(
example,
context
) as ReturnType<SavedObjectMigrationFn<LensDocShape, LensDocShape>>;
expect(result.attributes.state.visualization).toEqual({
layerId: 'first',
columns: [
@ -856,19 +860,22 @@ describe('Lens migrations', () => {
};
it('should rename only specific operation types', () => {
const result = migrations['7.13.0'](example, context) as ReturnType<
SavedObjectMigrationFn<LensDocShape, LensDocShape>
>;
const result = SavedObjectsUtils.getMigrationFunction(migrations['7.13.0'])(
example,
context
) as ReturnType<SavedObjectMigrationFn<LensDocShape, LensDocShape>>;
validate(result);
});
it('can be applied multiple times', () => {
const result1 = migrations['7.13.0'](example, context) as ReturnType<
SavedObjectMigrationFn<LensDocShape, LensDocShape>
>;
const result2 = migrations['7.13.1'](result1, context) as ReturnType<
SavedObjectMigrationFn<LensDocShape, LensDocShape>
>;
const result1 = SavedObjectsUtils.getMigrationFunction(migrations['7.13.0'])(
example,
context
) as ReturnType<SavedObjectMigrationFn<LensDocShape, LensDocShape>>;
const result2 = SavedObjectsUtils.getMigrationFunction(migrations['7.13.1'])(
result1,
context
) as ReturnType<SavedObjectMigrationFn<LensDocShape, LensDocShape>>;
validate(result2);
});
});
@ -949,9 +956,10 @@ describe('Lens migrations', () => {
};
it('should remove time zone param from date histogram', () => {
const result = migrations['7.14.0'](example, context) as ReturnType<
SavedObjectMigrationFn<LensDocShape, LensDocShape>
>;
const result = SavedObjectsUtils.getMigrationFunction(migrations['7.14.0'])(
example,
context
) as ReturnType<SavedObjectMigrationFn<LensDocShape, LensDocShape>>;
const layers = Object.values(result.attributes.state.datasourceStates.indexpattern.layers);
expect(layers.length).toBe(1);
const columns = Object.values(layers[0].columns);
@ -1059,9 +1067,10 @@ describe('Lens migrations', () => {
},
],
} as unknown as VisStatePre715;
const result = migrations['7.15.0'](xyExample, context) as ReturnType<
SavedObjectMigrationFn<LensDocShape, LensDocShape>
>;
const result = SavedObjectsUtils.getMigrationFunction(migrations['7.15.0'])(
xyExample,
context
) as ReturnType<SavedObjectMigrationFn<LensDocShape, LensDocShape>>;
const state = (result.attributes as LensDocShape715<VisStatePost715>).state.visualization;
if ('layers' in state) {
for (const layer of state.layers) {
@ -1087,9 +1096,10 @@ describe('Lens migrations', () => {
},
],
} as unknown as VisStatePre715;
const result = migrations['7.15.0'](pieExample, context) as ReturnType<
SavedObjectMigrationFn<LensDocShape, LensDocShape>
>;
const result = SavedObjectsUtils.getMigrationFunction(migrations['7.15.0'])(
pieExample,
context
) as ReturnType<SavedObjectMigrationFn<LensDocShape, LensDocShape>>;
const state = (result.attributes as LensDocShape715<VisStatePost715>).state.visualization;
if ('layers' in state) {
for (const layer of state.layers) {
@ -1104,9 +1114,10 @@ describe('Lens migrations', () => {
layerId: '1',
accessor: undefined,
} as unknown as VisStatePre715;
const result = migrations['7.15.0'](metricExample, context) as ReturnType<
SavedObjectMigrationFn<LensDocShape, LensDocShape>
>;
const result = SavedObjectsUtils.getMigrationFunction(migrations['7.15.0'])(
metricExample,
context
) as ReturnType<SavedObjectMigrationFn<LensDocShape, LensDocShape>>;
const state = (result.attributes as LensDocShape715<VisStatePost715>).state.visualization;
expect('layerType' in state).toEqual(true);
if ('layerType' in state) {
@ -1120,9 +1131,10 @@ describe('Lens migrations', () => {
layerId: '1',
accessor: undefined,
} as unknown as VisStatePre715;
const result = migrations['7.15.0'](datatableExample, context) as ReturnType<
SavedObjectMigrationFn<LensDocShape, LensDocShape>
>;
const result = SavedObjectsUtils.getMigrationFunction(migrations['7.15.0'])(
datatableExample,
context
) as ReturnType<SavedObjectMigrationFn<LensDocShape, LensDocShape>>;
const state = (result.attributes as LensDocShape715<VisStatePost715>).state.visualization;
expect('layerType' in state).toEqual(true);
if ('layerType' in state) {
@ -1136,9 +1148,10 @@ describe('Lens migrations', () => {
layerId: '1',
accessor: undefined,
} as unknown as VisStatePre715;
const result = migrations['7.15.0'](heatmapExample, context) as ReturnType<
SavedObjectMigrationFn<LensDocShape, LensDocShape>
>;
const result = SavedObjectsUtils.getMigrationFunction(migrations['7.15.0'])(
heatmapExample,
context
) as ReturnType<SavedObjectMigrationFn<LensDocShape, LensDocShape>>;
const state = (result.attributes as LensDocShape715<VisStatePost715>).state.visualization;
expect('layerType' in state).toEqual(true);
if ('layerType' in state) {
@ -1221,9 +1234,10 @@ describe('Lens migrations', () => {
},
],
} as unknown as VisState716;
const result = migrations['7.16.0'](exampleCopy, context) as ReturnType<
SavedObjectMigrationFn<LensDocShape, LensDocShape>
>;
const result = SavedObjectsUtils.getMigrationFunction(migrations['7.16.0'])(
exampleCopy,
context
) as ReturnType<SavedObjectMigrationFn<LensDocShape, LensDocShape>>;
expect(result).toEqual(exampleCopy);
}
});
@ -1241,9 +1255,10 @@ describe('Lens migrations', () => {
},
],
} as unknown as VisState716;
const result = migrations['7.16.0'](datatableExample, context) as ReturnType<
SavedObjectMigrationFn<LensDocShape, LensDocShape>
>;
const result = SavedObjectsUtils.getMigrationFunction(migrations['7.16.0'])(
datatableExample,
context
) as ReturnType<SavedObjectMigrationFn<LensDocShape, LensDocShape>>;
expect(result).toEqual(datatableExample);
});
@ -1260,9 +1275,10 @@ describe('Lens migrations', () => {
},
],
} as unknown as VisState716;
const result = migrations['7.16.0'](datatableExample, context) as ReturnType<
SavedObjectMigrationFn<LensDocShape, LensDocShape>
>;
const result = SavedObjectsUtils.getMigrationFunction(migrations['7.16.0'])(
datatableExample,
context
) as ReturnType<SavedObjectMigrationFn<LensDocShape, LensDocShape>>;
expect(result).toEqual(datatableExample);
});
@ -1272,9 +1288,10 @@ describe('Lens migrations', () => {
(datatableExample.attributes as LensDocShape715<VisState716>).state.visualization = {
columns: [{ colorMode: 'none' }, {}],
} as unknown as VisState716;
const result = migrations['7.16.0'](datatableExample, context) as ReturnType<
SavedObjectMigrationFn<LensDocShape, LensDocShape>
>;
const result = SavedObjectsUtils.getMigrationFunction(migrations['7.16.0'])(
datatableExample,
context
) as ReturnType<SavedObjectMigrationFn<LensDocShape, LensDocShape>>;
expect(result).toEqual(datatableExample);
});
@ -1291,9 +1308,10 @@ describe('Lens migrations', () => {
},
],
} as unknown as VisState716;
const result = migrations['7.16.0'](datatableExample, context) as ReturnType<
SavedObjectMigrationFn<LensDocShape, LensDocShape>
>;
const result = SavedObjectsUtils.getMigrationFunction(migrations['7.16.0'])(
datatableExample,
context
) as ReturnType<SavedObjectMigrationFn<LensDocShape, LensDocShape>>;
expect(result).toEqual(datatableExample);
});
@ -1340,9 +1358,10 @@ describe('Lens migrations', () => {
},
],
} as unknown as VisState716;
const result = migrations['7.16.0'](datatableExample, context) as ReturnType<
SavedObjectMigrationFn<LensDocShape, LensDocShape>
>;
const result = SavedObjectsUtils.getMigrationFunction(migrations['7.16.0'])(
datatableExample,
context
) as ReturnType<SavedObjectMigrationFn<LensDocShape, LensDocShape>>;
const state = (
result.attributes as LensDocShape715<Extract<VisState716, { columns: unknown[] }>>
).state.visualization;
@ -1390,9 +1409,10 @@ describe('Lens migrations', () => {
},
},
} as unknown as VisState716;
const result = migrations['7.16.0'](datatableExample, context) as ReturnType<
SavedObjectMigrationFn<LensDocShape, LensDocShape>
>;
const result = SavedObjectsUtils.getMigrationFunction(migrations['7.16.0'])(
datatableExample,
context
) as ReturnType<SavedObjectMigrationFn<LensDocShape, LensDocShape>>;
const state = (
result.attributes as LensDocShape715<
Extract<VisState716, { palette?: PaletteOutput<CustomPaletteParams> }>
@ -1519,9 +1539,10 @@ describe('Lens migrations', () => {
};
});
const result = migrations['8.1.0'](example, context) as ReturnType<
SavedObjectMigrationFn<LensDocShape, LensDocShape>
>;
const result = SavedObjectsUtils.getMigrationFunction(migrations['8.1.0'])(
example,
context
) as ReturnType<SavedObjectMigrationFn<LensDocShape, LensDocShape>>;
expect(result.attributes.state.filters).toEqual(expectedFilters);
});
@ -1586,9 +1607,10 @@ describe('Lens migrations', () => {
} as unknown as SavedObjectUnsanitizedDoc<LensDocShape810>;
it('should change field for count operations but not for others, not changing the vis', () => {
const result = migrations['8.1.0'](example, context) as ReturnType<
SavedObjectMigrationFn<LensDocShape, LensDocShape>
>;
const result = SavedObjectsUtils.getMigrationFunction(migrations['8.1.0'])(
example,
context
) as ReturnType<SavedObjectMigrationFn<LensDocShape, LensDocShape>>;
expect(
Object.values(
@ -1632,10 +1654,9 @@ describe('Lens migrations', () => {
{}
);
const migratedLensDoc = migrationFunctionsObject[migrationVersion](
lensVisualizationDoc as SavedObjectUnsanitizedDoc,
{} as SavedObjectMigrationContext
);
const migratedLensDoc = SavedObjectsUtils.getMigrationFunction(
migrationFunctionsObject[migrationVersion]
)(lensVisualizationDoc as SavedObjectUnsanitizedDoc, {} as SavedObjectMigrationContext);
expect(migratedLensDoc).toEqual({
attributes: {
@ -1687,10 +1708,9 @@ describe('Lens migrations', () => {
{}
);
const migratedLensDoc = migrationFunctionsObject[migrationVersion](
lensVisualizationDoc as SavedObjectUnsanitizedDoc,
{} as SavedObjectMigrationContext
);
const migratedLensDoc = SavedObjectsUtils.getMigrationFunction(
migrationFunctionsObject[migrationVersion]
)(lensVisualizationDoc as SavedObjectUnsanitizedDoc, {} as SavedObjectMigrationContext);
expect(migratedLensDoc).toEqual({
attributes: {
@ -1735,11 +1755,12 @@ describe('Lens migrations', () => {
}),
}
);
const migratedLensDoc = migrationFunctionsObject[migrationVersion](
lensVisualizationDoc as SavedObjectUnsanitizedDoc,
{} as SavedObjectMigrationContext
);
const otherLensDoc = migrationFunctionsObject[migrationVersion](
const migratedLensDoc = SavedObjectsUtils.getMigrationFunction(
migrationFunctionsObject[migrationVersion]
)(lensVisualizationDoc as SavedObjectUnsanitizedDoc, {} as SavedObjectMigrationContext);
const otherLensDoc = SavedObjectsUtils.getMigrationFunction(
migrationFunctionsObject[migrationVersion]
)(
{
...lensVisualizationDoc,
attributes: {
@ -1829,9 +1850,10 @@ describe('Lens migrations', () => {
} as unknown as SavedObjectUnsanitizedDoc<LensDocShape810>;
it('should change field for count operations but not for others, not changing the vis', () => {
const result = migrations['8.1.0'](example, context) as ReturnType<
SavedObjectMigrationFn<LensDocShape, LensDocShape>
>;
const result = SavedObjectsUtils.getMigrationFunction(migrations['8.1.0'])(
example,
context
) as ReturnType<SavedObjectMigrationFn<LensDocShape, LensDocShape>>;
expect(
Object.values(
@ -1924,9 +1946,10 @@ describe('Lens migrations', () => {
} as unknown as SavedObjectUnsanitizedDoc<LensDocShape810>;
it('should set showArrayValues for last-value columns', () => {
const result = migrations['8.2.0'](example, context) as ReturnType<
SavedObjectMigrationFn<LensDocShape, LensDocShape>
>;
const result = SavedObjectsUtils.getMigrationFunction(migrations['8.2.0'])(
example,
context
) as ReturnType<SavedObjectMigrationFn<LensDocShape, LensDocShape>>;
const layer2Columns =
result.attributes.state.datasourceStates.indexpattern.layers['2'].columns;
@ -2031,7 +2054,10 @@ describe('Lens migrations', () => {
}
it('should migrate enabled fitRowToContent to new rowHeight: "auto"', () => {
const result = migrations['8.2.0'](getExample(true), context) as ReturnType<
const result = SavedObjectsUtils.getMigrationFunction(migrations['8.2.0'])(
getExample(true),
context
) as ReturnType<
SavedObjectMigrationFn<LensDocShape810<VisState810>, LensDocShape810<VisState820>>
>;
@ -2043,7 +2069,10 @@ describe('Lens migrations', () => {
});
it('should migrate disabled fitRowToContent to new rowHeight: "single"', () => {
const result = migrations['8.2.0'](getExample(false), context) as ReturnType<
const result = SavedObjectsUtils.getMigrationFunction(migrations['8.2.0'])(
getExample(false),
context
) as ReturnType<
SavedObjectMigrationFn<LensDocShape810<VisState810>, LensDocShape810<VisState820>>
>;
@ -2117,9 +2146,10 @@ describe('Lens migrations', () => {
} as unknown as SavedObjectUnsanitizedDoc<LensDocShape810>;
it('should set include empty rows for all date histogram columns', () => {
const result = migrations['8.2.0'](example, context) as ReturnType<
SavedObjectMigrationFn<LensDocShape, LensDocShape>
>;
const result = SavedObjectsUtils.getMigrationFunction(migrations['8.2.0'])(
example,
context
) as ReturnType<SavedObjectMigrationFn<LensDocShape, LensDocShape>>;
const layer2Columns =
result.attributes.state.datasourceStates.indexpattern.layers['2'].columns;
@ -2145,9 +2175,10 @@ describe('Lens migrations', () => {
} as unknown as SavedObjectUnsanitizedDoc<LensDocShape810>;
it('preserves current config for existing visualizations that are using the DEFAULTS', () => {
const result = migrations['8.3.0'](example, context) as ReturnType<
SavedObjectMigrationFn<LensDocShape, LensDocShape>
>;
const result = SavedObjectsUtils.getMigrationFunction(migrations['8.3.0'])(
example,
context
) as ReturnType<SavedObjectMigrationFn<LensDocShape, LensDocShape>>;
const visState = result.attributes.state.visualization as LegacyMetricState;
expect(visState.textAlign).toBe('center');
expect(visState.titlePosition).toBe('bottom');
@ -2155,7 +2186,7 @@ describe('Lens migrations', () => {
});
it('preserves current config for existing visualizations that are using CUSTOM settings', () => {
const result = migrations['8.3.0'](
const result = SavedObjectsUtils.getMigrationFunction(migrations['8.3.0'])(
{
...example,
attributes: {
@ -2180,7 +2211,7 @@ describe('Lens migrations', () => {
describe('8.3.0 - convert legend sizes to strings', () => {
const context = { log: { warn: () => {} } } as unknown as SavedObjectMigrationContext;
const migrate = migrations['8.3.0'];
const migrate = SavedObjectsUtils.getMigrationFunction(migrations['8.3.0']);
const autoLegendSize = 'auto';
const largeLegendSize = 'large';
@ -2266,15 +2297,16 @@ describe('Lens migrations', () => {
} as unknown as SavedObjectUnsanitizedDoc<LensDocShape810>;
it('migrates valueLabels from `inside` to `show`', () => {
const result = migrations['8.3.0'](example, context) as ReturnType<
SavedObjectMigrationFn<LensDocShape, LensDocShape>
>;
const result = SavedObjectsUtils.getMigrationFunction(migrations['8.3.0'])(
example,
context
) as ReturnType<SavedObjectMigrationFn<LensDocShape, LensDocShape>>;
const visState = result.attributes.state.visualization as VisState830;
expect(visState.valueLabels).toBe('show');
});
it("doesn't migrate valueLabels with `hide` value", () => {
const result = migrations['8.3.0'](
const result = SavedObjectsUtils.getMigrationFunction(migrations['8.3.0'])(
{
...example,
attributes: {
@ -2320,9 +2352,10 @@ describe('Lens migrations', () => {
} as unknown as SavedObjectUnsanitizedDoc<LensDocShape850<XYVisStatePre850>>;
it('migrates existing annotation events as manual type', () => {
const result = migrations['8.5.0'](example, context) as ReturnType<
SavedObjectMigrationFn<LensDocShape, LensDocShape>
>;
const result = SavedObjectsUtils.getMigrationFunction(migrations['8.5.0'])(
example,
context
) as ReturnType<SavedObjectMigrationFn<LensDocShape, LensDocShape>>;
const visState = result.attributes.state.visualization as VisState850;
const [dataLayer, annotationLayer] = visState.layers;
expect(dataLayer).toEqual({ layerType: 'data' });
@ -2349,14 +2382,15 @@ describe('Lens migrations', () => {
} as unknown as SavedObjectUnsanitizedDoc<LensDocShape810>;
it('lnsMetric => lnsLegacyMetric', () => {
const result = migrations['8.5.0'](example, context) as ReturnType<
SavedObjectMigrationFn<LensDocShape, LensDocShape>
>;
const result = SavedObjectsUtils.getMigrationFunction(migrations['8.5.0'])(
example,
context
) as ReturnType<SavedObjectMigrationFn<LensDocShape, LensDocShape>>;
expect(result.attributes.visualizationType).toBe('lnsLegacyMetric');
});
it('lnsMetricNew => lnsMetric', () => {
const result = migrations['8.5.0'](
const result = SavedObjectsUtils.getMigrationFunction(migrations['8.5.0'])(
{ ...example, attributes: { ...example.attributes, visualizationType: 'lnsMetricNew' } },
context
) as ReturnType<SavedObjectMigrationFn<LensDocShape, LensDocShape>>;
@ -2390,9 +2424,10 @@ describe('Lens migrations', () => {
} as unknown as SavedObjectUnsanitizedDoc<LensDocShape810>;
it('make metric an array', () => {
const result = migrations['8.6.0'](example, context) as ReturnType<
SavedObjectMigrationFn<LensDocShape, LensDocShape>
>;
const result = SavedObjectsUtils.getMigrationFunction(migrations['8.6.0'])(
example,
context
) as ReturnType<SavedObjectMigrationFn<LensDocShape, LensDocShape>>;
expect(
(result.attributes.state.visualization as { layers: Array<{ metrics: string[] }> })
.layers[0]
@ -2496,7 +2531,7 @@ describe('Lens migrations', () => {
};
it('migrates the indexpattern datasource to formBased', () => {
const result = migrations['8.6.0'](example, context);
const result = SavedObjectsUtils.getMigrationFunction(migrations['8.6.0'])(example, context);
expect(result.attributes.state.datasourceStates.formBased).toBe(
example.attributes.state.datasourceStates.indexpattern
);

View file

@ -72,6 +72,7 @@
"@kbn/core-saved-objects-api-server",
"@kbn/object-versioning",
"@kbn/config-schema",
"@kbn/core-saved-objects-utils-server",
],
"exclude": [
"target/**/*",

View file

@ -7,6 +7,7 @@
import type { SavedObjectUnsanitizedDoc } from '@kbn/core/server';
import { migrationMocks } from '@kbn/core/server/mocks';
import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server';
import { ManifestConstants } from './common';
import type { OldInternalManifestSchema } from './migrations';
import { migrations } from './migrations';
@ -21,7 +22,7 @@ describe('7.12.0 manifest migrations', () => {
const ARTIFACT_ID_3 =
'endpoint-trustlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3';
const migration = migrations['7.12.0'];
const migration = SavedObjectsUtils.getMigrationFunction(migrations['7.12.0']);
test('Migrates ids property', () => {
const doc: SavedObjectUnsanitizedDoc<OldInternalManifestSchema> = {

View file

@ -9,6 +9,7 @@ import { v4 as uuidv4 } from 'uuid';
import { getMigrations } from './migrations';
import { SavedObjectUnsanitizedDoc } from '@kbn/core/server';
import { migrationMocks } from '@kbn/core/server/mocks';
import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server';
import { TaskInstanceWithDeprecatedFields } from '../task';
const migrationContext = migrationMocks.createContext();
@ -19,7 +20,7 @@ describe('successful migrations', () => {
});
describe('7.4.0', () => {
test('extend task instance with updated_at', () => {
const migration740 = getMigrations()['7.4.0'];
const migration740 = SavedObjectsUtils.getMigrationFunction(getMigrations()['7.4.0']);
const taskInstance = getMockData({});
expect(migration740(taskInstance, migrationContext).attributes.updated_at).not.toBeNull();
});
@ -27,7 +28,7 @@ describe('successful migrations', () => {
describe('7.6.0', () => {
test('rename property Internal to Schedule', () => {
const migration760 = getMigrations()['7.6.0'];
const migration760 = SavedObjectsUtils.getMigrationFunction(getMigrations()['7.6.0']);
const taskInstance = getMockData({});
expect(migration760(taskInstance, migrationContext)).toEqual({
...taskInstance,
@ -41,7 +42,7 @@ describe('successful migrations', () => {
describe('8.0.0', () => {
test('transforms actionsTasksLegacyIdToSavedObjectIds', () => {
const migration800 = getMigrations()['8.0.0'];
const migration800 = SavedObjectsUtils.getMigrationFunction(getMigrations()['8.0.0']);
const taskInstance = getMockData({
taskType: 'actions:123456',
params: JSON.stringify({ spaceId: 'user1', actionTaskParamsId: '123456' }),
@ -57,7 +58,7 @@ describe('successful migrations', () => {
});
test('it is only applicable for saved objects that live in a custom space', () => {
const migration800 = getMigrations()['8.0.0'];
const migration800 = SavedObjectsUtils.getMigrationFunction(getMigrations()['8.0.0']);
const taskInstance = getMockData({
taskType: 'actions:123456',
params: JSON.stringify({ spaceId: 'default', actionTaskParamsId: '123456' }),
@ -67,7 +68,7 @@ describe('successful migrations', () => {
});
test('it is only applicable for saved objects that live in a custom space even if spaces are disabled', () => {
const migration800 = getMigrations()['8.0.0'];
const migration800 = SavedObjectsUtils.getMigrationFunction(getMigrations()['8.0.0']);
const taskInstance = getMockData({
taskType: 'actions:123456',
params: JSON.stringify({ actionTaskParamsId: '123456' }),
@ -77,7 +78,7 @@ describe('successful migrations', () => {
});
test('transforms alertingTaskLegacyIdToSavedObjectIds', () => {
const migration800 = getMigrations()['8.0.0'];
const migration800 = SavedObjectsUtils.getMigrationFunction(getMigrations()['8.0.0']);
const taskInstance = getMockData({
taskType: 'alerting:123456',
params: JSON.stringify({ spaceId: 'user1', alertId: '123456' }),
@ -93,7 +94,7 @@ describe('successful migrations', () => {
});
test('skip transformation for defult space scenario', () => {
const migration800 = getMigrations()['8.0.0'];
const migration800 = SavedObjectsUtils.getMigrationFunction(getMigrations()['8.0.0']);
const taskInstance = getMockData({
taskType: 'alerting:123456',
params: JSON.stringify({ spaceId: 'default', alertId: '123456' }),
@ -111,7 +112,7 @@ describe('successful migrations', () => {
describe('8.2.0', () => {
test('resets attempts and status of a "failed" alerting tasks without schedule interval', () => {
const migration820 = getMigrations()['8.2.0'];
const migration820 = SavedObjectsUtils.getMigrationFunction(getMigrations()['8.2.0']);
const taskInstance = getMockData({
taskType: 'alerting:123',
status: 'failed',
@ -129,7 +130,7 @@ describe('successful migrations', () => {
});
test('resets attempts and status of a "running" alerting tasks without schedule interval', () => {
const migration820 = getMigrations()['8.2.0'];
const migration820 = SavedObjectsUtils.getMigrationFunction(getMigrations()['8.2.0']);
const taskInstance = getMockData({
taskType: 'alerting:123',
status: 'running',
@ -147,7 +148,7 @@ describe('successful migrations', () => {
});
test('does not update the tasks that are not "failed"', () => {
const migration820 = getMigrations()['8.2.0'];
const migration820 = SavedObjectsUtils.getMigrationFunction(getMigrations()['8.2.0']);
const taskInstance = getMockData({
taskType: 'alerting:123',
status: 'idle',
@ -159,7 +160,7 @@ describe('successful migrations', () => {
});
test('does not update the tasks that are not "failed" and has a schedule', () => {
const migration820 = getMigrations()['8.2.0'];
const migration820 = SavedObjectsUtils.getMigrationFunction(getMigrations()['8.2.0']);
const taskInstance = getMockData({
taskType: 'alerting:123',
status: 'idle',
@ -171,7 +172,7 @@ describe('successful migrations', () => {
});
test('resets "unrecognized" status to "idle" when task type is not in REMOVED_TYPES list', () => {
const migration820 = getMigrations()['8.2.0'];
const migration820 = SavedObjectsUtils.getMigrationFunction(getMigrations()['8.2.0']);
const taskInstance = getMockData({
taskType: 'someValidTask',
status: 'unrecognized',
@ -187,7 +188,7 @@ describe('successful migrations', () => {
});
test('does not modify "unrecognized" status when task type is in REMOVED_TYPES list', () => {
const migration820 = getMigrations()['8.2.0'];
const migration820 = SavedObjectsUtils.getMigrationFunction(getMigrations()['8.2.0']);
const taskInstance = getMockData({
taskType: 'sampleTaskRemovedType',
status: 'unrecognized',
@ -197,7 +198,7 @@ describe('successful migrations', () => {
});
test('does not modify document when status is "running"', () => {
const migration820 = getMigrations()['8.2.0'];
const migration820 = SavedObjectsUtils.getMigrationFunction(getMigrations()['8.2.0']);
const taskInstance = getMockData({
taskType: 'someTask',
status: 'running',
@ -207,7 +208,7 @@ describe('successful migrations', () => {
});
test('does not modify document when status is "idle"', () => {
const migration820 = getMigrations()['8.2.0'];
const migration820 = SavedObjectsUtils.getMigrationFunction(getMigrations()['8.2.0']);
const taskInstance = getMockData({
taskType: 'someTask',
status: 'idle',
@ -217,7 +218,7 @@ describe('successful migrations', () => {
});
test('does not modify document when status is "failed"', () => {
const migration820 = getMigrations()['8.2.0'];
const migration820 = SavedObjectsUtils.getMigrationFunction(getMigrations()['8.2.0']);
const taskInstance = getMockData({
taskType: 'someTask',
status: 'failed',
@ -229,7 +230,7 @@ describe('successful migrations', () => {
describe('8.5.0', () => {
test('adds enabled: true to tasks that are running, claiming, or idle', () => {
const migration850 = getMigrations()['8.5.0'];
const migration850 = SavedObjectsUtils.getMigrationFunction(getMigrations()['8.5.0']);
const activeTasks = [
getMockData({
status: 'running',
@ -253,7 +254,7 @@ describe('successful migrations', () => {
});
test('does not modify tasks that are failed or unrecognized', () => {
const migration850 = getMigrations()['8.5.0'];
const migration850 = SavedObjectsUtils.getMigrationFunction(getMigrations()['8.5.0']);
const inactiveTasks = [
getMockData({
status: 'failed',
@ -272,7 +273,7 @@ describe('successful migrations', () => {
describe('handles errors during migrations', () => {
describe('8.0.0 throws if migration fails', () => {
test('should throw the exception if task instance params format is wrong', () => {
const migration800 = getMigrations()['8.0.0'];
const migration800 = SavedObjectsUtils.getMigrationFunction(getMigrations()['8.0.0']);
const taskInstance = getMockData({
taskType: 'alerting:123456',
params: `{ spaceId: 'user1', customId: '123456' }`,

View file

@ -8,6 +8,7 @@
import { omit, cloneDeep } from 'lodash';
import { SavedObjectUnsanitizedDoc } from '@kbn/core/server';
import { migrationMocks } from '@kbn/core/server/mocks';
import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server';
import type {
RuleTaskState,
WrappedLifecycleRuleState,
@ -20,7 +21,7 @@ import { SerializedConcreteTaskInstance, TaskStatus } from '../task';
type RawAlertInstances = Record<string, RawAlertInstance>;
const migrationContext = migrationMocks.createContext();
const migration880 = getMigrations()['8.8.0'];
const migration880 = SavedObjectsUtils.getMigrationFunction(getMigrations()['8.8.0']);
describe('successful migrations for 8.8.0', () => {
beforeEach(() => {

View file

@ -18,7 +18,8 @@
"@kbn/safer-lodash-set",
"@kbn/es-types",
"@kbn/apm-utils",
"@kbn/core-saved-objects-common"
"@kbn/core-saved-objects-common",
"@kbn/core-saved-objects-utils-server"
],
"exclude": [
"target/**/*",