[Migrations] Add migrationVersion property to the Saved Objects API output (#154364)

This commit is contained in:
Michael Dokolin 2023-04-13 12:05:58 +02:00 committed by GitHub
parent 42a893db40
commit 768fe1af31
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 353 additions and 106 deletions

View file

@ -19,7 +19,12 @@ export type { SavedObjectsFindOptionsReference } from '@kbn/core-saved-objects-a
*/
export type SavedObjectsFindOptions = Omit<
SavedObjectFindOptionsServer,
'pit' | 'rootSearchFields' | 'searchAfter' | 'sortOrder' | 'typeToNamespacesMap'
| 'pit'
| 'rootSearchFields'
| 'searchAfter'
| 'sortOrder'
| 'typeToNamespacesMap'
| 'migrationVersionCompatibility'
>;
/**

View file

@ -10,8 +10,8 @@ import type { MgetResponseItem } from '@elastic/elasticsearch/lib/api/typesWithB
import { isNotFoundFromUnsupportedServer } from '@kbn/core-elasticsearch-server-internal';
import type {
SavedObjectsBaseOptions,
SavedObjectsBulkResolveObject,
SavedObjectsResolveOptions,
SavedObjectsResolveResponse,
SavedObjectsIncrementCounterField,
SavedObjectsIncrementCounterOptions,
@ -70,7 +70,7 @@ export interface InternalBulkResolveParams {
encryptionExtension: ISavedObjectsEncryptionExtension | undefined;
securityExtension: ISavedObjectsSecurityExtension | undefined;
objects: SavedObjectsBulkResolveObject[];
options?: SavedObjectsBaseOptions;
options?: SavedObjectsResolveOptions;
}
/**
@ -115,6 +115,7 @@ export async function internalBulkResolve<T>(
const validObjects = allObjects.filter(isRight);
const namespace = normalizeNamespace(options.namespace);
const { migrationVersionCompatibility } = options;
const aliasDocs = await fetchAndUpdateAliases(
validObjects,
@ -178,7 +179,9 @@ export async function internalBulkResolve<T>(
) {
// Encryption
// @ts-expect-error MultiGetHit._source is optional
const object = getSavedObjectFromSource<T>(registry, objectType, objectId, doc);
const object = getSavedObjectFromSource<T>(registry, objectType, objectId, doc, {
migrationVersionCompatibility,
});
if (!encryptionExtension?.isEncryptableType(object.type)) {
return object;
}

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
import { omit } from 'lodash';
import type { SavedObjectsRawDoc } from '@kbn/core-saved-objects-server';
import { ALL_NAMESPACES_STRING } from '@kbn/core-saved-objects-utils-server';
import { encodeHitVersion } from '@kbn/core-saved-objects-base-server-internal';
@ -170,6 +171,49 @@ describe('#getSavedObjectFromSource', () => {
expect(result1).toEqual(expect.objectContaining({ namespaces: ['default'] }));
expect(result2).toEqual(expect.objectContaining({ namespaces: ['foo-ns'] }));
});
it('keeps original `migrationVersion` in compatibility mode', () => {
const type = NAMESPACE_AGNOSTIC_TYPE;
const doc = createRawDoc(type);
const result = getSavedObjectFromSource(registry, type, id, doc, {
migrationVersionCompatibility: 'compatible',
});
expect(result).toHaveProperty('migrationVersion', migrationVersion);
});
it('derives `migrationVersion` in compatibility mode', () => {
const type = NAMESPACE_AGNOSTIC_TYPE;
const doc = omit(createRawDoc(type), '_source.migrationVersion');
const result = getSavedObjectFromSource(registry, type, id, doc, {
migrationVersionCompatibility: 'compatible',
});
expect(result).toHaveProperty('migrationVersion', { [type]: typeMigrationVersion });
});
it('does not derive `migrationVersion` in compatibility mode if there is no type version', () => {
const type = NAMESPACE_AGNOSTIC_TYPE;
const doc = omit(
createRawDoc(type),
'_source.migrationVersion',
'_source.typeMigrationVersion'
);
const result = getSavedObjectFromSource(registry, type, id, doc, {
migrationVersionCompatibility: 'compatible',
});
expect(result).toHaveProperty('migrationVersion', undefined);
});
it('does not derive `migrationVersion` in raw mode', () => {
const type = NAMESPACE_AGNOSTIC_TYPE;
const doc = omit(createRawDoc(type), '_source.migrationVersion');
const result = getSavedObjectFromSource(registry, type, id, doc, {
migrationVersionCompatibility: 'raw',
});
expect(result).toHaveProperty('migrationVersion', undefined);
});
});
describe('#rawDocExistsInNamespace', () => {

View file

@ -13,6 +13,7 @@ import {
type SavedObjectsRawDocSource,
type SavedObject,
SavedObjectsErrorHelpers,
type SavedObjectsRawDocParseOptions,
} from '@kbn/core-saved-objects-server';
import { SavedObjectsUtils, ALL_NAMESPACES_STRING } from '@kbn/core-saved-objects-utils-server';
import {
@ -112,6 +113,14 @@ export function getExpectedVersionProperties(version?: string, document?: SavedO
return {};
}
/**
* @internal
*/
export interface GetSavedObjectFromSourceOptions {
/** {@link SavedObjectsRawDocParseOptions.migrationVersionCompatibility} */
migrationVersionCompatibility?: SavedObjectsRawDocParseOptions['migrationVersionCompatibility'];
}
/**
* Gets a saved object from a raw ES document.
*
@ -126,9 +135,19 @@ export function getSavedObjectFromSource<T>(
registry: ISavedObjectTypeRegistry,
type: string,
id: string,
doc: { _seq_no?: number; _primary_term?: number; _source: SavedObjectsRawDocSource }
doc: { _seq_no?: number; _primary_term?: number; _source: SavedObjectsRawDocSource },
{ migrationVersionCompatibility = 'raw' }: GetSavedObjectFromSourceOptions = {}
): SavedObject<T> {
const { originId, updated_at: updatedAt, created_at: createdAt } = doc._source;
const {
originId,
updated_at: updatedAt,
created_at: createdAt,
coreMigrationVersion,
typeMigrationVersion,
migrationVersion = migrationVersionCompatibility === 'compatible' && typeMigrationVersion
? { [type]: typeMigrationVersion }
: undefined,
} = doc._source;
let namespaces: string[] = [];
if (!registry.isNamespaceAgnostic(type)) {
@ -141,15 +160,15 @@ export function getSavedObjectFromSource<T>(
id,
type,
namespaces,
migrationVersion,
coreMigrationVersion,
typeMigrationVersion,
...(originId && { originId }),
...(updatedAt && { updated_at: updatedAt }),
...(createdAt && { created_at: createdAt }),
version: encodeHitVersion(doc),
attributes: doc._source[type],
references: doc._source.references || [],
migrationVersion: doc._source.migrationVersion,
coreMigrationVersion: doc._source.coreMigrationVersion,
typeMigrationVersion: doc._source.typeMigrationVersion,
};
}

View file

@ -929,25 +929,35 @@ describe('SavedObjectsRepository', () => {
});
// Assert that both raw docs from the ES response are deserialized
expect(serializer.rawToSavedObject).toHaveBeenNthCalledWith(1, {
...response.items[0].create,
_source: {
...response.items[0].create._source,
namespaces: response.items[0].create._source.namespaces,
coreMigrationVersion: expect.any(String),
typeMigrationVersion: '1.1.1',
expect(serializer.rawToSavedObject).toHaveBeenNthCalledWith(
1,
{
...response.items[0].create,
_source: {
...response.items[0].create._source,
namespaces: response.items[0].create._source.namespaces,
coreMigrationVersion: expect.any(String),
typeMigrationVersion: '1.1.1',
},
_id: expect.stringMatching(
/^myspace:config:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/
),
},
_id: expect.stringMatching(/^myspace:config:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/),
});
expect(serializer.rawToSavedObject).toHaveBeenNthCalledWith(2, {
...response.items[1].create,
_source: {
...response.items[1].create._source,
namespaces: response.items[1].create._source.namespaces,
coreMigrationVersion: expect.any(String),
typeMigrationVersion: '1.1.1',
expect.any(Object)
);
expect(serializer.rawToSavedObject).toHaveBeenNthCalledWith(
2,
{
...response.items[1].create,
_source: {
...response.items[1].create._source,
namespaces: response.items[1].create._source.namespaces,
coreMigrationVersion: expect.any(String),
typeMigrationVersion: '1.1.1',
},
},
});
expect.any(Object)
);
// Assert that ID's are deserialized to remove the type and namespace
expect(result.saved_objects[0].id).toEqual(

View file

@ -17,7 +17,10 @@ import {
isSupportedEsServer,
isNotFoundFromUnsupportedServer,
} from '@kbn/core-elasticsearch-server-internal';
import type { BulkResolveError } from '@kbn/core-saved-objects-server';
import type {
BulkResolveError,
SavedObjectsRawDocParseOptions,
} from '@kbn/core-saved-objects-server';
import type {
SavedObjectsBaseOptions,
SavedObjectsIncrementCounterOptions,
@ -46,6 +49,7 @@ import type {
SavedObjectsClosePointInTimeResponse,
ISavedObjectsPointInTimeFinder,
SavedObjectsCreatePointInTimeFinderDependencies,
SavedObjectsResolveOptions,
SavedObjectsResolveResponse,
SavedObjectsCollectMultiNamespaceReferencesObject,
SavedObjectsUpdateObjectsSpacesObject,
@ -54,6 +58,7 @@ import type {
SavedObjectsClosePointInTimeOptions,
SavedObjectsCreatePointInTimeFinderOptions,
SavedObjectsFindOptions,
SavedObjectsGetOptions,
SavedObjectsBulkDeleteObject,
SavedObjectsBulkDeleteOptions,
SavedObjectsBulkDeleteResponse,
@ -310,6 +315,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
initialNamespaces,
version,
} = options;
const { migrationVersionCompatibility } = options;
if (!this._allowedTypes.includes(type)) {
throw SavedObjectsErrorHelpers.createUnsupportedTypeError(type);
}
@ -418,7 +424,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
}
return this.optionallyDecryptAndRedactSingleResult(
this._rawToSavedObject<T>({ ...raw, ...body }),
this._rawToSavedObject<T>({ ...raw, ...body }, { migrationVersionCompatibility }),
authorizationResult?.typeMap,
attributes
);
@ -432,7 +438,11 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
options: SavedObjectsCreateOptions = {}
): Promise<SavedObjectsBulkResponse<T>> {
const namespace = this.getCurrentNamespace(options.namespace);
const { overwrite = false, refresh = DEFAULT_REFRESH_SETTING } = options;
const {
migrationVersionCompatibility,
overwrite = false,
refresh = DEFAULT_REFRESH_SETTING,
} = options;
const time = getCurrentTime();
let preflightCheckIndexCounter = 0;
@ -667,10 +677,13 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
// When method == 'index' the bulkResponse doesn't include the indexed
// _source so we return rawMigratedDoc but have to spread the latest
// _seq_no and _primary_term values from the rawResponse.
return this._rawToSavedObject({
...rawMigratedDoc,
...{ _seq_no: rawResponse._seq_no, _primary_term: rawResponse._primary_term },
});
return this._rawToSavedObject(
{
...rawMigratedDoc,
...{ _seq_no: rawResponse._seq_no, _primary_term: rawResponse._primary_term },
},
{ migrationVersionCompatibility }
);
}),
};
@ -1295,6 +1308,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
filter,
preference,
aggs,
migrationVersionCompatibility,
} = options;
if (!type) {
@ -1444,7 +1458,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
saved_objects: body.hits.hits.map(
(hit: estypes.SearchHit<SavedObjectsRawDocSource>): SavedObjectsFindResult => ({
// @ts-expect-error @elastic/elasticsearch _source is optional
...this._rawToSavedObject(hit),
...this._rawToSavedObject(hit, { migrationVersionCompatibility }),
score: hit._score!,
sort: hit.sort,
})
@ -1480,9 +1494,10 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
*/
async bulkGet<T = unknown>(
objects: SavedObjectsBulkGetObject[] = [],
options: SavedObjectsBaseOptions = {}
options: SavedObjectsGetOptions = {}
): Promise<SavedObjectsBulkResponse<T>> {
const namespace = this.getCurrentNamespace(options.namespace);
const { migrationVersionCompatibility } = options;
if (objects.length === 0) {
return { saved_objects: [] };
@ -1628,7 +1643,9 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
}
// @ts-expect-error MultiGetHit._source is optional
return getSavedObjectFromSource(this._registry, type, id, doc);
return getSavedObjectFromSource(this._registry, type, id, doc, {
migrationVersionCompatibility,
});
}),
};
@ -1645,7 +1662,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
*/
async bulkResolve<T = unknown>(
objects: SavedObjectsBulkResolveObject[],
options: SavedObjectsBaseOptions = {}
options: SavedObjectsResolveOptions = {}
): Promise<SavedObjectsBulkResolveResponse<T>> {
const namespace = this.getCurrentNamespace(options.namespace);
const { resolved_objects: bulkResults } = await internalBulkResolve<T>({
@ -1681,9 +1698,10 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
async get<T = unknown>(
type: string,
id: string,
options: SavedObjectsBaseOptions = {}
options: SavedObjectsGetOptions = {}
): Promise<SavedObject<T>> {
const namespace = this.getCurrentNamespace(options.namespace);
const { migrationVersionCompatibility } = options;
if (!this._allowedTypes.includes(type)) {
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
@ -1719,7 +1737,9 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}
const result = getSavedObjectFromSource<T>(this._registry, type, id, body);
const result = getSavedObjectFromSource<T>(this._registry, type, id, body, {
migrationVersionCompatibility,
});
return this.optionallyDecryptAndRedactSingleResult(result, authorizationResult?.typeMap);
}
@ -1730,7 +1750,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
async resolve<T = unknown>(
type: string,
id: string,
options: SavedObjectsBaseOptions = {}
options: SavedObjectsResolveOptions = {}
): Promise<SavedObjectsResolveResponse<T>> {
const namespace = this.getCurrentNamespace(options.namespace);
const { resolved_objects: bulkResults } = await internalBulkResolve<T>({
@ -2588,12 +2608,16 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
return unique(types.map((t) => this.getIndexForType(t)));
}
private _rawToSavedObject<T = unknown>(raw: SavedObjectsRawDoc): SavedObject<T> {
const savedObject = this._serializer.rawToSavedObject(raw);
private _rawToSavedObject<T = unknown>(
raw: SavedObjectsRawDoc,
options?: SavedObjectsRawDocParseOptions
): SavedObject<T> {
const savedObject = this._serializer.rawToSavedObject(raw, options);
const { namespace, type } = savedObject;
if (this._registry.isSingleNamespace(type)) {
savedObject.namespaces = [SavedObjectsUtils.namespaceIdToString(namespace)];
}
return omit(savedObject, ['namespace']) as SavedObject<T>;
}

View file

@ -31,6 +31,7 @@ import type {
SavedObjectsBulkUpdateObject,
ISavedObjectsPointInTimeFinder,
SavedObjectsCreatePointInTimeFinderDependencies,
SavedObjectsResolveOptions,
SavedObjectsResolveResponse,
SavedObjectsCollectMultiNamespaceReferencesObject,
SavedObjectsUpdateObjectsSpacesObject,
@ -39,6 +40,7 @@ import type {
SavedObjectsClosePointInTimeOptions,
SavedObjectsCreatePointInTimeFinderOptions,
SavedObjectsFindOptions,
SavedObjectsGetOptions,
SavedObjectsBulkDeleteObject,
SavedObjectsBulkDeleteOptions,
SavedObjectsBulkDeleteResponse,
@ -103,7 +105,7 @@ export class SavedObjectsClient implements SavedObjectsClientContract {
/** {@inheritDoc SavedObjectsClientContract.bulkGet} */
async bulkGet<T = unknown>(
objects: SavedObjectsBulkGetObject[] = [],
options: SavedObjectsBaseOptions = {}
options: SavedObjectsGetOptions = {}
): Promise<SavedObjectsBulkResponse<T>> {
return await this._repository.bulkGet(objects, options);
}
@ -112,7 +114,7 @@ export class SavedObjectsClient implements SavedObjectsClientContract {
async get<T = unknown>(
type: string,
id: string,
options: SavedObjectsBaseOptions = {}
options: SavedObjectsGetOptions = {}
): Promise<SavedObject<T>> {
return await this._repository.get(type, id, options);
}
@ -120,7 +122,7 @@ export class SavedObjectsClient implements SavedObjectsClientContract {
/** {@inheritDoc SavedObjectsClientContract.bulkResolve} */
async bulkResolve<T = unknown>(
objects: SavedObjectsBulkResolveObject[],
options?: SavedObjectsBaseOptions
options?: SavedObjectsResolveOptions
): Promise<SavedObjectsBulkResolveResponse<T>> {
return await this._repository.bulkResolve(objects, options);
}
@ -129,7 +131,7 @@ export class SavedObjectsClient implements SavedObjectsClientContract {
async resolve<T = unknown>(
type: string,
id: string,
options: SavedObjectsBaseOptions = {}
options: SavedObjectsResolveOptions = {}
): Promise<SavedObjectsResolveResponse<T>> {
return await this._repository.resolve(type, id, options);
}

View file

@ -384,21 +384,20 @@ export const createRegistry = () => {
};
export const createSpySerializer = (registry: SavedObjectTypeRegistry) => {
const spyInstance = {
isRawSavedObject: jest.fn(),
rawToSavedObject: jest.fn(),
savedObjectToRaw: jest.fn(),
generateRawId: jest.fn(),
generateRawLegacyUrlAliasId: jest.fn(),
trimIdPrefix: jest.fn(),
};
const realInstance = new SavedObjectsSerializer(registry);
Object.keys(spyInstance).forEach((key) => {
// @ts-expect-error no proper way to do this with typing support
spyInstance[key].mockImplementation((...args) => realInstance[key](...args));
});
const serializer = new SavedObjectsSerializer(registry);
return spyInstance as unknown as jest.Mocked<SavedObjectsSerializer>;
for (const method of [
'isRawSavedObject',
'rawToSavedObject',
'savedObjectToRaw',
'generateRawId',
'generateRawLegacyUrlAliasId',
'trimIdPrefix',
] as Array<keyof SavedObjectsSerializer>) {
jest.spyOn(serializer, method);
}
return serializer as jest.Mocked<SavedObjectsSerializer>;
};
export const createDocumentMigrator = (registry: SavedObjectTypeRegistry) => {

View file

@ -44,6 +44,7 @@ export type {
ISavedObjectsPointInTimeFinder,
SavedObjectsCreatePointInTimeFinderDependencies,
SavedObjectsPitParams,
SavedObjectsResolveOptions,
SavedObjectsResolveResponse,
SavedObjectsCollectMultiNamespaceReferencesObject,
SavedObjectsUpdateObjectsSpacesResponseObject,
@ -54,6 +55,7 @@ export type {
SavedObjectsClosePointInTimeOptions,
SavedObjectsCreatePointInTimeFinderOptions,
SavedObjectsFindOptions,
SavedObjectsGetOptions,
SavedObjectsPointInTimeFinderClient,
SavedObjectsBulkDeleteObject,
SavedObjectsBulkDeleteOptions,

View file

@ -61,4 +61,6 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions {
* * For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used.
*/
initialNamespaces?: string[];
/** {@link SavedObjectsRawDocParseOptions.migrationVersionCompatibility} */
migrationVersionCompatibility?: 'compatible' | 'raw';
}

View file

@ -136,6 +136,8 @@ export interface SavedObjectsFindOptions {
* Search against a specific Point In Time (PIT) that you've opened with {@link SavedObjectsClient.openPointInTimeForType}.
*/
pit?: SavedObjectsPitParams;
/** {@link SavedObjectsRawDocParseOptions.migrationVersionCompatibility} */
migrationVersionCompatibility?: 'compatible' | 'raw';
}
/**

View file

@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { SavedObjectsBaseOptions } from './base';
/**
* Options for the saved objects get operation
*
* @public
*/
export interface SavedObjectsGetOptions extends SavedObjectsBaseOptions {
/** {@link SavedObjectsRawDocParseOptions.migrationVersionCompatibility} */
migrationVersionCompatibility?: 'compatible' | 'raw';
}

View file

@ -52,6 +52,7 @@ export type {
SavedObjectsFindResult,
SavedObjectsPitParams,
} from './find';
export type { SavedObjectsGetOptions } from './get';
export type {
SavedObjectsIncrementCounterField,
SavedObjectsIncrementCounterOptions,
@ -64,7 +65,7 @@ export type {
SavedObjectsRemoveReferencesToOptions,
SavedObjectsRemoveReferencesToResponse,
} from './remove_references_to';
export type { SavedObjectsResolveResponse } from './resolve';
export type { SavedObjectsResolveOptions, SavedObjectsResolveResponse } from './resolve';
export type { SavedObjectsUpdateResponse, SavedObjectsUpdateOptions } from './update';
export type {
SavedObjectsUpdateObjectsSpacesObject,

View file

@ -6,8 +6,19 @@
* Side Public License, v 1.
*/
import { SavedObjectsBaseOptions } from './base';
import type { SavedObject } from '../..';
/**
* Options for the saved objects get operation
*
* @public
*/
export interface SavedObjectsResolveOptions extends SavedObjectsBaseOptions {
/** {@link SavedObjectsRawDocParseOptions.migrationVersionCompatibility} */
migrationVersionCompatibility?: 'compatible' | 'raw';
}
/**
*
* @public

View file

@ -10,6 +10,7 @@ import type { SavedObject } from '..';
import type {
SavedObjectsBaseOptions,
SavedObjectsFindOptions,
SavedObjectsGetOptions,
SavedObjectsClosePointInTimeOptions,
SavedObjectsOpenPointInTimeOptions,
SavedObjectsCreatePointInTimeFinderOptions,
@ -19,6 +20,7 @@ import type {
SavedObjectsUpdateObjectsSpacesOptions,
SavedObjectsCollectMultiNamespaceReferencesObject,
SavedObjectsUpdateObjectsSpacesResponse,
SavedObjectsResolveOptions,
SavedObjectsResolveResponse,
ISavedObjectsPointInTimeFinder,
SavedObjectsRemoveReferencesToOptions,
@ -182,7 +184,7 @@ export interface SavedObjectsClientContract {
* Returns an array of objects by id
*
* @param objects - array of objects to get (contains id, type, and optional fields)
* @param options {@link SavedObjectsBaseOptions} - options for the bulk get operation
* @param options {@link SavedObjectsGetOptions} - options for the bulk get operation
* @returns the {@link SavedObjectsBulkResponse}
* @example
*
@ -193,7 +195,7 @@ export interface SavedObjectsClientContract {
*/
bulkGet<T = unknown>(
objects: SavedObjectsBulkGetObject[],
options?: SavedObjectsBaseOptions
options?: SavedObjectsGetOptions
): Promise<SavedObjectsBulkResponse<T>>;
/**
@ -201,12 +203,12 @@ export interface SavedObjectsClientContract {
*
* @param type - The type of the object to retrieve
* @param id - The ID of the object to retrieve
* @param options {@link SavedObjectsBaseOptions} - options for the get operation
* @param options {@link SavedObjectsGetOptions} - options for the get operation
*/
get<T = unknown>(
type: string,
id: string,
options?: SavedObjectsBaseOptions
options?: SavedObjectsGetOptions
): Promise<SavedObject<T>>;
/**
@ -215,7 +217,7 @@ export interface SavedObjectsClientContract {
* See documentation for `.resolve`.
*
* @param objects - an array of objects to resolve (contains id and type)
* @param options {@link SavedObjectsBaseOptions} - options for the bulk resolve operation
* @param options {@link SavedObjectsResolveOptions} - options for the bulk resolve operation
* @returns the {@link SavedObjectsBulkResolveResponse}
* @example
*
@ -230,7 +232,7 @@ export interface SavedObjectsClientContract {
*/
bulkResolve<T = unknown>(
objects: SavedObjectsBulkResolveObject[],
options?: SavedObjectsBaseOptions
options?: SavedObjectsResolveOptions
): Promise<SavedObjectsBulkResolveResponse<T>>;
/**
@ -246,13 +248,13 @@ export interface SavedObjectsClientContract {
*
* @param type - The type of SavedObject to retrieve
* @param id - The ID of the SavedObject to retrieve
* @param options {@link SavedObjectsBaseOptions} - options for the resolve operation
* @param options {@link SavedObjectsResolveOptions} - options for the resolve operation
* @returns the {@link SavedObjectsResolveResponse}
*/
resolve<T = unknown>(
type: string,
id: string,
options?: SavedObjectsBaseOptions
options?: SavedObjectsResolveOptions
): Promise<SavedObjectsResolveResponse<T>>;
/**

View file

@ -10,6 +10,7 @@ import type { SavedObject } from '@kbn/core-saved-objects-common';
import type {
SavedObjectsBaseOptions,
SavedObjectsFindOptions,
SavedObjectsGetOptions,
SavedObjectsClosePointInTimeOptions,
SavedObjectsOpenPointInTimeOptions,
SavedObjectsCreatePointInTimeFinderOptions,
@ -19,6 +20,7 @@ import type {
SavedObjectsUpdateObjectsSpacesOptions,
SavedObjectsCollectMultiNamespaceReferencesObject,
SavedObjectsUpdateObjectsSpacesResponse,
SavedObjectsResolveOptions,
SavedObjectsResolveResponse,
ISavedObjectsPointInTimeFinder,
SavedObjectsRemoveReferencesToOptions,
@ -78,6 +80,7 @@ export interface ISavedObjectsRepository {
* @property {boolean} [options.overwrite=false]
* @property {string} [options.namespace]
* @property {array} [options.references=[]] - [{ name, type, id }]
* @property {string} [options.migrationVersionCompatibility]
* @returns {promise} the created saved object { id, type, version, attributes }
*/
create<T = unknown>(
@ -93,6 +96,7 @@ export interface ISavedObjectsRepository {
* @param {object} [options={}] {@link SavedObjectsCreateOptions} - options for the bulk create operation
* @property {boolean} [options.overwrite=false] - overwrites existing documents
* @property {string} [options.namespace]
* @property {string} [options.migrationVersionCompatibility]
* @returns {promise} - {saved_objects: [[{ id, type, version, references, attributes, error: { message } }]}
*/
bulkCreate<T = unknown>(
@ -177,7 +181,8 @@ export interface ISavedObjectsRepository {
* Returns an array of objects by id
*
* @param {array} objects - an array of objects containing id, type and optionally fields
* @param {object} [options={}] {@link SavedObjectsBaseOptions} - options for the bulk get operation
* @param {object} [options={}] {@link SavedObjectsGetOptions} - options for the bulk get operation
* @property {string} [options.migrationVersionCompatibility]
* @property {string} [options.namespace]
* @returns {promise} - { saved_objects: [{ id, type, version, attributes }] }
* @example
@ -189,14 +194,15 @@ export interface ISavedObjectsRepository {
*/
bulkGet<T = unknown>(
objects: SavedObjectsBulkGetObject[],
options?: SavedObjectsBaseOptions
options?: SavedObjectsGetOptions
): Promise<SavedObjectsBulkResponse<T>>;
/**
* Resolves an array of objects by id, using any legacy URL aliases if they exist
*
* @param {array} objects - an array of objects containing id, type
* @param {object} [options={}] {@link SavedObjectsBaseOptions} - options for the bulk resolve operation
* @param {object} [options={}] {@link SavedObjectsResolveOptions} - options for the bulk resolve operation
* @property {string} [options.migrationVersionCompatibility]
* @property {string} [options.namespace]
* @returns {promise} - { resolved_objects: [{ saved_object, outcome }] }
* @example
@ -208,7 +214,7 @@ export interface ISavedObjectsRepository {
*/
bulkResolve<T = unknown>(
objects: SavedObjectsBulkResolveObject[],
options?: SavedObjectsBaseOptions
options?: SavedObjectsResolveOptions
): Promise<SavedObjectsBulkResolveResponse<T>>;
/**
@ -216,14 +222,15 @@ export interface ISavedObjectsRepository {
*
* @param {string} type - the type of the object to get
* @param {string} id - the ID of the object to get
* @param {object} [options={}] {@link SavedObjectsBaseOptions} - options for the get operation
* @param {object} [options={}] {@link SavedObjectsGetOptions} - options for the get operation
* @property {string} [options.migrationVersionCompatibility]
* @property {string} [options.namespace]
* @returns {promise} - { id, type, version, attributes }
*/
get<T = unknown>(
type: string,
id: string,
options?: SavedObjectsBaseOptions
options?: SavedObjectsGetOptions
): Promise<SavedObject<T>>;
/**
@ -231,14 +238,15 @@ export interface ISavedObjectsRepository {
*
* @param {string} type - the type of the object to resolve
* @param {string} id - the id of the object to resolve
* @param {object} [options={}] {@link SavedObjectsBaseOptions} - options for the resolve operation
* @param {object} [options={}] {@link SavedObjectsResolveOptions} - options for the resolve operation
* @property {string} [options.migrationVersionCompatibility]
* @property {string} [options.namespace]
* @returns {promise} - { saved_object, outcome }
*/
resolve<T = unknown>(
type: string,
id: string,
options?: SavedObjectsBaseOptions
options?: SavedObjectsResolveOptions
): Promise<SavedObjectsResolveResponse<T>>;
/**

View file

@ -104,13 +104,59 @@ describe('#rawToSavedObject', () => {
});
});
test(`if _source.migrationVersion is unspecified it doesn't set migrationVersion`, () => {
const actual = singleNamespaceSerializer.rawToSavedObject({
_id: 'foo:bar',
_source: {
type: 'foo',
test('derives original `migrationVersion` in compatibility mode', () => {
const actual = singleNamespaceSerializer.rawToSavedObject(
{
_id: 'foo:bar',
_source: {
migrationVersion: { foo: '1.0.0' },
type: 'foo',
typeMigrationVersion: '1.2.3',
},
},
});
{ migrationVersionCompatibility: 'compatible' }
);
expect(actual).toHaveProperty('migrationVersion', { foo: '1.0.0' });
});
test('derives `migrationVersion` in compatibility mode', () => {
const actual = singleNamespaceSerializer.rawToSavedObject(
{
_id: 'foo:bar',
_source: {
type: 'foo',
typeMigrationVersion: '1.2.3',
},
},
{ migrationVersionCompatibility: 'compatible' }
);
expect(actual).toHaveProperty('migrationVersion', { foo: '1.2.3' });
});
test('does not derive `migrationVersion` if there is no type version', () => {
const actual = singleNamespaceSerializer.rawToSavedObject(
{
_id: 'foo:bar',
_source: {
type: 'foo',
},
},
{ migrationVersionCompatibility: 'compatible' }
);
expect(actual).not.toHaveProperty('migrationVersion');
});
test('does not derive `migrationVersion` in raw mode', () => {
const actual = singleNamespaceSerializer.rawToSavedObject(
{
_id: 'foo:bar',
_source: {
type: 'foo',
typeMigrationVersion: '1.2.3',
},
},
{ migrationVersionCompatibility: 'raw' }
);
expect(actual).not.toHaveProperty('migrationVersion');
});

View file

@ -85,16 +85,18 @@ export class SavedObjectsSerializer implements ISavedObjectsSerializer {
): SavedObjectSanitizedDoc<T> {
this.checkIsRawSavedObject(doc, options); // throws a descriptive error if the document is not a saved object
const { namespaceTreatment = 'strict' } = options;
const { namespaceTreatment = 'strict', migrationVersionCompatibility = 'raw' } = options;
const { _id, _source, _seq_no, _primary_term } = doc;
const {
type,
namespaces,
originId,
migrationVersion,
references,
coreMigrationVersion,
typeMigrationVersion,
migrationVersion = migrationVersionCompatibility === 'compatible' && typeMigrationVersion
? { [type]: typeMigrationVersion }
: undefined,
} = _source;
const version =

View file

@ -76,7 +76,10 @@ export const registerBulkCreateRoute = (
if (!allowHttpApiAccess) {
throwIfAnyTypeNotVisibleByAPI(typesToCheck, savedObjects.typeRegistry);
}
const result = await savedObjects.client.bulkCreate(req.body, { overwrite });
const result = await savedObjects.client.bulkCreate(req.body, {
overwrite,
migrationVersionCompatibility: 'compatible',
});
return res.ok({ body: result });
})
);

View file

@ -57,7 +57,9 @@ export const registerBulkGetRoute = (
if (!allowHttpApiAccess) {
throwIfAnyTypeNotVisibleByAPI(typesToCheck, savedObjects.typeRegistry);
}
const result = await savedObjects.client.bulkGet(req.body);
const result = await savedObjects.client.bulkGet(req.body, {
migrationVersionCompatibility: 'compatible',
});
return res.ok({ body: result });
})
);

View file

@ -55,7 +55,9 @@ export const registerBulkResolveRoute = (
if (!allowHttpApiAccess) {
throwIfAnyTypeNotVisibleByAPI(typesToCheck, savedObjects.typeRegistry);
}
const result = await savedObjects.client.bulkResolve(req.body);
const result = await savedObjects.client.bulkResolve(req.body, {
migrationVersionCompatibility: 'compatible',
});
return res.ok({ body: result });
})
);

View file

@ -90,6 +90,7 @@ export const registerCreateRoute = (
typeMigrationVersion,
references,
initialNamespaces,
migrationVersionCompatibility: 'compatible' as const,
};
const result = await savedObjects.client.create(type, attributes, options);
return res.ok({ body: result });

View file

@ -123,6 +123,7 @@ export const registerFindRoute = (
filter: query.filter,
aggs,
namespaces,
migrationVersionCompatibility: 'compatible',
});
return res.ok({ body: result });

View file

@ -56,7 +56,9 @@ export const registerGetRoute = (
throwIfTypeNotVisibleByAPI(type, savedObjects.typeRegistry);
}
const object = await savedObjects.client.get(type, id);
const object = await savedObjects.client.get(type, id, {
migrationVersionCompatibility: 'compatible',
});
return res.ok({ body: object });
})
);

View file

@ -49,7 +49,9 @@ export const registerResolveRoute = (
if (!allowHttpApiAccess) {
throwIfTypeNotVisibleByAPI(type, savedObjects.typeRegistry);
}
const result = await savedObjects.client.resolve(type, id);
const result = await savedObjects.client.resolve(type, id, {
migrationVersionCompatibility: 'compatible',
});
return res.ok({ body: result });
})
);

View file

@ -143,4 +143,12 @@ export interface SavedObjectsRawDocParseOptions {
* If not specified, the default treatment is `strict`.
*/
namespaceTreatment?: 'strict' | 'lax';
/**
* Optional setting to allow compatible handling of the `migrationVersion` field.
* This is needed to return the `migrationVersion` field in the same format as it was before migrating to the `typeMigrationVersion` property.
*
* @default 'raw'
*/
migrationVersionCompatibility?: 'compatible' | 'raw';
}

View file

@ -79,9 +79,10 @@ describe('POST /api/saved_objects/_bulk_create with allowApiAccess true', () =>
.expect(200);
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1);
const args = savedObjectsClient.bulkCreate.mock.calls[0];
expect(args[1]).toEqual({ overwrite: true });
expect(savedObjectsClient.bulkCreate).nthCalledWith(1, expect.anything(), {
migrationVersionCompatibility: 'compatible',
overwrite: true,
});
const result = await supertest(httpSetup.server.listener)
.post('/api/saved_objects/_bulk_create')
.send([

View file

@ -148,8 +148,8 @@ describe('POST /api/saved_objects/_bulk_create', () => {
.expect(200);
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1);
const args = savedObjectsClient.bulkCreate.mock.calls[0];
expect(args[1]).toEqual({ overwrite: true });
const [[, options]] = savedObjectsClient.bulkCreate.mock.calls;
expect(options).toEqual({ migrationVersionCompatibility: 'compatible', overwrite: true });
});
it('returns with status 400 when a type is hidden from the HTTP APIs', async () => {

View file

@ -112,7 +112,9 @@ describe('POST /api/saved_objects/_bulk_get', () => {
.expect(200);
expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1);
expect(savedObjectsClient.bulkGet).toHaveBeenCalledWith(docs);
expect(savedObjectsClient.bulkGet).toHaveBeenCalledWith(docs, {
migrationVersionCompatibility: 'compatible',
});
});
it('returns with status 400 when a type is hidden from the HTTP APIs', async () => {

View file

@ -116,7 +116,9 @@ describe('POST /api/saved_objects/_bulk_resolve', () => {
.expect(200);
expect(savedObjectsClient.bulkResolve).toHaveBeenCalledTimes(1);
expect(savedObjectsClient.bulkResolve).toHaveBeenCalledWith(docs);
expect(savedObjectsClient.bulkResolve).toHaveBeenCalledWith(docs, {
migrationVersionCompatibility: 'compatible',
});
});
it('returns with status 400 when a type is hidden from the HTTP APIs', async () => {

View file

@ -115,7 +115,12 @@ describe('POST /api/saved_objects/{type}', () => {
expect(savedObjectsClient.create).toHaveBeenCalledWith(
'index-pattern',
{ title: 'Testing' },
{ overwrite: false, id: undefined, migrationVersion: undefined }
{
overwrite: false,
id: undefined,
migrationVersion: undefined,
migrationVersionCompatibility: 'compatible',
}
);
});
@ -135,7 +140,7 @@ describe('POST /api/saved_objects/{type}', () => {
expect(args).toEqual([
'index-pattern',
{ title: 'Testing' },
{ overwrite: false, id: 'logstash-*' },
{ overwrite: false, id: 'logstash-*', migrationVersionCompatibility: 'compatible' },
]);
});

View file

@ -176,6 +176,7 @@ describe('GET /api/saved_objects/_find', () => {
defaultSearchOperator: 'OR',
hasReferenceOperator: 'OR',
hasNoReferenceOperator: 'OR',
migrationVersionCompatibility: 'compatible',
});
});
@ -186,7 +187,7 @@ describe('GET /api/saved_objects/_find', () => {
expect(savedObjectsClient.find).toHaveBeenCalledTimes(1);
const options = savedObjectsClient.find.mock.calls[0][0];
const [[options]] = savedObjectsClient.find.mock.calls;
expect(options).toEqual(expect.objectContaining({ perPage: 10, page: 50 }));
});

View file

@ -118,9 +118,9 @@ describe('GET /api/saved_objects/{type}/{id}', () => {
.expect(200);
expect(savedObjectsClient.get).toHaveBeenCalled();
const args = savedObjectsClient.get.mock.calls[0];
expect(args).toEqual(['index-pattern', 'logstash-*']);
expect(savedObjectsClient.get).nthCalledWith(1, 'index-pattern', 'logstash-*', {
migrationVersionCompatibility: 'compatible',
});
});
it('returns with status 400 when a type is hidden from the http APIs', async () => {

View file

@ -118,9 +118,9 @@ describe('GET /api/saved_objects/resolve/{type}/{id}', () => {
.expect(200);
expect(savedObjectsClient.resolve).toHaveBeenCalled();
const args = savedObjectsClient.resolve.mock.calls[0];
expect(args).toEqual(['index-pattern', 'logstash-*']);
expect(savedObjectsClient.resolve).nthCalledWith(1, 'index-pattern', 'logstash-*', {
migrationVersionCompatibility: 'compatible',
});
});
it('returns with status 400 is a type is hidden from the HTTP APIs', async () => {

View file

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

View file

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

View file

@ -49,6 +49,7 @@ export default function ({ getService }: FtrProviderContext) {
expect(resp.body).to.eql({
id: resp.body.id,
type: 'visualization',
migrationVersion: resp.body.migrationVersion,
coreMigrationVersion: '8.8.0',
typeMigrationVersion: resp.body.typeMigrationVersion,
updated_at: resp.body.updated_at,
@ -60,6 +61,7 @@ export default function ({ getService }: FtrProviderContext) {
references: [],
namespaces: ['default'],
});
expect(resp.body.migrationVersion).to.be.ok();
expect(resp.body.typeMigrationVersion).to.be.ok();
});
});

View file

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

View file

@ -36,6 +36,7 @@ export default function ({ getService }: FtrProviderContext) {
updated_at: resp.body.updated_at,
created_at: resp.body.created_at,
version: resp.body.version,
migrationVersion: resp.body.migrationVersion,
coreMigrationVersion: '8.8.0',
typeMigrationVersion: resp.body.typeMigrationVersion,
attributes: {
@ -56,6 +57,7 @@ export default function ({ getService }: FtrProviderContext) {
],
namespaces: ['default'],
});
expect(resp.body.migrationVersion).to.be.ok();
expect(resp.body.typeMigrationVersion).to.be.ok();
}));

View file

@ -41,6 +41,7 @@ export default function ({ getService }: FtrProviderContext) {
updated_at: '2015-01-01T00:00:00.000Z',
created_at: '2015-01-01T00:00:00.000Z',
version: resp.body.saved_object.version,
migrationVersion: resp.body.saved_object.migrationVersion,
coreMigrationVersion: '8.8.0',
typeMigrationVersion: resp.body.saved_object.typeMigrationVersion,
attributes: {
@ -63,6 +64,7 @@ export default function ({ getService }: FtrProviderContext) {
},
outcome: 'exactMatch',
});
expect(resp.body.saved_object.migrationVersion).to.be.ok();
expect(resp.body.saved_object.typeMigrationVersion).to.be.ok();
}));