mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -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,
|
SavedObjectsUpdateOptions,
|
||||||
SavedObjectsFindResult,
|
SavedObjectsFindResult,
|
||||||
} from '@kbn/core-saved-objects-api-server';
|
} from '@kbn/core-saved-objects-api-server';
|
||||||
|
import type { Logger } from '@kbn/logging';
|
||||||
import { pick } from 'lodash';
|
import { pick } from 'lodash';
|
||||||
import type {
|
import type {
|
||||||
CMCrudTypes,
|
CMCrudTypes,
|
||||||
|
@ -138,6 +139,9 @@ export interface SOContentStorageConstrutorParams<Types extends CMCrudTypes> {
|
||||||
searchArgsToSOFindOptions?: SearchArgsToSOFindOptions<Types>;
|
searchArgsToSOFindOptions?: SearchArgsToSOFindOptions<Types>;
|
||||||
enableMSearch?: boolean;
|
enableMSearch?: boolean;
|
||||||
mSearchAdditionalSearchFields?: string[];
|
mSearchAdditionalSearchFields?: string[];
|
||||||
|
|
||||||
|
logger: Logger;
|
||||||
|
throwOnResultValidationError: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class SOContentStorage<Types extends CMCrudTypes>
|
export abstract class SOContentStorage<Types extends CMCrudTypes>
|
||||||
|
@ -157,7 +161,11 @@ export abstract class SOContentStorage<Types extends CMCrudTypes>
|
||||||
enableMSearch,
|
enableMSearch,
|
||||||
allowedSavedObjectAttributes,
|
allowedSavedObjectAttributes,
|
||||||
mSearchAdditionalSearchFields,
|
mSearchAdditionalSearchFields,
|
||||||
|
logger,
|
||||||
|
throwOnResultValidationError,
|
||||||
}: SOContentStorageConstrutorParams<Types>) {
|
}: SOContentStorageConstrutorParams<Types>) {
|
||||||
|
this.logger = logger;
|
||||||
|
this.throwOnResultValidationError = throwOnResultValidationError ?? false;
|
||||||
this.savedObjectType = savedObjectType;
|
this.savedObjectType = savedObjectType;
|
||||||
this.cmServicesDefinition = cmServicesDefinition;
|
this.cmServicesDefinition = cmServicesDefinition;
|
||||||
this.createArgsToSoCreateOptions =
|
this.createArgsToSoCreateOptions =
|
||||||
|
@ -174,16 +182,29 @@ export abstract class SOContentStorage<Types extends CMCrudTypes>
|
||||||
toItemResult: (ctx: StorageContext, savedObject: SavedObjectsFindResult): Types['Item'] => {
|
toItemResult: (ctx: StorageContext, savedObject: SavedObjectsFindResult): Types['Item'] => {
|
||||||
const transforms = ctx.utils.getTransforms(this.cmServicesDefinition);
|
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
|
// Validate DB response and DOWN transform to the request version
|
||||||
const { value, error: resultError } = transforms.mSearch.out.result.down<
|
const { value, error: resultError } = transforms.mSearch.out.result.down<
|
||||||
Types['Item'],
|
Types['Item'],
|
||||||
Types['Item']
|
Types['Item']
|
||||||
>(
|
>(
|
||||||
savedObjectToItem(
|
contentItem,
|
||||||
savedObject as SavedObjectsFindResult<Types['Attributes']>,
|
undefined, // do not override version
|
||||||
this.allowedSavedObjectAttributes,
|
{ validate: false } // validation is done above
|
||||||
false
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (resultError) {
|
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 savedObjectType: SOContentStorageConstrutorParams<Types>['savedObjectType'];
|
||||||
private cmServicesDefinition: SOContentStorageConstrutorParams<Types>['cmServicesDefinition'];
|
private cmServicesDefinition: SOContentStorageConstrutorParams<Types>['cmServicesDefinition'];
|
||||||
private createArgsToSoCreateOptions: CreateArgsToSoCreateOptions<Types>;
|
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
|
// Validate DB response and DOWN transform to the request version
|
||||||
const { value, error: resultError } = transforms.get.out.result.down<
|
const { value, error: resultError } = transforms.get.out.result.down<
|
||||||
Types['GetOut'],
|
Types['GetOut'],
|
||||||
Types['GetOut']
|
Types['GetOut']
|
||||||
>(response);
|
>(
|
||||||
|
response,
|
||||||
|
undefined, // do not override version
|
||||||
|
{ validate: false } // validation is done above
|
||||||
|
);
|
||||||
|
|
||||||
if (resultError) {
|
if (resultError) {
|
||||||
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
|
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
|
||||||
|
@ -282,13 +318,28 @@ export abstract class SOContentStorage<Types extends CMCrudTypes>
|
||||||
createOptions
|
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
|
// Validate DB response and DOWN transform to the request version
|
||||||
const { value, error: resultError } = transforms.create.out.result.down<
|
const { value, error: resultError } = transforms.create.out.result.down<
|
||||||
Types['CreateOut'],
|
Types['CreateOut'],
|
||||||
Types['CreateOut']
|
Types['CreateOut']
|
||||||
>({
|
>(
|
||||||
item: savedObjectToItem(savedObject, this.allowedSavedObjectAttributes, false),
|
result,
|
||||||
});
|
undefined, // do not override version
|
||||||
|
{ validate: false } // validation is done above
|
||||||
|
);
|
||||||
|
|
||||||
if (resultError) {
|
if (resultError) {
|
||||||
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
|
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
|
||||||
|
@ -333,13 +384,28 @@ export abstract class SOContentStorage<Types extends CMCrudTypes>
|
||||||
updateOptions
|
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
|
// Validate DB response and DOWN transform to the request version
|
||||||
const { value, error: resultError } = transforms.update.out.result.down<
|
const { value, error: resultError } = transforms.update.out.result.down<
|
||||||
Types['UpdateOut'],
|
Types['UpdateOut'],
|
||||||
Types['UpdateOut']
|
Types['UpdateOut']
|
||||||
>({
|
>(
|
||||||
item: savedObjectToItem(partialSavedObject, this.allowedSavedObjectAttributes, true),
|
result,
|
||||||
});
|
undefined, // do not override version
|
||||||
|
{ validate: false } // validation is done above
|
||||||
|
);
|
||||||
|
|
||||||
if (resultError) {
|
if (resultError) {
|
||||||
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
|
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
|
||||||
|
@ -382,20 +448,34 @@ export abstract class SOContentStorage<Types extends CMCrudTypes>
|
||||||
options: optionsToLatest,
|
options: optionsToLatest,
|
||||||
});
|
});
|
||||||
// Execute the query in the DB
|
// 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
|
// Validate the response and DOWN transform to the request version
|
||||||
const { value, error: resultError } = transforms.search.out.result.down<
|
const { value, error: resultError } = transforms.search.out.result.down<
|
||||||
Types['SearchOut'],
|
Types['SearchOut'],
|
||||||
Types['SearchOut']
|
Types['SearchOut']
|
||||||
>({
|
>(
|
||||||
hits: response.saved_objects.map((so) =>
|
response,
|
||||||
savedObjectToItem(so, this.allowedSavedObjectAttributes, false)
|
undefined, // do not override version
|
||||||
),
|
{ validate: false } // validation is done above
|
||||||
pagination: {
|
);
|
||||||
total: response.total,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (resultError) {
|
if (resultError) {
|
||||||
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
|
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
|
||||||
|
|
|
@ -21,5 +21,8 @@
|
||||||
"@kbn/core-saved-objects-api-server",
|
"@kbn/core-saved-objects-api-server",
|
||||||
"@kbn/config-schema",
|
"@kbn/config-schema",
|
||||||
"@kbn/object-versioning",
|
"@kbn/object-versioning",
|
||||||
|
"@kbn/logging",
|
||||||
|
"@kbn/logging-mocks",
|
||||||
|
"@kbn/core",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
import { SOContentStorage, tagsToFindOptions } from '@kbn/content-management-utils';
|
import { SOContentStorage, tagsToFindOptions } from '@kbn/content-management-utils';
|
||||||
import { SavedObjectsFindOptions } from '@kbn/core-saved-objects-api-server';
|
import { SavedObjectsFindOptions } from '@kbn/core-saved-objects-api-server';
|
||||||
|
import type { Logger } from '@kbn/logging';
|
||||||
|
|
||||||
import { CONTENT_ID } from '../../common/content_management';
|
import { CONTENT_ID } from '../../common/content_management';
|
||||||
import { cmServicesDefinition } from '../../common/content_management/cm_services';
|
import { cmServicesDefinition } from '../../common/content_management/cm_services';
|
||||||
|
@ -31,7 +32,13 @@ const searchArgsToSOFindOptions = (
|
||||||
};
|
};
|
||||||
|
|
||||||
export class DashboardStorage extends SOContentStorage<DashboardCrudTypes> {
|
export class DashboardStorage extends SOContentStorage<DashboardCrudTypes> {
|
||||||
constructor() {
|
constructor({
|
||||||
|
logger,
|
||||||
|
throwOnResultValidationError,
|
||||||
|
}: {
|
||||||
|
logger: Logger;
|
||||||
|
throwOnResultValidationError: boolean;
|
||||||
|
}) {
|
||||||
super({
|
super({
|
||||||
savedObjectType: CONTENT_ID,
|
savedObjectType: CONTENT_ID,
|
||||||
cmServicesDefinition,
|
cmServicesDefinition,
|
||||||
|
@ -50,6 +57,8 @@ export class DashboardStorage extends SOContentStorage<DashboardCrudTypes> {
|
||||||
'timeTo',
|
'timeTo',
|
||||||
'title',
|
'title',
|
||||||
],
|
],
|
||||||
|
logger,
|
||||||
|
throwOnResultValidationError,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@ export class DashboardPlugin
|
||||||
{
|
{
|
||||||
private readonly logger: Logger;
|
private readonly logger: Logger;
|
||||||
|
|
||||||
constructor(initializerContext: PluginInitializerContext) {
|
constructor(private initializerContext: PluginInitializerContext) {
|
||||||
this.logger = initializerContext.logger.get();
|
this.logger = initializerContext.logger.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,7 +62,10 @@ export class DashboardPlugin
|
||||||
|
|
||||||
plugins.contentManagement.register({
|
plugins.contentManagement.register({
|
||||||
id: CONTENT_ID,
|
id: CONTENT_ID,
|
||||||
storage: new DashboardStorage(),
|
storage: new DashboardStorage({
|
||||||
|
throwOnResultValidationError: this.initializerContext.env.mode.dev,
|
||||||
|
logger: this.logger.get('storage'),
|
||||||
|
}),
|
||||||
version: {
|
version: {
|
||||||
latest: LATEST_VERSION,
|
latest: LATEST_VERSION,
|
||||||
},
|
},
|
||||||
|
|
|
@ -67,7 +67,8 @@
|
||||||
"@kbn/serverless",
|
"@kbn/serverless",
|
||||||
"@kbn/no-data-page-plugin",
|
"@kbn/no-data-page-plugin",
|
||||||
"@kbn/react-kibana-mount",
|
"@kbn/react-kibana-mount",
|
||||||
"@kbn/core-lifecycle-browser"
|
"@kbn/core-lifecycle-browser",
|
||||||
|
"@kbn/logging"
|
||||||
],
|
],
|
||||||
"exclude": ["target/**/*"]
|
"exclude": ["target/**/*"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,13 +7,20 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { SOContentStorage } from '@kbn/content-management-utils';
|
import { SOContentStorage } from '@kbn/content-management-utils';
|
||||||
|
import type { Logger } from '@kbn/logging';
|
||||||
|
|
||||||
import type { DataViewCrudTypes } from '../../common/content_management';
|
import type { DataViewCrudTypes } from '../../common/content_management';
|
||||||
import { DataViewSOType } from '../../common/content_management';
|
import { DataViewSOType } from '../../common/content_management';
|
||||||
import { cmServicesDefinition } from '../../common/content_management/cm_services';
|
import { cmServicesDefinition } from '../../common/content_management/cm_services';
|
||||||
|
|
||||||
export class DataViewsStorage extends SOContentStorage<DataViewCrudTypes> {
|
export class DataViewsStorage extends SOContentStorage<DataViewCrudTypes> {
|
||||||
constructor() {
|
constructor({
|
||||||
|
logger,
|
||||||
|
throwOnResultValidationError,
|
||||||
|
}: {
|
||||||
|
logger: Logger;
|
||||||
|
throwOnResultValidationError: boolean;
|
||||||
|
}) {
|
||||||
super({
|
super({
|
||||||
savedObjectType: DataViewSOType,
|
savedObjectType: DataViewSOType,
|
||||||
cmServicesDefinition,
|
cmServicesDefinition,
|
||||||
|
@ -32,6 +39,8 @@ export class DataViewsStorage extends SOContentStorage<DataViewCrudTypes> {
|
||||||
'name',
|
'name',
|
||||||
],
|
],
|
||||||
mSearchAdditionalSearchFields: ['name'],
|
mSearchAdditionalSearchFields: ['name'],
|
||||||
|
logger,
|
||||||
|
throwOnResultValidationError,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,7 +61,10 @@ export class DataViewsServerPlugin
|
||||||
|
|
||||||
contentManagement.register({
|
contentManagement.register({
|
||||||
id: DATA_VIEW_SAVED_OBJECT_TYPE,
|
id: DATA_VIEW_SAVED_OBJECT_TYPE,
|
||||||
storage: new DataViewsStorage(),
|
storage: new DataViewsStorage({
|
||||||
|
throwOnResultValidationError: this.initializerContext.env.mode.dev,
|
||||||
|
logger: this.logger.get('storage'),
|
||||||
|
}),
|
||||||
version: {
|
version: {
|
||||||
latest: LATEST_VERSION,
|
latest: LATEST_VERSION,
|
||||||
},
|
},
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
"@kbn/content-management-utils",
|
"@kbn/content-management-utils",
|
||||||
"@kbn/object-versioning",
|
"@kbn/object-versioning",
|
||||||
"@kbn/core-saved-objects-server",
|
"@kbn/core-saved-objects-server",
|
||||||
|
"@kbn/logging",
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"target/**/*",
|
"target/**/*",
|
||||||
|
|
|
@ -7,13 +7,20 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { SOContentStorage } from '@kbn/content-management-utils';
|
import { SOContentStorage } from '@kbn/content-management-utils';
|
||||||
|
import type { Logger } from '@kbn/logging';
|
||||||
|
|
||||||
import type { SavedSearchCrudTypes } from '../../common/content_management';
|
import type { SavedSearchCrudTypes } from '../../common/content_management';
|
||||||
import { SavedSearchType } from '../../common/content_management';
|
import { SavedSearchType } from '../../common/content_management';
|
||||||
import { cmServicesDefinition } from '../../common/content_management/cm_services';
|
import { cmServicesDefinition } from '../../common/content_management/cm_services';
|
||||||
|
|
||||||
export class SavedSearchStorage extends SOContentStorage<SavedSearchCrudTypes> {
|
export class SavedSearchStorage extends SOContentStorage<SavedSearchCrudTypes> {
|
||||||
constructor() {
|
constructor({
|
||||||
|
logger,
|
||||||
|
throwOnResultValidationError,
|
||||||
|
}: {
|
||||||
|
logger: Logger;
|
||||||
|
throwOnResultValidationError: boolean;
|
||||||
|
}) {
|
||||||
super({
|
super({
|
||||||
savedObjectType: SavedSearchType,
|
savedObjectType: SavedSearchType,
|
||||||
cmServicesDefinition,
|
cmServicesDefinition,
|
||||||
|
@ -37,6 +44,8 @@ export class SavedSearchStorage extends SOContentStorage<SavedSearchCrudTypes> {
|
||||||
'rowsPerPage',
|
'rowsPerPage',
|
||||||
'breakdownField',
|
'breakdownField',
|
||||||
],
|
],
|
||||||
|
logger,
|
||||||
|
throwOnResultValidationError,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,10 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { PluginInitializerContext } from '@kbn/core-plugins-server';
|
||||||
import { SavedSearchServerPlugin } from './plugin';
|
import { SavedSearchServerPlugin } from './plugin';
|
||||||
|
|
||||||
export { getSavedSearch } from './services/saved_searches';
|
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.
|
* 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 { StartServicesAccessor } from '@kbn/core/server';
|
||||||
import type {
|
import type {
|
||||||
PluginSetup as DataPluginSetup,
|
PluginSetup as DataPluginSetup,
|
||||||
|
@ -37,13 +37,18 @@ export interface SavedSearchServerStartDeps {
|
||||||
export class SavedSearchServerPlugin
|
export class SavedSearchServerPlugin
|
||||||
implements Plugin<object, object, object, SavedSearchServerStartDeps>
|
implements Plugin<object, object, object, SavedSearchServerStartDeps>
|
||||||
{
|
{
|
||||||
|
constructor(private initializerContext: PluginInitializerContext) {}
|
||||||
|
|
||||||
public setup(
|
public setup(
|
||||||
core: CoreSetup,
|
core: CoreSetup,
|
||||||
{ data, contentManagement, expressions }: SavedSearchPublicSetupDependencies
|
{ data, contentManagement, expressions }: SavedSearchPublicSetupDependencies
|
||||||
) {
|
) {
|
||||||
contentManagement.register({
|
contentManagement.register({
|
||||||
id: SavedSearchType,
|
id: SavedSearchType,
|
||||||
storage: new SavedSearchStorage(),
|
storage: new SavedSearchStorage({
|
||||||
|
throwOnResultValidationError: this.initializerContext.env.mode.dev,
|
||||||
|
logger: this.initializerContext.logger.get('storage'),
|
||||||
|
}),
|
||||||
version: {
|
version: {
|
||||||
latest: LATEST_VERSION,
|
latest: LATEST_VERSION,
|
||||||
},
|
},
|
||||||
|
|
|
@ -29,6 +29,8 @@
|
||||||
"@kbn/saved-objects-plugin",
|
"@kbn/saved-objects-plugin",
|
||||||
"@kbn/es-query",
|
"@kbn/es-query",
|
||||||
"@kbn/discover-utils",
|
"@kbn/discover-utils",
|
||||||
|
"@kbn/logging",
|
||||||
|
"@kbn/core-plugins-server",
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"target/**/*",
|
"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 type { StorageContext } from '@kbn/content-management-plugin/server';
|
||||||
import { SOContentStorage, tagsToFindOptions } from '@kbn/content-management-utils';
|
import { SOContentStorage, tagsToFindOptions } from '@kbn/content-management-utils';
|
||||||
import type { SavedObject, SavedObjectReference } from '@kbn/core-saved-objects-api-server';
|
import type { SavedObject, SavedObjectReference } from '@kbn/core-saved-objects-api-server';
|
||||||
|
import type { Logger } from '@kbn/logging';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CONTENT_ID,
|
CONTENT_ID,
|
||||||
|
@ -82,13 +83,20 @@ function savedObjectToLensSavedObject(
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LensStorage extends SOContentStorage<LensCrudTypes> {
|
export class LensStorage extends SOContentStorage<LensCrudTypes> {
|
||||||
constructor() {
|
constructor(
|
||||||
|
private params: {
|
||||||
|
logger: Logger;
|
||||||
|
throwOnResultValidationError: boolean;
|
||||||
|
}
|
||||||
|
) {
|
||||||
super({
|
super({
|
||||||
savedObjectType: CONTENT_ID,
|
savedObjectType: CONTENT_ID,
|
||||||
cmServicesDefinition,
|
cmServicesDefinition,
|
||||||
searchArgsToSOFindOptions,
|
searchArgsToSOFindOptions,
|
||||||
enableMSearch: true,
|
enableMSearch: true,
|
||||||
allowedSavedObjectAttributes: ['title', 'description', 'visualizationType', 'state'],
|
allowedSavedObjectAttributes: ['title', 'description', 'visualizationType', 'state'],
|
||||||
|
logger: params.logger,
|
||||||
|
throwOnResultValidationError: params.throwOnResultValidationError,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,13 +142,28 @@ export class LensStorage extends SOContentStorage<LensCrudTypes> {
|
||||||
...optionsToLatest,
|
...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
|
// Validate DB response and DOWN transform to the request version
|
||||||
const { value, error: resultError } = transforms.update.out.result.down<
|
const { value, error: resultError } = transforms.update.out.result.down<
|
||||||
LensCrudTypes['UpdateOut'],
|
LensCrudTypes['UpdateOut'],
|
||||||
LensCrudTypes['UpdateOut']
|
LensCrudTypes['UpdateOut']
|
||||||
>({
|
>(
|
||||||
item: savedObjectToLensSavedObject(savedObject),
|
result,
|
||||||
});
|
undefined, // do not override version
|
||||||
|
{ validate: false } // validation is done above
|
||||||
|
);
|
||||||
|
|
||||||
if (resultError) {
|
if (resultError) {
|
||||||
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
|
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
|
||||||
|
|
|
@ -5,10 +5,10 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { PluginInitializerContext } from '@kbn/core-plugins-server';
|
||||||
import { LensServerPlugin } from './plugin';
|
import { LensServerPlugin } from './plugin';
|
||||||
|
|
||||||
export type { LensServerPluginSetup } 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';
|
export type { LensDocShape715 } from './migrations/types';
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
* 2.0.
|
* 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 DataViewsServerPluginStart } from '@kbn/data-views-plugin/server';
|
||||||
import {
|
import {
|
||||||
PluginStart as DataPluginStart,
|
PluginStart as DataPluginStart,
|
||||||
|
@ -64,7 +64,7 @@ export interface LensServerPluginSetup {
|
||||||
export class LensServerPlugin implements Plugin<LensServerPluginSetup, {}, {}, {}> {
|
export class LensServerPlugin implements Plugin<LensServerPluginSetup, {}, {}, {}> {
|
||||||
private customVisualizationMigrations: CustomVisualizationMigrations = {};
|
private customVisualizationMigrations: CustomVisualizationMigrations = {};
|
||||||
|
|
||||||
constructor() {}
|
constructor(private initializerContext: PluginInitializerContext) {}
|
||||||
|
|
||||||
setup(core: CoreSetup<PluginStartContract>, plugins: PluginSetupContract) {
|
setup(core: CoreSetup<PluginStartContract>, plugins: PluginSetupContract) {
|
||||||
const getFilterMigrations = plugins.data.query.filterManager.getAllMigrations.bind(
|
const getFilterMigrations = plugins.data.query.filterManager.getAllMigrations.bind(
|
||||||
|
@ -79,7 +79,10 @@ export class LensServerPlugin implements Plugin<LensServerPluginSetup, {}, {}, {
|
||||||
|
|
||||||
plugins.contentManagement.register({
|
plugins.contentManagement.register({
|
||||||
id: CONTENT_ID,
|
id: CONTENT_ID,
|
||||||
storage: new LensStorage(),
|
storage: new LensStorage({
|
||||||
|
throwOnResultValidationError: this.initializerContext.env.mode.dev,
|
||||||
|
logger: this.initializerContext.logger.get('storage'),
|
||||||
|
}),
|
||||||
version: {
|
version: {
|
||||||
latest: LATEST_VERSION,
|
latest: LATEST_VERSION,
|
||||||
},
|
},
|
||||||
|
|
|
@ -88,6 +88,8 @@
|
||||||
"@kbn/ebt-tools",
|
"@kbn/ebt-tools",
|
||||||
"@kbn/chart-expressions-common",
|
"@kbn/chart-expressions-common",
|
||||||
"@kbn/search-response-warnings",
|
"@kbn/search-response-warnings",
|
||||||
|
"@kbn/logging",
|
||||||
|
"@kbn/core-plugins-server",
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"target/**/*",
|
"target/**/*",
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
import { SOContentStorage, tagsToFindOptions } from '@kbn/content-management-utils';
|
import { SOContentStorage, tagsToFindOptions } from '@kbn/content-management-utils';
|
||||||
import { SavedObjectsFindOptions } from '@kbn/core-saved-objects-api-server';
|
import { SavedObjectsFindOptions } from '@kbn/core-saved-objects-api-server';
|
||||||
|
import type { Logger } from '@kbn/logging';
|
||||||
import { CONTENT_ID } from '../../common/content_management';
|
import { CONTENT_ID } from '../../common/content_management';
|
||||||
import { cmServicesDefinition } from '../../common/content_management/cm_services';
|
import { cmServicesDefinition } from '../../common/content_management/cm_services';
|
||||||
import type { MapCrudTypes } from '../../common/content_management';
|
import type { MapCrudTypes } from '../../common/content_management';
|
||||||
|
@ -27,7 +28,13 @@ const searchArgsToSOFindOptions = (args: MapCrudTypes['SearchIn']): SavedObjects
|
||||||
};
|
};
|
||||||
|
|
||||||
export class MapsStorage extends SOContentStorage<MapCrudTypes> {
|
export class MapsStorage extends SOContentStorage<MapCrudTypes> {
|
||||||
constructor() {
|
constructor({
|
||||||
|
logger,
|
||||||
|
throwOnResultValidationError,
|
||||||
|
}: {
|
||||||
|
logger: Logger;
|
||||||
|
throwOnResultValidationError: boolean;
|
||||||
|
}) {
|
||||||
super({
|
super({
|
||||||
savedObjectType: CONTENT_ID,
|
savedObjectType: CONTENT_ID,
|
||||||
cmServicesDefinition,
|
cmServicesDefinition,
|
||||||
|
@ -40,6 +47,8 @@ export class MapsStorage extends SOContentStorage<MapCrudTypes> {
|
||||||
'layerListJSON',
|
'layerListJSON',
|
||||||
'uiStateJSON',
|
'uiStateJSON',
|
||||||
],
|
],
|
||||||
|
logger,
|
||||||
|
throwOnResultValidationError,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -204,7 +204,10 @@ export class MapsPlugin implements Plugin {
|
||||||
|
|
||||||
contentManagement.register({
|
contentManagement.register({
|
||||||
id: CONTENT_ID,
|
id: CONTENT_ID,
|
||||||
storage: new MapsStorage(),
|
storage: new MapsStorage({
|
||||||
|
throwOnResultValidationError: this._initializerContext.env.mode.dev,
|
||||||
|
logger: this._logger.get('storage'),
|
||||||
|
}),
|
||||||
version: {
|
version: {
|
||||||
latest: LATEST_VERSION,
|
latest: LATEST_VERSION,
|
||||||
},
|
},
|
||||||
|
|
|
@ -73,6 +73,7 @@
|
||||||
"@kbn/content-management-table-list-view-table",
|
"@kbn/content-management-table-list-view-table",
|
||||||
"@kbn/content-management-table-list-view",
|
"@kbn/content-management-table-list-view",
|
||||||
"@kbn/serverless",
|
"@kbn/serverless",
|
||||||
|
"@kbn/logging",
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"target/**/*",
|
"target/**/*",
|
||||||
|
|
|
@ -142,7 +142,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||||
const errorMessages = await Promise.all(failureElements.map((el) => el.getVisibleText()));
|
const errorMessages = await Promise.all(failureElements.map((el) => el.getVisibleText()));
|
||||||
|
|
||||||
expect(errorMessages).to.eql([
|
expect(errorMessages).to.eql([
|
||||||
'Bad Request',
|
'Visualization type not found.',
|
||||||
'The visualization type lnsUNKNOWN could not be resolved.',
|
'The visualization type lnsUNKNOWN could not be resolved.',
|
||||||
'Could not find datasource for the visualization',
|
'Could not find datasource for the visualization',
|
||||||
]);
|
]);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue