Skip to content

Commit fd068da

Browse files
[SOR] Adds support for validation schema with models (#158527)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
1 parent 6ea3f39 commit fd068da

File tree

6 files changed

+301
-43
lines changed

6 files changed

+301
-43
lines changed
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
import { loggerMock, type MockedLogger } from '@kbn/logging-mocks';
10+
import { SavedObjectsType } from '@kbn/core-saved-objects-server';
11+
import { type SavedObjectSanitizedDoc } from '@kbn/core-saved-objects-server';
12+
import { ValidationHelper } from './validation';
13+
import { typedef, typedef1, typedef2 } from './validation_fixtures';
14+
import { SavedObjectTypeRegistry } from '@kbn/core-saved-objects-base-server-internal';
15+
16+
const defaultVersion = '8.10.0';
17+
const modelVirtualVersion = '10.1.0';
18+
const typeA = 'my-typeA';
19+
const typeB = 'my-typeB';
20+
const typeC = 'my-typeC';
21+
22+
describe('Saved Objects type validation helper', () => {
23+
let helper: ValidationHelper;
24+
let logger: MockedLogger;
25+
let typeRegistry: SavedObjectTypeRegistry;
26+
27+
const createMockObject = (
28+
type: string,
29+
attr: Partial<SavedObjectSanitizedDoc>
30+
): SavedObjectSanitizedDoc => ({
31+
type,
32+
id: 'test-id',
33+
references: [],
34+
attributes: {},
35+
...attr,
36+
});
37+
const registerType = (name: string, parts: Partial<SavedObjectsType>) => {
38+
typeRegistry.registerType({
39+
name,
40+
hidden: false,
41+
namespaceType: 'single',
42+
mappings: { properties: {} },
43+
...parts,
44+
});
45+
};
46+
beforeEach(() => {
47+
logger = loggerMock.create();
48+
typeRegistry = new SavedObjectTypeRegistry();
49+
});
50+
51+
afterEach(() => {
52+
jest.resetAllMocks();
53+
});
54+
55+
describe('validation helper', () => {
56+
beforeEach(() => {
57+
registerType(typeA, typedef);
58+
registerType(typeB, typedef1);
59+
registerType(typeC, typedef2);
60+
});
61+
62+
it('should validate objects against stack versions', () => {
63+
helper = new ValidationHelper({
64+
logger,
65+
registry: typeRegistry,
66+
kibanaVersion: defaultVersion,
67+
});
68+
const data = createMockObject(typeA, { attributes: { foo: 'hi', count: 1 } });
69+
expect(() => helper.validateObjectForCreate(typeA, data)).not.toThrowError();
70+
});
71+
72+
it('should validate objects against model versions', () => {
73+
helper = new ValidationHelper({
74+
logger,
75+
registry: typeRegistry,
76+
kibanaVersion: modelVirtualVersion,
77+
});
78+
const data = createMockObject(typeB, { attributes: { foo: 'hi', count: 1 } });
79+
expect(() => helper.validateObjectForCreate(typeB, data)).not.toThrowError();
80+
});
81+
82+
it('should fail validation against invalid objects when version requested does not support a field', () => {
83+
helper = new ValidationHelper({
84+
logger,
85+
registry: typeRegistry,
86+
kibanaVersion: defaultVersion,
87+
});
88+
const validationError = new Error(
89+
'[attributes.count]: definition for this key is missing: Bad Request'
90+
);
91+
const data = createMockObject(typeC, { attributes: { foo: 'hi', count: 1 } });
92+
expect(() => helper.validateObjectForCreate(typeC, data)).toThrowError(validationError);
93+
});
94+
});
95+
});

packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/validation.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
import type { PublicMethodsOf } from '@kbn/utility-types';
1010
import type { Logger } from '@kbn/logging';
1111
import type { ISavedObjectTypeRegistry } from '@kbn/core-saved-objects-server';
12-
import { SavedObjectsTypeValidator } from '@kbn/core-saved-objects-base-server-internal';
12+
import {
13+
SavedObjectsTypeValidator,
14+
modelVersionToVirtualVersion,
15+
} from '@kbn/core-saved-objects-base-server-internal';
1316
import {
1417
SavedObjectsErrorHelpers,
1518
type SavedObjectSanitizedDoc,
@@ -91,7 +94,7 @@ export class ValidationHelper {
9194
}
9295
const validator = this.getTypeValidator(type);
9396
try {
94-
validator.validate(doc, this.kibanaVersion);
97+
validator.validate(doc);
9598
} catch (error) {
9699
throw SavedObjectsErrorHelpers.createBadRequestError(error.message);
97100
}
@@ -100,10 +103,30 @@ export class ValidationHelper {
100103
private getTypeValidator(type: string): SavedObjectsTypeValidator {
101104
if (!this.typeValidatorMap[type]) {
102105
const savedObjectType = this.registry.getType(type);
106+
107+
const stackVersionSchemas =
108+
typeof savedObjectType?.schemas === 'function'
109+
? savedObjectType.schemas()
110+
: savedObjectType?.schemas ?? {};
111+
112+
const modelVersionCreateSchemas =
113+
typeof savedObjectType?.modelVersions === 'function'
114+
? savedObjectType.modelVersions()
115+
: savedObjectType?.modelVersions ?? {};
116+
117+
const combinedSchemas = { ...stackVersionSchemas };
118+
Object.entries(modelVersionCreateSchemas).reduce((map, [key, modelVersion]) => {
119+
if (modelVersion.schemas?.create) {
120+
const virtualVersion = modelVersionToVirtualVersion(key);
121+
combinedSchemas[virtualVersion] = modelVersion.schemas!.create!;
122+
}
123+
return map;
124+
}, {});
125+
103126
this.typeValidatorMap[type] = new SavedObjectsTypeValidator({
104127
logger: this.logger.get('type-validator'),
105128
type,
106-
validationMap: savedObjectType!.schemas ?? {},
129+
validationMap: combinedSchemas,
107130
defaultVersion: this.kibanaVersion,
108131
});
109132
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
import { schema } from '@kbn/config-schema';
10+
import { SavedObjectsType } from '@kbn/core-saved-objects-server';
11+
12+
export const typedef: Partial<SavedObjectsType> = {
13+
mappings: {
14+
properties: {
15+
foo: {
16+
type: 'keyword',
17+
},
18+
count: {
19+
type: 'integer',
20+
},
21+
},
22+
},
23+
schemas: {
24+
'8.9.0': schema.object({
25+
foo: schema.string(),
26+
}),
27+
'8.10.0': schema.object({
28+
foo: schema.string(),
29+
count: schema.number(),
30+
}),
31+
},
32+
switchToModelVersionAt: '8.10.0',
33+
};
34+
35+
export const typedef1: Partial<SavedObjectsType> = {
36+
mappings: {
37+
properties: {
38+
foo: {
39+
type: 'keyword',
40+
},
41+
count: {
42+
type: 'integer',
43+
},
44+
},
45+
},
46+
schemas: {
47+
'8.9.0': schema.object({
48+
foo: schema.string(),
49+
}),
50+
'8.10.0': schema.object({
51+
foo: schema.string(),
52+
count: schema.number(),
53+
}),
54+
},
55+
switchToModelVersionAt: '8.10.0',
56+
modelVersions: {
57+
'1': {
58+
changes: [
59+
{
60+
type: 'mappings_addition',
61+
addedMappings: {
62+
count: {
63+
properties: {
64+
count: {
65+
type: 'integer',
66+
},
67+
},
68+
},
69+
},
70+
},
71+
],
72+
schemas: {
73+
create: schema.object({
74+
foo: schema.string(),
75+
count: schema.number(),
76+
}),
77+
},
78+
},
79+
},
80+
};
81+
82+
export const typedef2: Partial<SavedObjectsType> = {
83+
mappings: {
84+
properties: {
85+
foo: {
86+
type: 'keyword',
87+
},
88+
count: {
89+
type: 'integer',
90+
},
91+
},
92+
},
93+
schemas: {
94+
'8.9.0': schema.object({
95+
foo: schema.string(),
96+
}),
97+
},
98+
switchToModelVersionAt: '8.10.0',
99+
modelVersions: {
100+
'1': {
101+
changes: [
102+
{
103+
type: 'mappings_addition',
104+
addedMappings: {
105+
count: {
106+
properties: {
107+
count: {
108+
type: 'integer',
109+
},
110+
},
111+
},
112+
},
113+
},
114+
],
115+
schemas: {
116+
create: schema.object({
117+
foo: schema.string(),
118+
count: schema.number(),
119+
}),
120+
},
121+
},
122+
},
123+
};

0 commit comments

Comments
 (0)