Skip to content

Commit

Permalink
Add "conversion" transforms for object types
Browse files Browse the repository at this point in the history
These can only be applied using the
`convertToMultiNamespaceTypeVersion` field when defining the object
type. Doing so will cause the document migrator to apply a special
transform function to convert the document from a single-namespace
type to a multi-namespace type in the specified version. Note that
this transform is applied before any consumer-defined migrations in
the same version.
  • Loading branch information
jportner committed Nov 23, 2020
1 parent b1123b9 commit 2dbe7cd
Show file tree
Hide file tree
Showing 10 changed files with 514 additions and 49 deletions.
24 changes: 24 additions & 0 deletions src/core/server/saved_objects/migrations/core/__mocks__/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

const mockUuidv5 = jest.fn().mockReturnValue('uuidv5');
Object.defineProperty(mockUuidv5, 'DNS', { value: 'DNSUUID', writable: false });
jest.mock('uuid/v5', () => mockUuidv5);

export { mockUuidv5 };
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* under the License.
*/

import { mockUuidv5 } from './__mocks__';
import { set } from '@elastic/safer-lodash-set';
import _ from 'lodash';
import { SavedObjectUnsanitizedDoc } from '../../serialization';
Expand All @@ -43,6 +44,10 @@ const createRegistry = (...types: Array<Partial<SavedObjectsType>>) => {
return registry;
};

beforeEach(() => {
mockUuidv5.mockClear();
});

