APEX-54 Stricter type checking for unsafe_transform functions (#222973)

Address https://github.com/elastic/kibana/issues/216061

Adds an indirection layer in the definition of the `transformFn:`, which
forces devs to explicitly define the types of the documents being
transformed.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Gerard Soldevila 2025-06-13 12:29:13 +02:00 committed by GitHub
parent 6aafb4f7f4
commit 852f416a01
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 471 additions and 181 deletions

View file

@ -350,12 +350,24 @@ Used to execute an arbitrary transformation function.
*Usage example:*
```ts
let change: SavedObjectsModelUnsafeTransformChange = {
// Please define your transform function on a separate const.
// Use explicit types for the generic arguments, as shown below.
// This will reduce the chances of introducing bugs.
const transformFn: SavedObjectModelUnsafeTransformFn<BeforeType, AfterType> = (
doc: SavedObjectModelTransformationDoc<BeforeType>
) => {
const attributes: AfterType = {
...doc.attributes,
someAddedField: 'defaultValue',
};
return { document: { ...doc, attributes } };
};
// this is how you would specify a change in the changes: []
const change: SavedObjectsModelUnsafeTransformChange = {
type: 'unsafe_transform',
transformFn: (document) => {
document.attributes.someAddedField = 'defaultValue';
return { document };
},
transformFn: (typeSafeGuard) => typeSafeGuard(transformFn),
};
```
@ -1111,7 +1123,3 @@ Which is why, when using this option, the API consumer needs to make sure that *
#### Using `bulkUpdate` for fields with large `json` blobs [_using_bulkupdate_for_fields_with_large_json_blobs]
The savedObjects `bulkUpdate` API will update documents client-side and then reindex the updated documents. These update operations are done in-memory, and cause memory constraint issues when updating many objects with large `json` blobs stored in some fields. As such, we recommend against using `bulkUpdate` for savedObjects that: - use arrays (as these tend to be large objects) - store large `json` blobs in some fields

View file

@ -104,9 +104,10 @@ export class EsoModelVersionExample
changes: [
{
type: 'unsafe_transform',
transformFn: (document) => {
transformFn: (typeSafeGuard) =>
typeSafeGuard((document) => {
return { document };
},
}),
},
],
schemas: {

View file

@ -185,7 +185,7 @@ describe('buildModelVersionTransformFn', () => {
const changes: SavedObjectsModelChange[] = [
{
type: 'unsafe_transform',
transformFn,
transformFn: (typeSafeGuard) => typeSafeGuard(transformFn),
},
];
@ -203,11 +203,20 @@ describe('buildModelVersionTransformFn', () => {
const changes: SavedObjectsModelChange[] = [
{
type: 'unsafe_transform',
transformFn: (document, ctx) => {
transformFn: (typeSafeGuard) =>
typeSafeGuard(
(
document: SavedObjectModelTransformationDoc<{
oldProp?: string;
newProp: string;
}>,
ctx
) => {
delete document.attributes.oldProp;
document.attributes.newProp = 'newValue';
return { document };
},
}
),
},
];
@ -305,10 +314,17 @@ describe('buildModelVersionTransformFn', () => {
},
{
type: 'unsafe_transform',
transformFn: (document) => {
transformFn: (typeSafeGuard) =>
typeSafeGuard(
(
document: SavedObjectModelTransformationDoc<{
unsafeNewProp: string;
}>
) => {
document.attributes.unsafeNewProp = 'unsafeNewValue';
return { document };
},
}
),
},
];

View file

@ -14,6 +14,10 @@ import type {
SavedObjectsModelUnsafeTransformChange,
SavedObjectsModelDataBackfillChange,
SavedObjectsModelDataRemovalChange,
SavedObjectModelUnsafeTransformFn,
SavedObjectModelTransformationDoc,
SavedObjectModelTransformationContext,
SavedObjectModelTransformationResult,
} from '@kbn/core-saved-objects-server';
/**
@ -59,11 +63,25 @@ export const dataBackfillChangeToTransformFn = (
};
};
// we must force 'any' type on the generic arguments of the received function
// otherwise they are 'unknown' and they cannot be cast to the PreviousAttributes and NewAttributes
// generic arguments needed by the typeSafeGuard functions
type TypeSafeGuardUnsafeTransformFn = (
fn: SavedObjectModelUnsafeTransformFn<any, any>
) => SavedObjectModelUnsafeTransformFn;
const typeSafeGuard: TypeSafeGuardUnsafeTransformFn = (
fn: SavedObjectModelUnsafeTransformFn
): SavedObjectModelTransformationFn => {
return (
document: SavedObjectModelTransformationDoc,
context: SavedObjectModelTransformationContext
): SavedObjectModelTransformationResult => fn(document, context);
};
export const unsafeTransformChangeToTransformFn = (
change: SavedObjectsModelUnsafeTransformChange
): SavedObjectModelTransformationFn => {
return change.transformFn;
};
): SavedObjectModelTransformationFn => change.transformFn(typeSafeGuard);
const mergeTransformFunctions = (
transformFns: SavedObjectModelTransformationFn[]

View file

@ -276,12 +276,24 @@ Used to execute an arbitrary transformation function.
*Usage example:*
```ts
let change: SavedObjectsModelUnsafeTransformChange = {
// Please define your transform function on a separate const.
// Use explicit types for the generic arguments, as shown below.
// This will reduce the chances of introducing bugs.
const transformFn: SavedObjectModelUnsafeTransformFn<BeforeType, AfterType> = (
doc: SavedObjectModelTransformationDoc<BeforeType>
) => {
const attributes: AfterType = {
...doc.attributes,
someAddedField: 'defaultValue',
};
return { document: { ...doc, attributes } };
};
// this is how you would specify a change in the changes: []
const change: SavedObjectsModelUnsafeTransformChange = {
type: 'unsafe_transform',
transformFn: (document) => {
document.attributes.someAddedField = 'defaultValue';
return { document };
},
transformFn: (typeSafeGuard) => typeSafeGuard(transformFn),
};
```

View file

@ -0,0 +1,96 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { SavedObjectDoc } from '../serialization';
import { SavedObjectsModelUnsafeTransformChange } from './model_change';
import {
SavedObjectModelTransformationContext,
SavedObjectModelTransformationDoc,
SavedObjectModelUnsafeTransformFn,
} from './transformations';
interface BeforeType {
a: boolean;
}
interface AfterType extends BeforeType {
aString: string;
}
describe('test', () => {
const testDoc: SavedObjectDoc<BeforeType> = {
id: 'someType:docId',
type: 'someType',
attributes: {
a: false,
},
};
const testContext: SavedObjectModelTransformationContext = {
log: {
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
},
modelVersion: 1,
namespaceType: 'agnostic',
};
it('TS fails if users try to define untyped transform functions', () => {
const untypedTransformFn: SavedObjectModelUnsafeTransformFn = (doc) => {
const attributes: AfterType = {
// @ts-expect-error
...doc.attributes,
// @ts-expect-error
aString: doc.attributes.a ? 'true' : 'false',
};
return { document: { ...doc, attributes } };
};
expect(untypedTransformFn(testDoc, testContext)).toMatchInlineSnapshot(`
Object {
"document": Object {
"attributes": Object {
"a": false,
"aString": "false",
},
"id": "someType:docId",
"type": "someType",
},
}
`);
});
it('allows defining transform changes', () => {
const transformFn: SavedObjectModelUnsafeTransformFn<BeforeType, AfterType> = (
doc: SavedObjectModelTransformationDoc<BeforeType>
) => {
const attributes: AfterType = {
...doc.attributes,
aString: doc.attributes.a ? 'true' : 'false',
};
return { document: { ...doc, attributes } };
};
// this is how you would specify a change in the changes: []
const change: SavedObjectsModelUnsafeTransformChange = {
type: 'unsafe_transform',
transformFn: (typeSafeGuard) => typeSafeGuard(transformFn),
};
expect(change).toMatchInlineSnapshot(`
Object {
"transformFn": [Function],
"type": "unsafe_transform",
}
`);
});
});

View file

@ -10,6 +10,7 @@
import type { SavedObjectsMappingProperties } from '../mapping_definition';
import type {
SavedObjectModelDataBackfillFn,
SavedObjectModelTransformationFn,
SavedObjectModelUnsafeTransformFn,
} from './transformations';
@ -143,15 +144,26 @@ export interface SavedObjectsModelDataRemovalChange {
/**
* A {@link SavedObjectsModelChange | model change} executing an arbitrary transformation function.
*
* Please define your transform function on a separate const.
* Use explicit types for the generic arguments, as shown below.
* This will reduce the chances of introducing bugs.
* @example
* ```ts
* let change: SavedObjectsModelUnsafeTransformChange = {
* const transformFn: SavedObjectModelUnsafeTransformFn<BeforeType, AfterType> = (
* doc: SavedObjectModelTransformationDoc<BeforeType>
* ) => {
* const attributes: AfterType = {
* ...doc.attributes,
* someAddedField: 'defaultValue',
* };
*
* return { document: { ...doc, attributes } };
* };
*
* // this is how you would specify a change in the changes: []
* const change: SavedObjectsModelUnsafeTransformChange = {
* type: 'unsafe_transform',
* transformFn: (document) => {
* document.attributes.someAddedField = 'defaultValue';
* return { document };
* },
* transformFn: (typeSafeGuard) => typeSafeGuard(transformFn),
* };
* ```
*
@ -160,13 +172,14 @@ export interface SavedObjectsModelDataRemovalChange {
* Those should only be used when there's no other way to cover one's migration needs.
* Please reach out to the Core team if you think you need to use this, as you theoretically shouldn't.
*/
export interface SavedObjectsModelUnsafeTransformChange<
PreviousAttributes = any,
NewAttributes = any
> {
export interface SavedObjectsModelUnsafeTransformChange {
type: 'unsafe_transform';
/**
* The transform function to execute.
*/
transformFn: SavedObjectModelUnsafeTransformFn<PreviousAttributes, NewAttributes>;
transformFn: (
typeSafeGuard: <PreviousAttributes, NewAttributes>(
fn: SavedObjectModelUnsafeTransformFn<PreviousAttributes, NewAttributes>
) => SavedObjectModelTransformationFn
) => SavedObjectModelTransformationFn;
}

View file

@ -8,7 +8,10 @@
*/
import type { SavedObjectsBulkCreateObject } from '@kbn/core-saved-objects-api-server';
import type { SavedObjectsType } from '@kbn/core-saved-objects-server';
import type {
SavedObjectModelUnsafeTransformFn,
SavedObjectsType,
} from '@kbn/core-saved-objects-server';
import type { IndexTypesMap } from '@kbn/core-saved-objects-base-server-internal';
import type { ElasticsearchClientWrapperFactory } from './elasticsearch_client_wrapper';
import {
@ -45,6 +48,18 @@ const defaultType: SavedObjectsType<any> = {
export const REMOVED_TYPES = ['deprecated', 'server'];
interface ComplexTypeV0 {
name: string;
value: number;
firstHalf: boolean;
}
interface ComplexTypeV1 {
name: string;
value: number;
firstHalf: boolean;
}
export const baselineTypes: Array<SavedObjectsType<any>> = [
{
...defaultType,
@ -123,8 +138,18 @@ export const getCompatibleBaselineTypes = (removedTypes: string[]) =>
}
});
export const getReindexingBaselineTypes = (removedTypes: string[]) =>
getUpToDateBaselineTypes(removedTypes).map<SavedObjectsType>((type) => {
export const getReindexingBaselineTypes = (removedTypes: string[]) => {
const transformComplex: SavedObjectModelUnsafeTransformFn<ComplexTypeV0, ComplexTypeV1> = (
doc
) => {
if (doc.attributes.value % 100 === 0) {
throw new Error(
`Cannot convert 'complex' objects with values that are multiple of 100 ${doc.id}`
);
}
return { document: doc };
};
return getUpToDateBaselineTypes(removedTypes).map<SavedObjectsType>((type) => {
// introduce an incompatible change
if (type.name === 'complex') {
return {
@ -152,14 +177,7 @@ export const getReindexingBaselineTypes = (removedTypes: string[]) =>
},
{
type: 'unsafe_transform',
transformFn: (doc) => {
if (doc.attributes.value % 100 === 0) {
throw new Error(
`Cannot convert 'complex' objects with values that are multiple of 100 ${doc.id}`
);
}
return { document: doc };
},
transformFn: (typeSafeGuard) => typeSafeGuard(transformComplex),
},
],
},
@ -192,6 +210,7 @@ export const getReindexingBaselineTypes = (removedTypes: string[]) =>
return type;
}
});
};
export interface GetBaselineDocumentsParams {
documentsPerType?: number;

View file

@ -107,9 +107,10 @@ describe('ZDT upgrades - encountering conversion failures', () => {
changes: [
{
type: 'unsafe_transform',
transformFn: (doc) => {
transformFn: (typeSafeGuard) =>
typeSafeGuard((doc) => {
throw new Error(`error from ${doc.id}`);
},
}),
},
],
},
@ -122,12 +123,13 @@ describe('ZDT upgrades - encountering conversion failures', () => {
changes: [
{
type: 'unsafe_transform',
transformFn: (doc) => {
transformFn: (typeSafeGuard) =>
typeSafeGuard((doc) => {
if (doc.id === 'b-0') {
throw new Error(`error from ${doc.id}`);
}
return { document: doc };
},
}),
},
],
},

View file

@ -8,7 +8,11 @@
*/
import { schema } from '@kbn/config-schema';
import type { SavedObjectsType, SavedObject } from '@kbn/core-saved-objects-server';
import type {
SavedObjectsType,
SavedObject,
SavedObjectModelUnsafeTransformFn,
} from '@kbn/core-saved-objects-server';
import { createModelVersionTestMigrator } from './model_version_tester';
const createObject = (parts: Partial<SavedObject>): SavedObject => {
@ -22,6 +26,26 @@ const createObject = (parts: Partial<SavedObject>): SavedObject => {
};
describe('modelVersionTester', () => {
interface V3 {
someExistingField: string;
}
interface V4 extends V3 {
fieldUnsafelyAddedInV4: string;
}
const testTypeUnsafeTransform: SavedObjectModelUnsafeTransformFn<V3, V4> = (doc) => {
const transformedDoc = {
...doc,
attributes: {
...doc.attributes,
fieldUnsafelyAddedInV4: '4',
},
};
return { document: transformedDoc };
};
const testType: SavedObjectsType = {
name: 'test-type',
hidden: false,
@ -90,14 +114,7 @@ describe('modelVersionTester', () => {
changes: [
{
type: 'unsafe_transform',
transformFn: (doc) => {
doc.attributes = {
...doc.attributes,
fieldUnsafelyAddedInV4: '4',
};
return { document: doc };
},
transformFn: (typeSafeGuard) => typeSafeGuard(testTypeUnsafeTransform),
},
],
schemas: {

View file

@ -9,6 +9,10 @@ import { logger } from 'elastic-apm-node';
import type {
SavedObjectModelTransformationContext,
SavedObjectModelTransformationDoc,
SavedObjectModelTransformationFn,
SavedObjectModelTransformationResult,
SavedObjectModelUnsafeTransformFn,
SavedObjectsModelUnsafeTransformChange,
} from '@kbn/core-saved-objects-server';
@ -17,6 +21,15 @@ import type { EncryptedSavedObjectTypeRegistration } from './crypto';
import { EncryptionError, EncryptionErrorOperation } from './crypto';
import { encryptedSavedObjectsServiceMock } from './crypto/index.mock';
const dummyTypeSafeGuard = (
fn: SavedObjectModelUnsafeTransformFn<any, any>
): SavedObjectModelTransformationFn => {
return (
document: SavedObjectModelTransformationDoc,
ctx: SavedObjectModelTransformationContext
): SavedObjectModelTransformationResult => fn(document, ctx);
};
describe('create ESO model version', () => {
afterEach(() => {
jest.clearAllMocks();
@ -47,9 +60,10 @@ describe('create ESO model version', () => {
changes: [
{
type: 'unsafe_transform',
transformFn: (document) => {
transformFn: (typeSafeGuard) =>
typeSafeGuard((document) => {
return { document };
},
}),
},
],
},
@ -99,10 +113,11 @@ describe('create ESO model version', () => {
changes: [
{
type: 'unsafe_transform',
transformFn: (document) => {
transformFn: (typeSafeGuard) =>
typeSafeGuard((document: SavedObjectModelTransformationDoc<{ three: string }>) => {
document.attributes.three = '3';
return { document };
},
}),
},
{
type: 'data_removal',
@ -110,10 +125,11 @@ describe('create ESO model version', () => {
},
{
type: 'unsafe_transform',
transformFn: (document) => {
transformFn: (typeSafeGuard) =>
typeSafeGuard((document: SavedObjectModelTransformationDoc<{ two: string }>) => {
document.attributes.two = '2';
return { document: { ...document, new_prop_1: 'new prop 1' } };
},
}),
},
{
type: 'data_backfill',
@ -123,10 +139,11 @@ describe('create ESO model version', () => {
},
{
type: 'unsafe_transform',
transformFn: (document) => {
transformFn: (typeSafeGuard) =>
typeSafeGuard((document: SavedObjectModelTransformationDoc<{ four: string }>) => {
document.attributes.four = '4';
return { document: { ...document, new_prop_2: 'new prop 2' } };
},
}),
},
],
},
@ -157,7 +174,7 @@ describe('create ESO model version', () => {
) as SavedObjectsModelUnsafeTransformChange[];
expect(unsafeTransforms.length === 1);
const result = unsafeTransforms[0].transformFn(
const result = unsafeTransforms[0].transformFn(dummyTypeSafeGuard)(
{
id: '123',
type: 'known-type-1',
@ -204,9 +221,10 @@ describe('create ESO model version', () => {
changes: [
{
type: 'unsafe_transform',
transformFn: (document) => {
transformFn: (typeSafeGuard) =>
typeSafeGuard((document) => {
return { document };
},
}),
},
],
},
@ -228,7 +246,7 @@ describe('create ESO model version', () => {
) as SavedObjectsModelUnsafeTransformChange[];
expect(unsafeTransforms.length === 1);
expect(() => {
unsafeTransforms[0].transformFn(
unsafeTransforms[0].transformFn(dummyTypeSafeGuard)(
{
id: '123',
type: 'known-type-1',
@ -265,9 +283,10 @@ describe('create ESO model version', () => {
changes: [
{
type: 'unsafe_transform',
transformFn: (document) => {
transformFn: (typeSafeGuard) =>
typeSafeGuard((document) => {
return { document };
},
}),
},
],
},
@ -289,7 +308,7 @@ describe('create ESO model version', () => {
) as SavedObjectsModelUnsafeTransformChange[];
expect(unsafeTransforms.length === 1);
expect(() => {
unsafeTransforms[0].transformFn(
unsafeTransforms[0].transformFn(dummyTypeSafeGuard)(
{
id: '123',
type: 'known-type-1',
@ -327,9 +346,10 @@ describe('create ESO model version', () => {
changes: [
{
type: 'unsafe_transform',
transformFn: (document) => {
transformFn: (typeSafeGuard) =>
typeSafeGuard((document) => {
return { document };
},
}),
},
],
},
@ -363,7 +383,7 @@ describe('create ESO model version', () => {
(change) => change.type === 'unsafe_transform'
) as SavedObjectsModelUnsafeTransformChange[];
expect(unsafeTransforms.length === 1);
unsafeTransforms[0].transformFn(
unsafeTransforms[0].transformFn(dummyTypeSafeGuard)(
{
id: '123',
type: 'known-type-1',
@ -406,9 +426,10 @@ describe('create ESO model version', () => {
changes: [
{
type: 'unsafe_transform',
transformFn: (document) => {
transformFn: (typeSafeGuard) =>
typeSafeGuard((document) => {
throw new Error('transform failed!');
},
}),
},
],
},
@ -427,7 +448,7 @@ describe('create ESO model version', () => {
) as SavedObjectsModelUnsafeTransformChange[];
expect(unsafeTransforms.length === 1);
expect(() => {
unsafeTransforms[0].transformFn(
unsafeTransforms[0].transformFn(dummyTypeSafeGuard)(
{
id: '123',
type: 'known-type-1',
@ -464,9 +485,10 @@ describe('create ESO model version', () => {
changes: [
{
type: 'unsafe_transform',
transformFn: (document) => {
transformFn: (typeSafeGuard) =>
typeSafeGuard((document) => {
throw new Error('transform failed!');
},
}),
},
],
},
@ -486,7 +508,7 @@ describe('create ESO model version', () => {
) as SavedObjectsModelUnsafeTransformChange[];
expect(unsafeTransforms.length === 1);
expect(() => {
unsafeTransforms[0].transformFn(
unsafeTransforms[0].transformFn(dummyTypeSafeGuard)(
{
id: '123',
type: 'known-type-1',
@ -523,9 +545,10 @@ describe('create ESO model version', () => {
changes: [
{
type: 'unsafe_transform',
transformFn: (document) => {
transformFn: (typeSafeGuard) =>
typeSafeGuard((document) => {
return { document };
},
}),
},
],
},
@ -547,7 +570,7 @@ describe('create ESO model version', () => {
) as SavedObjectsModelUnsafeTransformChange[];
expect(unsafeTransforms.length === 1);
expect(() => {
unsafeTransforms[0].transformFn(
unsafeTransforms[0].transformFn(dummyTypeSafeGuard)(
{
id: '123',
type: 'known-type-1',
@ -591,9 +614,10 @@ describe('create ESO model version', () => {
changes: [
{
type: 'unsafe_transform',
transformFn: (document) => {
transformFn: (typeSafeGuard) =>
typeSafeGuard((document) => {
return { document };
},
}),
},
],
},
@ -616,7 +640,7 @@ describe('create ESO model version', () => {
) as SavedObjectsModelUnsafeTransformChange[];
expect(unsafeTransforms.length === 1);
expect(() => {
unsafeTransforms[0].transformFn(
unsafeTransforms[0].transformFn(dummyTypeSafeGuard)(
{
id: '123',
type: 'known-type-1',
@ -667,7 +691,15 @@ describe('create ESO model version', () => {
changes: [
{
type: 'unsafe_transform',
transformFn: (document) => {
transformFn: (typeSafeGuard) =>
typeSafeGuard(
(
document: SavedObjectModelTransformationDoc<{
firstAttr: string;
nonEncryptedAttr?: string;
encryptedAttr: string;
}>
) => {
// modify an encrypted field
document.attributes.firstAttr = `~~${document.attributes.firstAttr}~~`;
// encrypt a non encrypted field if it's there
@ -676,7 +708,8 @@ describe('create ESO model version', () => {
delete document.attributes.nonEncryptedAttr;
}
return { document };
},
}
),
},
],
},
@ -703,7 +736,7 @@ describe('create ESO model version', () => {
(change) => change.type === 'unsafe_transform'
) as SavedObjectsModelUnsafeTransformChange[];
expect(unsafeTransforms.length === 1);
const result = unsafeTransforms[0].transformFn(
const result = unsafeTransforms[0].transformFn(dummyTypeSafeGuard)(
{
id: '123',
type: 'known-type-1',

View file

@ -73,7 +73,12 @@ export const getCreateEsoModelVersion =
incomingChanges
);
return { ...modelVersion, changes: [{ type: 'unsafe_transform', transformFn }] };
return {
...modelVersion,
changes: [
{ type: 'unsafe_transform', transformFn: (typeSafeGuard) => typeSafeGuard(transformFn) },
],
};
};
function createMergedTransformFn(

View file

@ -119,7 +119,7 @@ export const entityDefinition: SavedObjectsType = {
changes: [
{
type: 'unsafe_transform',
transformFn: removeOptionalIdentityFields,
transformFn: (typeSafeGuard) => typeSafeGuard(removeOptionalIdentityFields),
},
],
},

View file

@ -13,6 +13,7 @@ import type {
SavedObjectsNamespaceType,
SavedObjectUnsanitizedDoc,
} from '@kbn/core/server';
import type { SavedObjectModelTransformationDoc } from '@kbn/core-saved-objects-server';
import type {
EncryptedSavedObjectsPluginSetup,
EncryptedSavedObjectsPluginStart,
@ -337,13 +338,22 @@ function defineModelVersionWithMigration(core: CoreSetup<PluginsStart>, deps: Pl
changes: [
{
type: 'unsafe_transform',
transformFn: (document) => {
transformFn: (typeSafeGuard) =>
typeSafeGuard(
// ideally, we should use generic types for the whole function, defining it on a separate const
(
document: SavedObjectModelTransformationDoc<{
additionalEncryptedAttribute: string;
nonEncryptedAttribute: string;
}>
) => {
const {
attributes: { nonEncryptedAttribute },
} = document;
document.attributes.nonEncryptedAttribute = `${nonEncryptedAttribute}-migrated`;
return { document };
},
}
),
},
],
},
@ -356,11 +366,20 @@ function defineModelVersionWithMigration(core: CoreSetup<PluginsStart>, deps: Pl
changes: [
{
type: 'unsafe_transform',
transformFn: (document) => {
transformFn: (typeSafeGuard) =>
typeSafeGuard(
// ideally, we should use generic types for the whole function, defining it on a separate const
(
document: SavedObjectModelTransformationDoc<{
additionalEncryptedAttribute: string;
nonEncryptedAttribute: string;
}>
) => {
// clone and modify the non encrypted field
document.attributes.additionalEncryptedAttribute = `${document.attributes.nonEncryptedAttribute}-encrypted`;
return { document };
},
}
),
},
],
},

View file

@ -16,5 +16,6 @@
"@kbn/std",
"@kbn/encrypted-saved-objects-plugin",
"@kbn/spaces-plugin",
"@kbn/core-saved-objects-server",
]
}

View file

@ -5,10 +5,15 @@
* 2.0.
*/
import type { TypeOf } from '@kbn/config-schema';
import { schema } from '@kbn/config-schema';
import { fold } from 'fp-ts/Either';
import { pipe } from 'fp-ts/pipeable';
import type { SavedObject, SavedObjectsType } from '@kbn/core/server';
import type {
SavedObjectModelTransformationDoc,
SavedObjectModelUnsafeTransformFn,
} from '@kbn/core-saved-objects-server';
import { inventoryViewSavedObjectRT } from './types';
export const inventoryViewSavedObjectName = 'inventory-view';
@ -32,6 +37,37 @@ const schemaV2 = schema.object(
{ unknowns: 'allow' }
);
type V1 = TypeOf<typeof schemaV1>;
type V2 = TypeOf<typeof schemaV2>;
const inventoryV2Transform: SavedObjectModelUnsafeTransformFn<V1, V2> = (doc) => {
// steps property did exist, even though it wasn't present in the schema
const asV2 = doc as SavedObjectModelTransformationDoc<V2>;
if (typeof asV2.attributes.legend?.steps === 'undefined') {
return { document: asV2 };
} else {
let steps = asV2.attributes.legend?.steps;
if (steps > 18) {
steps = 18;
} else if (steps < 2) {
steps = 2;
}
const document: SavedObjectModelTransformationDoc<V2> = {
...asV2,
attributes: {
...asV2.attributes,
legend: {
...asV2.attributes.legend,
steps,
},
},
};
return { document };
}
};
export const inventoryViewSavedObjectType: SavedObjectsType = {
name: inventoryViewSavedObjectName,
hidden: false,
@ -58,14 +94,7 @@ export const inventoryViewSavedObjectType: SavedObjectsType = {
changes: [
{
type: 'unsafe_transform',
transformFn: (document) => {
if (document.attributes.legend?.steps > 18) {
document.attributes.legend.steps = 18;
} else if (document.attributes.legend?.steps < 2) {
document.attributes.legend.steps = 2;
}
return { document };
},
transformFn: (typeSafeGuard) => typeSafeGuard(inventoryV2Transform),
},
],
schemas: {

View file

@ -119,7 +119,8 @@
"@kbn/core-chrome-browser",
"@kbn/presentation-containers",
"@kbn/object-utils",
"@kbn/coloring"
"@kbn/coloring",
"@kbn/core-saved-objects-server"
],
"exclude": ["target/**/*"]
}

View file

@ -58,7 +58,7 @@ export const modelVersion1: SavedObjectsModelVersion = {
changes: [
{
type: 'unsafe_transform',
transformFn: transformGeoProperty,
transformFn: (typeSafeGuard) => typeSafeGuard(transformGeoProperty),
},
],
};