mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[CM] Soften response validation (#166919)
## Summary Close https://github.com/elastic/kibana/issues/167152 Log a warning instead of throwing an error in `saved_object_content_storage` when response validation failed. We decided to do this as a precaution and as a follow up to an issue found in saved search https://github.com/elastic/kibana/pull/166886 where storage started failing because of too strict validation. As of this PR the saved_object_content_storage covers and this change cover: - `search` - `index_pattern` - `dashboard` - `lens` - `maps` For other types we agreed with @dej611 that instead of applying the same change for other types (visualization, graph, annotation) the team would look into migrating their types to also use `saved_object_content_storage` https://github.com/elastic/kibana/issues/167421
This commit is contained in:
parent
88b8b8c190
commit
6fd9909b5e
21 changed files with 811 additions and 43 deletions
|
@ -0,0 +1,600 @@
|
|||
/*
|
||||
* 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 { SOContentStorage } from './saved_object_content_storage';
|
||||
import { CMCrudTypes } from './types';
|
||||
import { loggerMock, MockedLogger } from '@kbn/logging-mocks';
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import type {
|
||||
ContentManagementServicesDefinition as ServicesDefinition,
|
||||
Version,
|
||||
} from '@kbn/object-versioning';
|
||||
import { getContentManagmentServicesTransforms } from '@kbn/object-versioning';
|
||||
import { savedObjectSchema, objectTypeToGetResultSchema, createResultSchema } from './schema';
|
||||
|
||||
import { coreMock } from '@kbn/core/server/mocks';
|
||||
import type { SavedObject } from '@kbn/core/server';
|
||||
|
||||
const testAttributesSchema = schema.object(
|
||||
{
|
||||
title: schema.string(),
|
||||
description: schema.string(),
|
||||
},
|
||||
{ unknowns: 'forbid' }
|
||||
);
|
||||
|
||||
const testSavedObjectSchema = savedObjectSchema(testAttributesSchema);
|
||||
|
||||
export const serviceDefinition: ServicesDefinition = {
|
||||
get: {
|
||||
out: {
|
||||
result: {
|
||||
schema: objectTypeToGetResultSchema(testSavedObjectSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
create: {
|
||||
out: {
|
||||
result: {
|
||||
schema: createResultSchema(testSavedObjectSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
update: {
|
||||
out: {
|
||||
result: {
|
||||
schema: createResultSchema(testSavedObjectSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
search: {
|
||||
out: {
|
||||
result: {
|
||||
schema: schema.object({ hits: schema.arrayOf(testSavedObjectSchema) }),
|
||||
},
|
||||
},
|
||||
},
|
||||
mSearch: {
|
||||
out: {
|
||||
result: {
|
||||
schema: testSavedObjectSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const cmServicesDefinition: { [version: Version]: ServicesDefinition } = {
|
||||
1: serviceDefinition,
|
||||
};
|
||||
|
||||
const transforms = getContentManagmentServicesTransforms(cmServicesDefinition, 1);
|
||||
|
||||
class TestSOContentStorage extends SOContentStorage<CMCrudTypes> {
|
||||
constructor({
|
||||
throwOnResultValidationError,
|
||||
logger,
|
||||
}: { throwOnResultValidationError?: boolean; logger?: MockedLogger } = {}) {
|
||||
super({
|
||||
savedObjectType: 'test',
|
||||
cmServicesDefinition,
|
||||
allowedSavedObjectAttributes: ['title', 'description'],
|
||||
logger: logger ?? loggerMock.create(),
|
||||
throwOnResultValidationError: throwOnResultValidationError ?? false,
|
||||
enableMSearch: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const setup = ({ storage }: { storage?: TestSOContentStorage } = {}) => {
|
||||
storage = storage ?? new TestSOContentStorage();
|
||||
const requestHandlerCoreContext = coreMock.createRequestHandlerContext();
|
||||
|
||||
const requestHandlerContext = {
|
||||
core: Promise.resolve(requestHandlerCoreContext),
|
||||
resolve: jest.fn(),
|
||||
};
|
||||
|
||||
return {
|
||||
get: (mockSavedObject: SavedObject) => {
|
||||
requestHandlerCoreContext.savedObjects.client.resolve.mockResolvedValue({
|
||||
saved_object: mockSavedObject,
|
||||
outcome: 'exactMatch',
|
||||
});
|
||||
|
||||
return storage!.get(
|
||||
{
|
||||
requestHandlerContext,
|
||||
version: {
|
||||
request: 1,
|
||||
latest: 1,
|
||||
},
|
||||
utils: {
|
||||
getTransforms: () => transforms,
|
||||
},
|
||||
},
|
||||
mockSavedObject.id
|
||||
);
|
||||
},
|
||||
create: (mockSavedObject: SavedObject<{}>) => {
|
||||
requestHandlerCoreContext.savedObjects.client.create.mockResolvedValue(mockSavedObject);
|
||||
|
||||
return storage!.create(
|
||||
{
|
||||
requestHandlerContext,
|
||||
version: {
|
||||
request: 1,
|
||||
latest: 1,
|
||||
},
|
||||
utils: {
|
||||
getTransforms: () => transforms,
|
||||
},
|
||||
},
|
||||
mockSavedObject.attributes,
|
||||
{}
|
||||
);
|
||||
},
|
||||
update: (mockSavedObject: SavedObject<{}>) => {
|
||||
requestHandlerCoreContext.savedObjects.client.update.mockResolvedValue(mockSavedObject);
|
||||
|
||||
return storage!.update(
|
||||
{
|
||||
requestHandlerContext,
|
||||
version: {
|
||||
request: 1,
|
||||
latest: 1,
|
||||
},
|
||||
utils: {
|
||||
getTransforms: () => transforms,
|
||||
},
|
||||
},
|
||||
mockSavedObject.id,
|
||||
mockSavedObject.attributes,
|
||||
{}
|
||||
);
|
||||
},
|
||||
search: (mockSavedObject: SavedObject<{}>) => {
|
||||
requestHandlerCoreContext.savedObjects.client.find.mockResolvedValue({
|
||||
saved_objects: [{ ...mockSavedObject, score: 100 }],
|
||||
total: 1,
|
||||
per_page: 10,
|
||||
page: 1,
|
||||
});
|
||||
|
||||
return storage!.search(
|
||||
{
|
||||
requestHandlerContext,
|
||||
version: {
|
||||
request: 1,
|
||||
latest: 1,
|
||||
},
|
||||
utils: {
|
||||
getTransforms: () => transforms,
|
||||
},
|
||||
},
|
||||
{},
|
||||
{}
|
||||
);
|
||||
},
|
||||
mSearch: async (mockSavedObject: SavedObject<{}>) => {
|
||||
return storage!.mSearch!.toItemResult(
|
||||
{
|
||||
requestHandlerContext,
|
||||
version: {
|
||||
request: 1,
|
||||
latest: 1,
|
||||
},
|
||||
utils: {
|
||||
getTransforms: () => transforms,
|
||||
},
|
||||
},
|
||||
{ ...mockSavedObject, score: 100 }
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
describe('get', () => {
|
||||
test('returns the storage get() result', async () => {
|
||||
const get = setup().get;
|
||||
|
||||
const testSavedObject = {
|
||||
id: 'id',
|
||||
type: 'test',
|
||||
references: [],
|
||||
attributes: {
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
},
|
||||
};
|
||||
|
||||
const result = await get(testSavedObject);
|
||||
|
||||
expect(result).toEqual({ item: testSavedObject, meta: { outcome: 'exactMatch' } });
|
||||
});
|
||||
|
||||
test('filters out unknown attributes', async () => {
|
||||
const get = setup().get;
|
||||
|
||||
const testSavedObject = {
|
||||
id: 'id',
|
||||
type: 'test',
|
||||
references: [],
|
||||
attributes: {
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
unknown: 'unknown',
|
||||
},
|
||||
};
|
||||
|
||||
const result = await get(testSavedObject);
|
||||
expect(result.item.attributes).not.toHaveProperty('unknown');
|
||||
});
|
||||
|
||||
test('throws response validation error', async () => {
|
||||
const get = setup({
|
||||
storage: new TestSOContentStorage({ throwOnResultValidationError: true }),
|
||||
}).get;
|
||||
|
||||
const testSavedObject = {
|
||||
id: 'id',
|
||||
type: 'test',
|
||||
references: [],
|
||||
attributes: {
|
||||
title: 'title',
|
||||
description: null,
|
||||
},
|
||||
};
|
||||
|
||||
await expect(get(testSavedObject)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid response. [item.attributes.description]: expected value of type [string] but got [null]"`
|
||||
);
|
||||
});
|
||||
|
||||
test('logs response validation error', async () => {
|
||||
const logger = loggerMock.create();
|
||||
const get = setup({
|
||||
storage: new TestSOContentStorage({ throwOnResultValidationError: false, logger }),
|
||||
}).get;
|
||||
|
||||
const testSavedObject = {
|
||||
id: 'id',
|
||||
type: 'test',
|
||||
references: [],
|
||||
attributes: {
|
||||
title: 'title',
|
||||
description: null,
|
||||
},
|
||||
};
|
||||
|
||||
await expect(get(testSavedObject)).resolves.toBeDefined();
|
||||
expect(logger.warn).toBeCalledWith(
|
||||
`Invalid response. [item.attributes.description]: expected value of type [string] but got [null]`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
test('returns the storage create() result', async () => {
|
||||
const create = setup().create;
|
||||
|
||||
const testSavedObject = {
|
||||
id: 'id',
|
||||
type: 'test',
|
||||
references: [],
|
||||
attributes: {
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
},
|
||||
};
|
||||
|
||||
const result = await create(testSavedObject);
|
||||
|
||||
expect(result).toEqual({ item: testSavedObject });
|
||||
});
|
||||
|
||||
test('filters out unknown attributes', async () => {
|
||||
const create = setup().create;
|
||||
|
||||
const testSavedObject = {
|
||||
id: 'id',
|
||||
type: 'test',
|
||||
references: [],
|
||||
attributes: {
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
unknown: 'unknown',
|
||||
},
|
||||
};
|
||||
|
||||
const result = await create(testSavedObject);
|
||||
expect(result.item.attributes).not.toHaveProperty('unknown');
|
||||
});
|
||||
|
||||
test('throws response validation error', async () => {
|
||||
const create = setup({
|
||||
storage: new TestSOContentStorage({ throwOnResultValidationError: true }),
|
||||
}).create;
|
||||
|
||||
const testSavedObject = {
|
||||
id: 'id',
|
||||
type: 'test',
|
||||
references: [],
|
||||
attributes: {
|
||||
title: 'title',
|
||||
description: null,
|
||||
},
|
||||
};
|
||||
|
||||
await expect(create(testSavedObject)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid response. [item.attributes.description]: expected value of type [string] but got [null]"`
|
||||
);
|
||||
});
|
||||
|
||||
test('logs response validation error', async () => {
|
||||
const logger = loggerMock.create();
|
||||
const create = setup({
|
||||
storage: new TestSOContentStorage({ throwOnResultValidationError: false, logger }),
|
||||
}).create;
|
||||
|
||||
const testSavedObject = {
|
||||
id: 'id',
|
||||
type: 'test',
|
||||
references: [],
|
||||
attributes: {
|
||||
title: 'title',
|
||||
description: null,
|
||||
},
|
||||
};
|
||||
|
||||
await expect(create(testSavedObject)).resolves.toBeDefined();
|
||||
expect(logger.warn).toBeCalledWith(
|
||||
`Invalid response. [item.attributes.description]: expected value of type [string] but got [null]`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
test('returns the storage update() result', async () => {
|
||||
const update = setup().update;
|
||||
|
||||
const testSavedObject = {
|
||||
id: 'id',
|
||||
type: 'test',
|
||||
references: [],
|
||||
attributes: {
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
},
|
||||
};
|
||||
|
||||
const result = await update(testSavedObject);
|
||||
|
||||
expect(result).toEqual({ item: testSavedObject });
|
||||
});
|
||||
|
||||
test('filters out unknown attributes', async () => {
|
||||
const update = setup().update;
|
||||
|
||||
const testSavedObject = {
|
||||
id: 'id',
|
||||
type: 'test',
|
||||
references: [],
|
||||
attributes: {
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
unknown: 'unknown',
|
||||
},
|
||||
};
|
||||
|
||||
const result = await update(testSavedObject);
|
||||
expect(result.item.attributes).not.toHaveProperty('unknown');
|
||||
});
|
||||
|
||||
test('throws response validation error', async () => {
|
||||
const update = setup({
|
||||
storage: new TestSOContentStorage({ throwOnResultValidationError: true }),
|
||||
}).update;
|
||||
|
||||
const testSavedObject = {
|
||||
id: 'id',
|
||||
type: 'test',
|
||||
references: [],
|
||||
attributes: {
|
||||
title: 'title',
|
||||
description: null,
|
||||
},
|
||||
};
|
||||
|
||||
await expect(update(testSavedObject)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid response. [item.attributes.description]: expected value of type [string] but got [null]"`
|
||||
);
|
||||
});
|
||||
|
||||
test('logs response validation error', async () => {
|
||||
const logger = loggerMock.create();
|
||||
const update = setup({
|
||||
storage: new TestSOContentStorage({ throwOnResultValidationError: false, logger }),
|
||||
}).update;
|
||||
|
||||
const testSavedObject = {
|
||||
id: 'id',
|
||||
type: 'test',
|
||||
references: [],
|
||||
attributes: {
|
||||
title: 'title',
|
||||
description: null,
|
||||
},
|
||||
};
|
||||
|
||||
await expect(update(testSavedObject)).resolves.toBeDefined();
|
||||
expect(logger.warn).toBeCalledWith(
|
||||
`Invalid response. [item.attributes.description]: expected value of type [string] but got [null]`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('search', () => {
|
||||
test('returns the storage search() result', async () => {
|
||||
const search = setup().search;
|
||||
|
||||
const testSavedObject = {
|
||||
id: 'id',
|
||||
type: 'test',
|
||||
references: [],
|
||||
attributes: {
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
},
|
||||
};
|
||||
|
||||
const result = await search(testSavedObject);
|
||||
|
||||
expect(result).toEqual({ hits: [testSavedObject], pagination: { total: 1 } });
|
||||
});
|
||||
|
||||
test('filters out unknown attributes', async () => {
|
||||
const search = setup().search;
|
||||
|
||||
const testSavedObject = {
|
||||
id: 'id',
|
||||
type: 'test',
|
||||
references: [],
|
||||
attributes: {
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
unknown: 'unknown',
|
||||
},
|
||||
};
|
||||
|
||||
const result = await search(testSavedObject);
|
||||
expect(result.hits[0].attributes).not.toHaveProperty('unknown');
|
||||
});
|
||||
|
||||
test('throws response validation error', async () => {
|
||||
const search = setup({
|
||||
storage: new TestSOContentStorage({ throwOnResultValidationError: true }),
|
||||
}).search;
|
||||
|
||||
const testSavedObject = {
|
||||
id: 'id',
|
||||
type: 'test',
|
||||
references: [],
|
||||
attributes: {
|
||||
title: 'title',
|
||||
description: null,
|
||||
},
|
||||
};
|
||||
|
||||
await expect(search(testSavedObject)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid response. [hits.0.attributes.description]: expected value of type [string] but got [null]"`
|
||||
);
|
||||
});
|
||||
|
||||
test('logs response validation error', async () => {
|
||||
const logger = loggerMock.create();
|
||||
const update = setup({
|
||||
storage: new TestSOContentStorage({ throwOnResultValidationError: false, logger }),
|
||||
}).search;
|
||||
|
||||
const testSavedObject = {
|
||||
id: 'id',
|
||||
type: 'test',
|
||||
references: [],
|
||||
attributes: {
|
||||
title: 'title',
|
||||
description: null,
|
||||
},
|
||||
};
|
||||
|
||||
await expect(update(testSavedObject)).resolves.toBeDefined();
|
||||
expect(logger.warn).toBeCalledWith(
|
||||
`Invalid response. [hits.0.attributes.description]: expected value of type [string] but got [null]`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mSearch', () => {
|
||||
test('returns the storage mSearch() result', async () => {
|
||||
const mSearch = setup().mSearch;
|
||||
|
||||
const testSavedObject = {
|
||||
id: 'id',
|
||||
type: 'test',
|
||||
references: [],
|
||||
attributes: {
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
},
|
||||
};
|
||||
|
||||
const result = await mSearch(testSavedObject);
|
||||
|
||||
expect(result).toEqual(testSavedObject);
|
||||
});
|
||||
|
||||
test('filters out unknown attributes', async () => {
|
||||
const mSearch = setup().mSearch;
|
||||
|
||||
const testSavedObject = {
|
||||
id: 'id',
|
||||
type: 'test',
|
||||
references: [],
|
||||
attributes: {
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
unknown: 'unknown',
|
||||
},
|
||||
};
|
||||
|
||||
const result = await mSearch(testSavedObject);
|
||||
expect(result.attributes).not.toHaveProperty('unknown');
|
||||
});
|
||||
|
||||
test('throws response validation error', async () => {
|
||||
const mSearch = setup({
|
||||
storage: new TestSOContentStorage({ throwOnResultValidationError: true }),
|
||||
}).mSearch;
|
||||
|
||||
const testSavedObject = {
|
||||
id: 'id',
|
||||
type: 'test',
|
||||
references: [],
|
||||
attributes: {
|
||||
title: 'title',
|
||||
description: null,
|
||||
},
|
||||
};
|
||||
|
||||
await expect(mSearch(testSavedObject)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid response. [attributes.description]: expected value of type [string] but got [null]"`
|
||||
);
|
||||
});
|
||||
|
||||
test('logs response validation error', async () => {
|
||||
const logger = loggerMock.create();
|
||||
const mSearch = setup({
|
||||
storage: new TestSOContentStorage({ throwOnResultValidationError: false, logger }),
|
||||
}).mSearch;
|
||||
|
||||
const testSavedObject = {
|
||||
id: 'id',
|
||||
type: 'test',
|
||||
references: [],
|
||||
attributes: {
|
||||
title: 'title',
|
||||
description: null,
|
||||
},
|
||||
};
|
||||
|
||||
await expect(mSearch(testSavedObject)).resolves.toBeDefined();
|
||||
expect(logger.warn).toBeCalledWith(
|
||||
'Invalid response. [attributes.description]: expected value of type [string] but got [null]'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -21,6 +21,7 @@ import type {
|
|||
SavedObjectsUpdateOptions,
|
||||
SavedObjectsFindResult,
|
||||
} from '@kbn/core-saved-objects-api-server';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
import { pick } from 'lodash';
|
||||
import type {
|
||||
CMCrudTypes,
|
||||
|
@ -138,6 +139,9 @@ export interface SOContentStorageConstrutorParams<Types extends CMCrudTypes> {
|
|||
searchArgsToSOFindOptions?: SearchArgsToSOFindOptions<Types>;
|
||||
enableMSearch?: boolean;
|
||||
mSearchAdditionalSearchFields?: string[];
|
||||
|
||||
logger: Logger;
|
||||
throwOnResultValidationError: boolean;
|
||||
}
|
||||
|
||||
export abstract class SOContentStorage<Types extends CMCrudTypes>
|
||||
|
@ -157,7 +161,11 @@ export abstract class SOContentStorage<Types extends CMCrudTypes>
|
|||
enableMSearch,
|
||||
allowedSavedObjectAttributes,
|
||||
mSearchAdditionalSearchFields,
|
||||
logger,
|
||||
throwOnResultValidationError,
|
||||
}: SOContentStorageConstrutorParams<Types>) {
|
||||
this.logger = logger;
|
||||
this.throwOnResultValidationError = throwOnResultValidationError ?? false;
|
||||
this.savedObjectType = savedObjectType;
|
||||
this.cmServicesDefinition = cmServicesDefinition;
|
||||
this.createArgsToSoCreateOptions =
|
||||
|
@ -174,16 +182,29 @@ export abstract class SOContentStorage<Types extends CMCrudTypes>
|
|||
toItemResult: (ctx: StorageContext, savedObject: SavedObjectsFindResult): Types['Item'] => {
|
||||
const transforms = ctx.utils.getTransforms(this.cmServicesDefinition);
|
||||
|
||||
const contentItem = savedObjectToItem(
|
||||
savedObject as SavedObjectsFindResult<Types['Attributes']>,
|
||||
this.allowedSavedObjectAttributes,
|
||||
false
|
||||
);
|
||||
|
||||
const validationError = transforms.mSearch.out.result.validate(contentItem);
|
||||
if (validationError) {
|
||||
if (this.throwOnResultValidationError) {
|
||||
throw Boom.badRequest(`Invalid response. ${validationError.message}`);
|
||||
} else {
|
||||
this.logger.warn(`Invalid response. ${validationError.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate DB response and DOWN transform to the request version
|
||||
const { value, error: resultError } = transforms.mSearch.out.result.down<
|
||||
Types['Item'],
|
||||
Types['Item']
|
||||
>(
|
||||
savedObjectToItem(
|
||||
savedObject as SavedObjectsFindResult<Types['Attributes']>,
|
||||
this.allowedSavedObjectAttributes,
|
||||
false
|
||||
)
|
||||
contentItem,
|
||||
undefined, // do not override version
|
||||
{ validate: false } // validation is done above
|
||||
);
|
||||
|
||||
if (resultError) {
|
||||
|
@ -196,6 +217,8 @@ export abstract class SOContentStorage<Types extends CMCrudTypes>
|
|||
}
|
||||
}
|
||||
|
||||
private throwOnResultValidationError: boolean;
|
||||
private logger: Logger;
|
||||
private savedObjectType: SOContentStorageConstrutorParams<Types>['savedObjectType'];
|
||||
private cmServicesDefinition: SOContentStorageConstrutorParams<Types>['cmServicesDefinition'];
|
||||
private createArgsToSoCreateOptions: CreateArgsToSoCreateOptions<Types>;
|
||||
|
@ -230,11 +253,24 @@ export abstract class SOContentStorage<Types extends CMCrudTypes>
|
|||
},
|
||||
};
|
||||
|
||||
const validationError = transforms.get.out.result.validate(response);
|
||||
if (validationError) {
|
||||
if (this.throwOnResultValidationError) {
|
||||
throw Boom.badRequest(`Invalid response. ${validationError.message}`);
|
||||
} else {
|
||||
this.logger.warn(`Invalid response. ${validationError.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate DB response and DOWN transform to the request version
|
||||
const { value, error: resultError } = transforms.get.out.result.down<
|
||||
Types['GetOut'],
|
||||
Types['GetOut']
|
||||
>(response);
|
||||
>(
|
||||
response,
|
||||
undefined, // do not override version
|
||||
{ validate: false } // validation is done above
|
||||
);
|
||||
|
||||
if (resultError) {
|
||||
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
|
||||
|
@ -282,13 +318,28 @@ export abstract class SOContentStorage<Types extends CMCrudTypes>
|
|||
createOptions
|
||||
);
|
||||
|
||||
const result = {
|
||||
item: savedObjectToItem(savedObject, this.allowedSavedObjectAttributes, false),
|
||||
};
|
||||
|
||||
const validationError = transforms.create.out.result.validate(result);
|
||||
if (validationError) {
|
||||
if (this.throwOnResultValidationError) {
|
||||
throw Boom.badRequest(`Invalid response. ${validationError.message}`);
|
||||
} else {
|
||||
this.logger.warn(`Invalid response. ${validationError.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate DB response and DOWN transform to the request version
|
||||
const { value, error: resultError } = transforms.create.out.result.down<
|
||||
Types['CreateOut'],
|
||||
Types['CreateOut']
|
||||
>({
|
||||
item: savedObjectToItem(savedObject, this.allowedSavedObjectAttributes, false),
|
||||
});
|
||||
>(
|
||||
result,
|
||||
undefined, // do not override version
|
||||
{ validate: false } // validation is done above
|
||||
);
|
||||
|
||||
if (resultError) {
|
||||
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
|
||||
|
@ -333,13 +384,28 @@ export abstract class SOContentStorage<Types extends CMCrudTypes>
|
|||
updateOptions
|
||||
);
|
||||
|
||||
const result = {
|
||||
item: savedObjectToItem(partialSavedObject, this.allowedSavedObjectAttributes, true),
|
||||
};
|
||||
|
||||
const validationError = transforms.update.out.result.validate(result);
|
||||
if (validationError) {
|
||||
if (this.throwOnResultValidationError) {
|
||||
throw Boom.badRequest(`Invalid response. ${validationError.message}`);
|
||||
} else {
|
||||
this.logger.warn(`Invalid response. ${validationError.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate DB response and DOWN transform to the request version
|
||||
const { value, error: resultError } = transforms.update.out.result.down<
|
||||
Types['UpdateOut'],
|
||||
Types['UpdateOut']
|
||||
>({
|
||||
item: savedObjectToItem(partialSavedObject, this.allowedSavedObjectAttributes, true),
|
||||
});
|
||||
>(
|
||||
result,
|
||||
undefined, // do not override version
|
||||
{ validate: false } // validation is done above
|
||||
);
|
||||
|
||||
if (resultError) {
|
||||
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
|
||||
|
@ -382,20 +448,34 @@ export abstract class SOContentStorage<Types extends CMCrudTypes>
|
|||
options: optionsToLatest,
|
||||
});
|
||||
// Execute the query in the DB
|
||||
const response = await soClient.find<Types['Attributes']>(soQuery);
|
||||
const soResponse = await soClient.find<Types['Attributes']>(soQuery);
|
||||
const response = {
|
||||
hits: soResponse.saved_objects.map((so) =>
|
||||
savedObjectToItem(so, this.allowedSavedObjectAttributes, false)
|
||||
),
|
||||
pagination: {
|
||||
total: soResponse.total,
|
||||
},
|
||||
};
|
||||
|
||||
const validationError = transforms.search.out.result.validate(response);
|
||||
if (validationError) {
|
||||
if (this.throwOnResultValidationError) {
|
||||
throw Boom.badRequest(`Invalid response. ${validationError.message}`);
|
||||
} else {
|
||||
this.logger.warn(`Invalid response. ${validationError.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the response and DOWN transform to the request version
|
||||
const { value, error: resultError } = transforms.search.out.result.down<
|
||||
Types['SearchOut'],
|
||||
Types['SearchOut']
|
||||
>({
|
||||
hits: response.saved_objects.map((so) =>
|
||||
savedObjectToItem(so, this.allowedSavedObjectAttributes, false)
|
||||
),
|
||||
pagination: {
|
||||
total: response.total,
|
||||
},
|
||||
});
|
||||
>(
|
||||
response,
|
||||
undefined, // do not override version
|
||||
{ validate: false } // validation is done above
|
||||
);
|
||||
|
||||
if (resultError) {
|
||||
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
|
||||
|
|
|
@ -21,5 +21,8 @@
|
|||
"@kbn/core-saved-objects-api-server",
|
||||
"@kbn/config-schema",
|
||||
"@kbn/object-versioning",
|
||||
"@kbn/logging",
|
||||
"@kbn/logging-mocks",
|
||||
"@kbn/core",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import { SOContentStorage, tagsToFindOptions } from '@kbn/content-management-utils';
|
||||
import { SavedObjectsFindOptions } from '@kbn/core-saved-objects-api-server';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
|
||||
import { CONTENT_ID } from '../../common/content_management';
|
||||
import { cmServicesDefinition } from '../../common/content_management/cm_services';
|
||||
|
@ -31,7 +32,13 @@ const searchArgsToSOFindOptions = (
|
|||
};
|
||||
|
||||
export class DashboardStorage extends SOContentStorage<DashboardCrudTypes> {
|
||||
constructor() {
|
||||
constructor({
|
||||
logger,
|
||||
throwOnResultValidationError,
|
||||
}: {
|
||||
logger: Logger;
|
||||
throwOnResultValidationError: boolean;
|
||||
}) {
|
||||
super({
|
||||
savedObjectType: CONTENT_ID,
|
||||
cmServicesDefinition,
|
||||
|
@ -50,6 +57,8 @@ export class DashboardStorage extends SOContentStorage<DashboardCrudTypes> {
|
|||
'timeTo',
|
||||
'title',
|
||||
],
|
||||
logger,
|
||||
throwOnResultValidationError,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ export class DashboardPlugin
|
|||
{
|
||||
private readonly logger: Logger;
|
||||
|
||||
constructor(initializerContext: PluginInitializerContext) {
|
||||
constructor(private initializerContext: PluginInitializerContext) {
|
||||
this.logger = initializerContext.logger.get();
|
||||
}
|
||||
|
||||
|
@ -62,7 +62,10 @@ export class DashboardPlugin
|
|||
|
||||
plugins.contentManagement.register({
|
||||
id: CONTENT_ID,
|
||||
storage: new DashboardStorage(),
|
||||
storage: new DashboardStorage({
|
||||
throwOnResultValidationError: this.initializerContext.env.mode.dev,
|
||||
logger: this.logger.get('storage'),
|
||||
}),
|
||||
version: {
|
||||
latest: LATEST_VERSION,
|
||||
},
|
||||
|
|
|
@ -67,7 +67,8 @@
|
|||
"@kbn/serverless",
|
||||
"@kbn/no-data-page-plugin",
|
||||
"@kbn/react-kibana-mount",
|
||||
"@kbn/core-lifecycle-browser"
|
||||
"@kbn/core-lifecycle-browser",
|
||||
"@kbn/logging"
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
|
@ -7,13 +7,20 @@
|
|||
*/
|
||||
|
||||
import { SOContentStorage } from '@kbn/content-management-utils';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
|
||||
import type { DataViewCrudTypes } from '../../common/content_management';
|
||||
import { DataViewSOType } from '../../common/content_management';
|
||||
import { cmServicesDefinition } from '../../common/content_management/cm_services';
|
||||
|
||||
export class DataViewsStorage extends SOContentStorage<DataViewCrudTypes> {
|
||||
constructor() {
|
||||
constructor({
|
||||
logger,
|
||||
throwOnResultValidationError,
|
||||
}: {
|
||||
logger: Logger;
|
||||
throwOnResultValidationError: boolean;
|
||||
}) {
|
||||
super({
|
||||
savedObjectType: DataViewSOType,
|
||||
cmServicesDefinition,
|
||||
|
@ -32,6 +39,8 @@ export class DataViewsStorage extends SOContentStorage<DataViewCrudTypes> {
|
|||
'name',
|
||||
],
|
||||
mSearchAdditionalSearchFields: ['name'],
|
||||
logger,
|
||||
throwOnResultValidationError,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,7 +61,10 @@ export class DataViewsServerPlugin
|
|||
|
||||
contentManagement.register({
|
||||
id: DATA_VIEW_SAVED_OBJECT_TYPE,
|
||||
storage: new DataViewsStorage(),
|
||||
storage: new DataViewsStorage({
|
||||
throwOnResultValidationError: this.initializerContext.env.mode.dev,
|
||||
logger: this.logger.get('storage'),
|
||||
}),
|
||||
version: {
|
||||
latest: LATEST_VERSION,
|
||||
},
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
"@kbn/content-management-utils",
|
||||
"@kbn/object-versioning",
|
||||
"@kbn/core-saved-objects-server",
|
||||
"@kbn/logging",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -7,13 +7,20 @@
|
|||
*/
|
||||
|
||||
import { SOContentStorage } from '@kbn/content-management-utils';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
|
||||
import type { SavedSearchCrudTypes } from '../../common/content_management';
|
||||
import { SavedSearchType } from '../../common/content_management';
|
||||
import { cmServicesDefinition } from '../../common/content_management/cm_services';
|
||||
|
||||
export class SavedSearchStorage extends SOContentStorage<SavedSearchCrudTypes> {
|
||||
constructor() {
|
||||
constructor({
|
||||
logger,
|
||||
throwOnResultValidationError,
|
||||
}: {
|
||||
logger: Logger;
|
||||
throwOnResultValidationError: boolean;
|
||||
}) {
|
||||
super({
|
||||
savedObjectType: SavedSearchType,
|
||||
cmServicesDefinition,
|
||||
|
@ -37,6 +44,8 @@ export class SavedSearchStorage extends SOContentStorage<SavedSearchCrudTypes> {
|
|||
'rowsPerPage',
|
||||
'breakdownField',
|
||||
],
|
||||
logger,
|
||||
throwOnResultValidationError,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,8 +6,10 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { PluginInitializerContext } from '@kbn/core-plugins-server';
|
||||
import { SavedSearchServerPlugin } from './plugin';
|
||||
|
||||
export { getSavedSearch } from './services/saved_searches';
|
||||
|
||||
export const plugin = () => new SavedSearchServerPlugin();
|
||||
export const plugin = (initContext: PluginInitializerContext) =>
|
||||
new SavedSearchServerPlugin(initContext);
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { CoreSetup, CoreStart, Plugin } from '@kbn/core/server';
|
||||
import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/server';
|
||||
import { StartServicesAccessor } from '@kbn/core/server';
|
||||
import type {
|
||||
PluginSetup as DataPluginSetup,
|
||||
|
@ -37,13 +37,18 @@ export interface SavedSearchServerStartDeps {
|
|||
export class SavedSearchServerPlugin
|
||||
implements Plugin<object, object, object, SavedSearchServerStartDeps>
|
||||
{
|
||||
constructor(private initializerContext: PluginInitializerContext) {}
|
||||
|
||||
public setup(
|
||||
core: CoreSetup,
|
||||
{ data, contentManagement, expressions }: SavedSearchPublicSetupDependencies
|
||||
) {
|
||||
contentManagement.register({
|
||||
id: SavedSearchType,
|
||||
storage: new SavedSearchStorage(),
|
||||
storage: new SavedSearchStorage({
|
||||
throwOnResultValidationError: this.initializerContext.env.mode.dev,
|
||||
logger: this.initializerContext.logger.get('storage'),
|
||||
}),
|
||||
version: {
|
||||
latest: LATEST_VERSION,
|
||||
},
|
||||
|
|
|
@ -29,6 +29,8 @@
|
|||
"@kbn/saved-objects-plugin",
|
||||
"@kbn/es-query",
|
||||
"@kbn/discover-utils",
|
||||
"@kbn/logging",
|
||||
"@kbn/core-plugins-server",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -9,6 +9,7 @@ import type { SavedObjectsFindOptions } from '@kbn/core-saved-objects-api-server
|
|||
import type { StorageContext } from '@kbn/content-management-plugin/server';
|
||||
import { SOContentStorage, tagsToFindOptions } from '@kbn/content-management-utils';
|
||||
import type { SavedObject, SavedObjectReference } from '@kbn/core-saved-objects-api-server';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
|
||||
import {
|
||||
CONTENT_ID,
|
||||
|
@ -82,13 +83,20 @@ function savedObjectToLensSavedObject(
|
|||
}
|
||||
|
||||
export class LensStorage extends SOContentStorage<LensCrudTypes> {
|
||||
constructor() {
|
||||
constructor(
|
||||
private params: {
|
||||
logger: Logger;
|
||||
throwOnResultValidationError: boolean;
|
||||
}
|
||||
) {
|
||||
super({
|
||||
savedObjectType: CONTENT_ID,
|
||||
cmServicesDefinition,
|
||||
searchArgsToSOFindOptions,
|
||||
enableMSearch: true,
|
||||
allowedSavedObjectAttributes: ['title', 'description', 'visualizationType', 'state'],
|
||||
logger: params.logger,
|
||||
throwOnResultValidationError: params.throwOnResultValidationError,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -134,13 +142,28 @@ export class LensStorage extends SOContentStorage<LensCrudTypes> {
|
|||
...optionsToLatest,
|
||||
});
|
||||
|
||||
const result = {
|
||||
item: savedObjectToLensSavedObject(savedObject),
|
||||
};
|
||||
|
||||
const validationError = transforms.update.out.result.validate(result);
|
||||
if (validationError) {
|
||||
if (this.params.throwOnResultValidationError) {
|
||||
throw Boom.badRequest(`Invalid response. ${validationError.message}`);
|
||||
} else {
|
||||
this.params.logger.warn(`Invalid response. ${validationError.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate DB response and DOWN transform to the request version
|
||||
const { value, error: resultError } = transforms.update.out.result.down<
|
||||
LensCrudTypes['UpdateOut'],
|
||||
LensCrudTypes['UpdateOut']
|
||||
>({
|
||||
item: savedObjectToLensSavedObject(savedObject),
|
||||
});
|
||||
>(
|
||||
result,
|
||||
undefined, // do not override version
|
||||
{ validate: false } // validation is done above
|
||||
);
|
||||
|
||||
if (resultError) {
|
||||
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { PluginInitializerContext } from '@kbn/core-plugins-server';
|
||||
import { LensServerPlugin } from './plugin';
|
||||
|
||||
export type { LensServerPluginSetup } from './plugin';
|
||||
|
||||
export const plugin = () => new LensServerPlugin();
|
||||
export const plugin = (initContext: PluginInitializerContext) => new LensServerPlugin(initContext);
|
||||
|
||||
export type { LensDocShape715 } from './migrations/types';
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Plugin, CoreSetup, CoreStart } from '@kbn/core/server';
|
||||
import { Plugin, CoreSetup, CoreStart, PluginInitializerContext } from '@kbn/core/server';
|
||||
import { PluginStart as DataViewsServerPluginStart } from '@kbn/data-views-plugin/server';
|
||||
import {
|
||||
PluginStart as DataPluginStart,
|
||||
|
@ -64,7 +64,7 @@ export interface LensServerPluginSetup {
|
|||
export class LensServerPlugin implements Plugin<LensServerPluginSetup, {}, {}, {}> {
|
||||
private customVisualizationMigrations: CustomVisualizationMigrations = {};
|
||||
|
||||
constructor() {}
|
||||
constructor(private initializerContext: PluginInitializerContext) {}
|
||||
|
||||
setup(core: CoreSetup<PluginStartContract>, plugins: PluginSetupContract) {
|
||||
const getFilterMigrations = plugins.data.query.filterManager.getAllMigrations.bind(
|
||||
|
@ -79,7 +79,10 @@ export class LensServerPlugin implements Plugin<LensServerPluginSetup, {}, {}, {
|
|||
|
||||
plugins.contentManagement.register({
|
||||
id: CONTENT_ID,
|
||||
storage: new LensStorage(),
|
||||
storage: new LensStorage({
|
||||
throwOnResultValidationError: this.initializerContext.env.mode.dev,
|
||||
logger: this.initializerContext.logger.get('storage'),
|
||||
}),
|
||||
version: {
|
||||
latest: LATEST_VERSION,
|
||||
},
|
||||
|
|
|
@ -88,6 +88,8 @@
|
|||
"@kbn/ebt-tools",
|
||||
"@kbn/chart-expressions-common",
|
||||
"@kbn/search-response-warnings",
|
||||
"@kbn/logging",
|
||||
"@kbn/core-plugins-server",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { SOContentStorage, tagsToFindOptions } from '@kbn/content-management-utils';
|
||||
import { SavedObjectsFindOptions } from '@kbn/core-saved-objects-api-server';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
import { CONTENT_ID } from '../../common/content_management';
|
||||
import { cmServicesDefinition } from '../../common/content_management/cm_services';
|
||||
import type { MapCrudTypes } from '../../common/content_management';
|
||||
|
@ -27,7 +28,13 @@ const searchArgsToSOFindOptions = (args: MapCrudTypes['SearchIn']): SavedObjects
|
|||
};
|
||||
|
||||
export class MapsStorage extends SOContentStorage<MapCrudTypes> {
|
||||
constructor() {
|
||||
constructor({
|
||||
logger,
|
||||
throwOnResultValidationError,
|
||||
}: {
|
||||
logger: Logger;
|
||||
throwOnResultValidationError: boolean;
|
||||
}) {
|
||||
super({
|
||||
savedObjectType: CONTENT_ID,
|
||||
cmServicesDefinition,
|
||||
|
@ -40,6 +47,8 @@ export class MapsStorage extends SOContentStorage<MapCrudTypes> {
|
|||
'layerListJSON',
|
||||
'uiStateJSON',
|
||||
],
|
||||
logger,
|
||||
throwOnResultValidationError,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -204,7 +204,10 @@ export class MapsPlugin implements Plugin {
|
|||
|
||||
contentManagement.register({
|
||||
id: CONTENT_ID,
|
||||
storage: new MapsStorage(),
|
||||
storage: new MapsStorage({
|
||||
throwOnResultValidationError: this._initializerContext.env.mode.dev,
|
||||
logger: this._logger.get('storage'),
|
||||
}),
|
||||
version: {
|
||||
latest: LATEST_VERSION,
|
||||
},
|
||||
|
|
|
@ -73,6 +73,7 @@
|
|||
"@kbn/content-management-table-list-view-table",
|
||||
"@kbn/content-management-table-list-view",
|
||||
"@kbn/serverless",
|
||||
"@kbn/logging",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -142,7 +142,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
const errorMessages = await Promise.all(failureElements.map((el) => el.getVisibleText()));
|
||||
|
||||
expect(errorMessages).to.eql([
|
||||
'Bad Request',
|
||||
'Visualization type not found.',
|
||||
'The visualization type lnsUNKNOWN could not be resolved.',
|
||||
'Could not find datasource for the visualization',
|
||||
]);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue