SO model versions: add validation for mapping additions (#158714)

## Summary

Add validation to ensure that mappings added via a `mappings_addition`
type change are also present in the type's full mappings.

(just an extra layer of protection for when the teams will switch to
using the new API)
This commit is contained in:
Pierre Gayvallet 2023-06-01 01:50:49 -04:00 committed by GitHub
parent 0f7eca4d68
commit 109e67b464
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 164 additions and 1 deletions

View file

@ -236,6 +236,103 @@ describe('validateTypeMigrations', () => {
});
});
describe('modelVersions mapping additions', () => {
it('throws when registering mapping additions not present in the global mappings', () => {
const type = createType({
name: 'foo',
switchToModelVersionAt: '8.8.0',
modelVersions: {
'1': {
changes: [
{
type: 'mappings_addition',
addedMappings: {
field2: { type: 'text' },
},
},
],
},
},
mappings: {
properties: {
field1: { type: 'text' },
},
},
});
expect(() => validate({ type, kibanaVersion: '3.2.3' })).toThrowErrorMatchingInlineSnapshot(
`"Type foo: mappings added on model versions not present on the global mappings definition: field2.type"`
);
});
it('does not throw when registering mapping additions are present in the global mappings', () => {
const type = createType({
name: 'foo',
switchToModelVersionAt: '8.8.0',
modelVersions: {
'1': {
changes: [
{
type: 'mappings_addition',
addedMappings: {
field2: { type: 'text' },
},
},
],
},
'2': {
changes: [
{
type: 'mappings_addition',
addedMappings: {
field3: { type: 'text' },
},
},
],
},
},
mappings: {
properties: {
field1: { type: 'text' },
field2: { type: 'text' },
field3: { type: 'text' },
},
},
});
expect(() => validate({ type, kibanaVersion: '3.2.3' })).not.toThrow();
});
it('throws when registering mapping additions different than the global mappings', () => {
const type = createType({
name: 'foo',
switchToModelVersionAt: '8.8.0',
modelVersions: {
'1': {
changes: [
{
type: 'mappings_addition',
addedMappings: {
field2: { type: 'boolean' },
},
},
],
},
},
mappings: {
properties: {
field1: { type: 'text' },
field2: { type: 'text' },
},
},
});
expect(() => validate({ type, kibanaVersion: '3.2.3' })).toThrowErrorMatchingInlineSnapshot(
`"Type foo: mappings added on model versions differs from the global mappings definition: field2.type"`
);
});
});
describe('convertToMultiNamespaceTypeVersion', () => {
it(`validates convertToMultiNamespaceTypeVersion can only be used with namespaceType 'multiple' or 'multiple-isolated'`, () => {
const type = createType({

View file

@ -7,9 +7,18 @@
*/
import Semver from 'semver';
import { getFlattenedObject } from '@kbn/std';
import type { SavedObjectsNamespaceType } from '@kbn/core-saved-objects-common';
import type { SavedObjectsType } from '@kbn/core-saved-objects-server';
import type {
SavedObjectsType,
SavedObjectsTypeMappingDefinition,
SavedObjectsModelVersionMap,
} from '@kbn/core-saved-objects-server';
import { assertValidModelVersion } from '@kbn/core-saved-objects-base-server-internal';
import {
SavedObjectsModelChange,
SavedObjectsModelMappingsAdditionChange,
} from '@kbn/core-saved-objects-server';
/**
* Validates the consistency of the given type for use with the document migrator.
@ -87,6 +96,9 @@ export function validateTypeMigrations({
if (minVersion > 1) {
throw new Error(`Type ${type.name}: model versioning must start with version 1`);
}
validateAddedMappings(type.name, type.mappings, modelVersionMap);
const missingVersions = getMissingVersions(
minVersion,
maxVersion,
@ -114,6 +126,60 @@ export function validateTypeMigrations({
}
}
function isMappingAddition(
change: SavedObjectsModelChange
): change is SavedObjectsModelMappingsAdditionChange {
return change.type === 'mappings_addition';
}
const validateAddedMappings = (
typeName: string,
mappings: SavedObjectsTypeMappingDefinition,
modelVersions: SavedObjectsModelVersionMap
) => {
const flattenedMappings = new Map(Object.entries(getFlattenedObject(mappings.properties)));
const mappingAdditionChanges = Object.values(modelVersions)
.flatMap((version) => version.changes)
.filter<SavedObjectsModelMappingsAdditionChange>(isMappingAddition);
const addedMappings = mappingAdditionChanges.reduce((map, change) => {
const flattened = getFlattenedObject(change.addedMappings);
Object.keys(flattened).forEach((key) => {
map.set(key, flattened[key]);
});
return map;
}, new Map<string, unknown>());
const missingMappings: string[] = [];
const mappingsWithDifferentValues: string[] = [];
for (const [key, value] of addedMappings.entries()) {
if (!flattenedMappings.has(key)) {
missingMappings.push(key);
} else {
const valueInMappings = flattenedMappings.get(key);
if (valueInMappings !== value) {
mappingsWithDifferentValues.push(key);
}
}
}
if (missingMappings.length) {
throw new Error(
`Type ${typeName}: mappings added on model versions not present on the global mappings definition: ${missingMappings.join(
','
)}`
);
}
if (mappingsWithDifferentValues.length) {
throw new Error(
`Type ${typeName}: mappings added on model versions differs from the global mappings definition: ${mappingsWithDifferentValues.join(
','
)}`
);
}
};
const assertObjectOrFunction = (entity: any, prefix: string) => {
if (!entity || (typeof entity !== 'function' && typeof entity !== 'object')) {
throw new Error(`${prefix} Got! ${typeof entity}, ${JSON.stringify(entity)}.`);