describe('DocumentMigrator', () => {
function testOpts() {
return {
Expand Down Expand Up @@ -114,6 +119,65 @@ describe('DocumentMigrator', () => {
/expected a function, but got 23/i
);
});

it(`validates convertToMultiNamespaceTypeVersion can only be used with namespaceType 'multiple'`, () => {
const invalidDefinition = {
kibanaVersion: '3.2.3',
typeRegistry: createRegistry({
name: 'foo',
convertToMultiNamespaceTypeVersion: 'bar',
}),
log: mockLogger,
};
expect(() => new DocumentMigrator(invalidDefinition)).toThrow(
`Invalid convertToMultiNamespaceTypeVersion for type foo. Expected namespaceType to be 'multiple', but got 'single'.`
);
});

it(`validates convertToMultiNamespaceTypeVersion must be a semver`, () => {
const invalidDefinition = {
kibanaVersion: '3.2.3',
typeRegistry: createRegistry({
name: 'foo',
convertToMultiNamespaceTypeVersion: 'bar',
namespaceType: 'multiple',
}),
log: mockLogger,
};
expect(() => new DocumentMigrator(invalidDefinition)).toThrow(
`Invalid convertToMultiNamespaceTypeVersion for type foo. Expected value to be a semver, but got 'bar'.`
);
});

it('validates convertToMultiNamespaceTypeVersion is not greater than the current Kibana version', () => {
const invalidDefinition = {
kibanaVersion: '3.2.3',
typeRegistry: createRegistry({
name: 'foo',
convertToMultiNamespaceTypeVersion: '3.2.4',
namespaceType: 'multiple',
}),
log: mockLogger,
};
expect(() => new DocumentMigrator(invalidDefinition)).toThrowError(
`Invalid convertToMultiNamespaceTypeVersion for type foo. Value '3.2.4' cannot be greater than the current Kibana version '3.2.3'.`
);
});

it('validates convertToMultiNamespaceTypeVersion is not used on a patch version', () => {
const invalidDefinition = {
kibanaVersion: '3.2.3',
typeRegistry: createRegistry({
name: 'foo',
convertToMultiNamespaceTypeVersion: '3.1.1',
namespaceType: 'multiple',
}),
log: mockLogger,
};
expect(() => new DocumentMigrator(invalidDefinition)).toThrowError(
`Invalid convertToMultiNamespaceTypeVersion for type foo. Value '3.1.1' cannot be used on a patch version (must be like 'x.y.0').`
);
});
});

describe('migration', () => {
Expand Down Expand Up @@ -638,6 +702,157 @@ describe('DocumentMigrator', () => {
bbb: '3.2.3',
});
});

describe('conversion to multi-namespace type', () => {
it('assumes documents w/ undefined migrationVersion are up to date', () => {
const migrator = new DocumentMigrator({
...testOpts(),
typeRegistry: createRegistry(
{ name: 'dog', namespaceType: 'multiple', convertToMultiNamespaceTypeVersion: '1.0.0' }
// no migration transforms are defined, the migrationVersion will be derived from 'convertToMultiNamespaceTypeVersion'
),
});
const obj = {
id: 'mischievous',
type: 'dog',
attributes: { name: 'Ann' },
} as SavedObjectUnsanitizedDoc;
const actual = migrator.migrate(obj, { convertTypes: true });
expect(actual).toEqual({
id: 'mischievous',
type: 'dog',
attributes: { name: 'Ann' },
migrationVersion: { dog: '1.0.0' },
// there is no 'namespaces' field because no transforms were applied; this scenario is contrived for a clean test case but is not indicative of a real-world scenario
});
});

it('skips conversion transforms when the "convertTypes" option is falsy', () => {
const migrator = new DocumentMigrator({
...testOpts(),
typeRegistry: createRegistry({
name: 'dog',
namespaceType: 'multiple',
convertToMultiNamespaceTypeVersion: '1.0.0',
}),
});
const obj = {
id: 'cowardly',
type: 'dog',
attributes: { name: 'Leslie' },
migrationVersion: {},
};
const test = (convertTypes?: boolean) => {
const actual = migrator.migrate(obj, { convertTypes });
expect(actual).toEqual({
id: 'cowardly',
type: 'dog',
attributes: { name: 'Leslie' },
migrationVersion: { dog: '1.0.0' },
// there is no 'namespaces' field because no transforms were applied; this scenario is contrived for a clean test case but is not indicative of a real-world scenario
});
};

test(undefined);
test(false);
});

describe('correctly applies conversion transforms', () => {
const migrator = new DocumentMigrator({
...testOpts(),
typeRegistry: createRegistry({
name: 'dog',
namespaceType: 'multiple',
convertToMultiNamespaceTypeVersion: '1.0.0',
}),
});
const obj = {
id: 'loud',
type: 'dog',
attributes: { name: 'Wally' },
migrationVersion: {},
};

it('in the default space', () => {
const actual = migrator.migrate(obj, { convertTypes: true });
expect(mockUuidv5).not.toHaveBeenCalled();
expect(actual).toEqual({
id: 'loud',
type: 'dog',
attributes: { name: 'Wally' },
migrationVersion: { dog: '1.0.0' },
namespaces: ['default'],
});
});

it('in a non-default space', () => {
const actual = migrator.migrate(
{ ...obj, namespace: 'foo-namespace' },
{ convertTypes: true }
);
expect(mockUuidv5).toHaveBeenCalledTimes(1);
expect(mockUuidv5).toHaveBeenCalledWith('foo-namespace:dog:loud', 'DNSUUID');
expect(actual).toEqual({
id: 'uuidv5',
type: 'dog',
attributes: { name: 'Wally' },
migrationVersion: { dog: '1.0.0' },
namespaces: ['foo-namespace'],
originId: 'loud',
});
});
});

describe('correctly applies conversion and migration transforms', () => {
const migrator = new DocumentMigrator({
...testOpts(),
typeRegistry: createRegistry({
name: 'dog',
namespaceType: 'multiple',
migrations: {
'1.0.0': setAttr('migrationVersion.dog', '2.0.0'),
'2.0.0': (doc) => doc, // noop
},
convertToMultiNamespaceTypeVersion: '1.0.0', // the conversion transform occurs before the migration transform above
}),
});
const obj = {
id: 'hungry',
type: 'dog',
attributes: { name: 'Remy' },
migrationVersion: {},
};

it('in the default space', () => {
const actual = migrator.migrate(obj, { convertTypes: true });
expect(mockUuidv5).not.toHaveBeenCalled();
expect(actual).toEqual({
id: 'hungry',
type: 'dog',
attributes: { name: 'Remy' },
migrationVersion: { dog: '2.0.0' },
namespaces: ['default'],
});
});

it('in a non-default space', () => {
const actual = migrator.migrate(
{ ...obj, namespace: 'foo-namespace' },
{ convertTypes: true }
);
expect(mockUuidv5).toHaveBeenCalledTimes(1);
expect(mockUuidv5).toHaveBeenCalledWith('foo-namespace:dog:hungry', 'DNSUUID');
expect(actual).toEqual({
id: 'uuidv5',
type: 'dog',
attributes: { name: 'Remy' },
migrationVersion: { dog: '2.0.0' },
namespaces: ['foo-namespace'],
originId: 'hungry',
});
});
});
});
});
});

Expand Down
Loading

0 comments on commit 2dbe7cd

Please sign in to comment.