[data views] Content management api implementation (#155803)

## Summary

Data views implements the content management api and associated minor
changes. The bulk of the changes are in
`(common|public|server)/content_management)`

Closes https://github.com/elastic/kibana/issues/157069

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Lukas Olson <olson.lukas@gmail.com>
This commit is contained in:
Matthew Kime 2023-05-24 13:29:59 -05:00 committed by GitHub
parent 282305fd65
commit 5fa2226b06
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 592 additions and 171 deletions

View file

@ -21,6 +21,7 @@ import type {
SavedObjectsUpdateOptions,
SavedObjectsFindResult,
} from '@kbn/core-saved-objects-api-server';
import { pick } from 'lodash';
import type {
CMCrudTypes,
ServicesDefinitionSet,
@ -44,16 +45,19 @@ type PartialSavedObject<T> = Omit<SavedObject<Partial<T>>, 'references'> & {
function savedObjectToItem<Attributes extends object, Item extends SOWithMetadata>(
savedObject: SavedObject<Attributes>,
allowedSavedObjectAttributes: string[],
partial: false
): Item;
function savedObjectToItem<Attributes extends object, PartialItem extends SOWithMetadata>(
savedObject: PartialSavedObject<Attributes>,
allowedSavedObjectAttributes: string[],
partial: true
): PartialItem;
function savedObjectToItem<Attributes extends object>(
savedObject: SavedObject<Attributes> | PartialSavedObject<Attributes>
savedObject: SavedObject<Attributes> | PartialSavedObject<Attributes>,
allowedSavedObjectAttributes: string[]
): SOWithMetadata | SOWithMetadataPartial {
const {
id,
@ -64,6 +68,7 @@ function savedObjectToItem<Attributes extends object>(
references,
error,
namespaces,
version,
} = savedObject;
return {
@ -71,10 +76,11 @@ function savedObjectToItem<Attributes extends object>(
type,
updatedAt,
createdAt,
attributes,
attributes: pick(attributes, allowedSavedObjectAttributes),
references,
error,
namespaces,
version,
};
}
@ -123,6 +129,8 @@ export type UpdateArgsToSoUpdateOptions<Types extends CMCrudTypes> = (
export interface SOContentStorageConstrutorParams<Types extends CMCrudTypes> {
savedObjectType: string;
cmServicesDefinition: ServicesDefinitionSet;
// this is necessary since unexpected saved object attributes could cause schema validation to fail
allowedSavedObjectAttributes: string[];
createArgsToSoCreateOptions?: CreateArgsToSoCreateOptions<Types>;
updateArgsToSoUpdateOptions?: UpdateArgsToSoUpdateOptions<Types>;
searchArgsToSOFindOptions?: SearchArgsToSOFindOptions<Types>;
@ -144,6 +152,7 @@ export abstract class SOContentStorage<Types extends CMCrudTypes>
updateArgsToSoUpdateOptions,
searchArgsToSOFindOptions,
enableMSearch,
allowedSavedObjectAttributes,
}: SOContentStorageConstrutorParams<Types>) {
this.savedObjectType = savedObjectType;
this.cmServicesDefinition = cmServicesDefinition;
@ -152,6 +161,7 @@ export abstract class SOContentStorage<Types extends CMCrudTypes>
this.updateArgsToSoUpdateOptions =
updateArgsToSoUpdateOptions || updateArgsToSoUpdateOptionsDefault;
this.searchArgsToSOFindOptions = searchArgsToSOFindOptions || searchArgsToSOFindOptionsDefault;
this.allowedSavedObjectAttributes = allowedSavedObjectAttributes;
if (enableMSearch) {
this.mSearch = {
@ -163,7 +173,13 @@ export abstract class SOContentStorage<Types extends CMCrudTypes>
const { value, error: resultError } = transforms.mSearch.out.result.down<
Types['Item'],
Types['Item']
>(savedObjectToItem(savedObject as SavedObjectsFindResult<Types['Attributes']>, false));
>(
savedObjectToItem(
savedObject as SavedObjectsFindResult<Types['Attributes']>,
this.allowedSavedObjectAttributes,
false
)
);
if (resultError) {
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
@ -180,6 +196,7 @@ export abstract class SOContentStorage<Types extends CMCrudTypes>
private createArgsToSoCreateOptions: CreateArgsToSoCreateOptions<Types>;
private updateArgsToSoUpdateOptions: UpdateArgsToSoUpdateOptions<Types>;
private searchArgsToSOFindOptions: SearchArgsToSOFindOptions<Types>;
private allowedSavedObjectAttributes: string[];
mSearch?: {
savedObjectType: string;
@ -199,7 +216,7 @@ export abstract class SOContentStorage<Types extends CMCrudTypes>
} = await soClient.resolve<Types['Attributes']>(this.savedObjectType, id);
const response: Types['GetOut'] = {
item: savedObjectToItem(savedObject, false),
item: savedObjectToItem(savedObject, this.allowedSavedObjectAttributes, false),
meta: {
aliasPurpose,
aliasTargetId,
@ -264,7 +281,7 @@ export abstract class SOContentStorage<Types extends CMCrudTypes>
Types['CreateOut'],
Types['CreateOut']
>({
item: savedObjectToItem(savedObject, false),
item: savedObjectToItem(savedObject, this.allowedSavedObjectAttributes, false),
});
if (resultError) {
@ -315,7 +332,7 @@ export abstract class SOContentStorage<Types extends CMCrudTypes>
Types['UpdateOut'],
Types['UpdateOut']
>({
item: savedObjectToItem(partialSavedObject, true),
item: savedObjectToItem(partialSavedObject, this.allowedSavedObjectAttributes, true),
});
if (resultError) {
@ -325,9 +342,14 @@ export abstract class SOContentStorage<Types extends CMCrudTypes>
return value;
}
async delete(ctx: StorageContext, id: string): Promise<Types['DeleteOut']> {
async delete(
ctx: StorageContext,
id: string,
// force is necessary to delete saved objects that exist in multiple namespaces
options?: { force: boolean }
): Promise<Types['DeleteOut']> {
const soClient = await savedObjectClientFromRequest(ctx);
await soClient.delete(this.savedObjectType, id);
await soClient.delete(this.savedObjectType, id, { force: options?.force ?? false });
return { success: true };
}
@ -361,7 +383,9 @@ export abstract class SOContentStorage<Types extends CMCrudTypes>
Types['SearchOut'],
Types['SearchOut']
>({
hits: response.saved_objects.map((so) => savedObjectToItem(so, false)),
hits: response.saved_objects.map((so) =>
savedObjectToItem(so, this.allowedSavedObjectAttributes, false)
),
pagination: {
total: response.total,
},

View file

@ -176,7 +176,7 @@ export interface SavedObjectUpdateOptions<Attributes = unknown> {
}
/** Return value for Saved Object get, T is item returned */
export type GetResultSO<T extends object> = GetResult<
export type GetResultSO<T extends object = object> = GetResult<
T,
{
outcome: 'exactMatch' | 'aliasMatch' | 'conflict';

View file

@ -46,6 +46,7 @@ describe('kibanaContextFn', () => {
delete: jest.fn(),
find: jest.fn(),
get: jest.fn(),
getSavedSearch: jest.fn(),
update: jest.fn(),
},
};
@ -53,7 +54,7 @@ describe('kibanaContextFn', () => {
it('merges and deduplicates queries from different sources', async () => {
const { fn } = kibanaContextFn;
startServicesMock.savedObjectsClient.get.mockResolvedValue({
startServicesMock.savedObjectsClient.getSavedSearch.mockResolvedValue({
attributes: {
kibanaSavedObjectMeta: {
searchSourceJSON: JSON.stringify({

View file

@ -129,7 +129,7 @@ export const getKibanaContextFn = (
let filters = [...(input?.filters || [])];
if (args.savedSearchId) {
const obj = await savedObjectsClient.get('search', args.savedSearchId);
const obj = await savedObjectsClient.getSavedSearch(args.savedSearchId);
const search = (obj.attributes as any).kibanaSavedObjectMeta.searchSourceJSON as string;
const { query, filter } = getParsedValue(search, {});

View file

@ -73,6 +73,7 @@ exports[`IndexedFieldsTable IndexedFieldsTable with rollup index pattern should
"excluded": false,
"format": "",
"hasRuntime": false,
"id": "Elastic",
"info": Array [
"Rollup aggregations:",
"terms",
@ -92,6 +93,7 @@ exports[`IndexedFieldsTable IndexedFieldsTable with rollup index pattern should
"excluded": false,
"format": "",
"hasRuntime": false,
"id": "timestamp",
"info": Array [
"Rollup aggregations:",
"date_histogram (interval: 30s, delay: 30s, UTC)",
@ -111,6 +113,7 @@ exports[`IndexedFieldsTable IndexedFieldsTable with rollup index pattern should
"excluded": false,
"format": "",
"hasRuntime": false,
"id": "conflictingField",
"info": Array [],
"isMapped": false,
"isUserEditable": false,
@ -126,6 +129,7 @@ exports[`IndexedFieldsTable IndexedFieldsTable with rollup index pattern should
"excluded": false,
"format": "",
"hasRuntime": false,
"id": "amount",
"info": Array [
"Rollup aggregations:",
"histogram (interval: 5)",
@ -149,6 +153,7 @@ exports[`IndexedFieldsTable IndexedFieldsTable with rollup index pattern should
"excluded": false,
"format": "",
"hasRuntime": true,
"id": "runtime",
"info": Array [],
"isMapped": false,
"isUserEditable": false,
@ -191,6 +196,7 @@ exports[`IndexedFieldsTable should filter based on the query bar 1`] = `
"excluded": false,
"format": "",
"hasRuntime": false,
"id": "Elastic",
"info": Array [],
"isMapped": false,
"isUserEditable": false,
@ -228,6 +234,7 @@ exports[`IndexedFieldsTable should filter based on the schema filter 1`] = `
"excluded": false,
"format": "",
"hasRuntime": true,
"id": "runtime",
"info": Array [],
"isMapped": false,
"isUserEditable": false,
@ -270,6 +277,7 @@ exports[`IndexedFieldsTable should filter based on the type filter 1`] = `
"excluded": false,
"format": "",
"hasRuntime": false,
"id": "timestamp",
"info": Array [],
"isMapped": false,
"isUserEditable": false,
@ -306,6 +314,7 @@ exports[`IndexedFieldsTable should render normally 1`] = `
"excluded": false,
"format": "",
"hasRuntime": false,
"id": "Elastic",
"info": Array [],
"isMapped": false,
"isUserEditable": false,
@ -322,6 +331,7 @@ exports[`IndexedFieldsTable should render normally 1`] = `
"excluded": false,
"format": "",
"hasRuntime": false,
"id": "timestamp",
"info": Array [],
"isMapped": false,
"isUserEditable": false,
@ -338,6 +348,7 @@ exports[`IndexedFieldsTable should render normally 1`] = `
"excluded": false,
"format": "",
"hasRuntime": false,
"id": "conflictingField",
"info": Array [],
"isMapped": false,
"isUserEditable": false,
@ -353,6 +364,7 @@ exports[`IndexedFieldsTable should render normally 1`] = `
"excluded": false,
"format": "",
"hasRuntime": false,
"id": "amount",
"info": Array [],
"isMapped": false,
"isUserEditable": false,
@ -368,6 +380,7 @@ exports[`IndexedFieldsTable should render normally 1`] = `
"excluded": false,
"format": "",
"hasRuntime": true,
"id": "runtime",
"info": Array [],
"isMapped": false,
"isUserEditable": false,

View file

@ -76,6 +76,7 @@ export class IndexedFieldsTable extends Component<
fields.map((field) => {
return {
...field.spec,
id: field.name,
type: field.esTypes?.join(', ') || '',
kbnType: field.type,
displayName: field.displayName,
@ -114,6 +115,7 @@ export class IndexedFieldsTable extends Component<
},
},
name,
id: name,
type: 'composite',
kbnType: '',
displayName: name,

View file

@ -0,0 +1,21 @@
/*
* 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 type {
ContentManagementServicesDefinition as ServicesDefinition,
Version,
} from '@kbn/object-versioning';
// We export the versionned service definition from this file and not the barrel to avoid adding
// the schemas in the "public" js bundle
import { serviceDefinition as v1 } from './v1/cm_services';
export const cmServicesDefinition: { [version: Version]: ServicesDefinition } = {
1: v1,
};

View file

@ -0,0 +1,9 @@
/*
* 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.
*/
export * from './v1';

View file

@ -0,0 +1,119 @@
/*
* 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 { schema } from '@kbn/config-schema';
import type { ContentManagementServicesDefinition as ServicesDefinition } from '@kbn/object-versioning';
import {
savedObjectSchema,
objectTypeToGetResultSchema,
createOptionsSchemas,
updateOptionsSchema,
createResultSchema,
searchOptionsSchemas,
} from '@kbn/content-management-utils';
import { serializedFieldFormatSchema, fieldSpecSchema } from '../../schemas';
const dataViewAttributesSchema = schema.object(
{
title: schema.string(),
type: schema.maybe(schema.literal('rollup')),
timeFieldName: schema.maybe(schema.string()),
sourceFilters: schema.maybe(
schema.arrayOf(
schema.object({
value: schema.string(),
})
)
),
fields: schema.maybe(schema.arrayOf(fieldSpecSchema)),
typeMeta: schema.maybe(schema.object({}, { unknowns: 'allow' })),
fieldFormatMap: schema.maybe(schema.recordOf(schema.string(), serializedFieldFormatSchema)),
fieldAttrs: schema.maybe(
schema.recordOf(
schema.string(),
schema.object({
customLabel: schema.maybe(schema.string()),
count: schema.maybe(schema.number()),
})
)
),
allowNoIndex: schema.maybe(schema.boolean()),
runtimeFieldMap: schema.maybe(schema.any()),
name: schema.maybe(schema.string()),
},
{ unknowns: 'forbid' }
);
const dataViewSavedObjectSchema = savedObjectSchema(dataViewAttributesSchema);
const dataViewCreateOptionsSchema = schema.object({
id: createOptionsSchemas.id,
initialNamespaces: createOptionsSchemas.initialNamespaces,
});
const dataViewSearchOptionsSchema = schema.object({
searchFields: searchOptionsSchemas.searchFields,
fields: searchOptionsSchemas.fields,
});
const dataViewUpdateOptionsSchema = schema.object({
version: updateOptionsSchema.version,
refresh: updateOptionsSchema.refresh,
retryOnConflict: updateOptionsSchema.retryOnConflict,
});
// Content management service definition.
// We need it for BWC support between different versions of the content
export const serviceDefinition: ServicesDefinition = {
get: {
out: {
result: {
schema: objectTypeToGetResultSchema(dataViewSavedObjectSchema),
},
},
},
create: {
in: {
options: {
schema: dataViewCreateOptionsSchema,
},
data: {
schema: dataViewAttributesSchema,
},
},
out: {
result: {
schema: createResultSchema(dataViewSavedObjectSchema),
},
},
},
update: {
in: {
options: {
schema: dataViewUpdateOptionsSchema,
},
data: {
schema: dataViewAttributesSchema,
},
},
},
search: {
in: {
options: {
schema: dataViewSearchOptionsSchema,
},
},
},
mSearch: {
out: {
result: {
schema: dataViewSavedObjectSchema,
},
},
},
};

View file

@ -0,0 +1,17 @@
/*
* 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 { DATA_VIEW_SAVED_OBJECT_TYPE as DataViewSOType } from '../..';
export { DataViewSOType };
/**
* Data view saved object version.
*/
export const LATEST_VERSION = 1;
export type DataViewContentType = typeof DataViewSOType;

View file

@ -0,0 +1,15 @@
/*
* 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.
*/
export { LATEST_VERSION } from './constants';
export type { DataViewCrudTypes } from './types';
export type { DataViewContentType } from './constants';
export { DataViewSOType } from './constants';

View file

@ -0,0 +1,40 @@
/*
* 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 type {
ContentManagementCrudTypes,
SavedObjectCreateOptions,
SavedObjectSearchOptions,
SavedObjectUpdateOptions,
} from '@kbn/content-management-utils';
import { DataViewAttributes } from '../../types';
import { DataViewContentType } from './constants';
interface DataViewCreateOptions {
id?: SavedObjectCreateOptions['id'];
initialNamespaces?: SavedObjectCreateOptions['initialNamespaces'];
}
interface DataViewUpdateOptions {
version?: SavedObjectUpdateOptions['version'];
refresh?: SavedObjectUpdateOptions['refresh'];
retryOnConflict?: SavedObjectUpdateOptions['retryOnConflict'];
}
interface DataViewSearchOptions {
searchFields?: SavedObjectSearchOptions['searchFields'];
fields?: SavedObjectSearchOptions['fields'];
}
export type DataViewCrudTypes = ContentManagementCrudTypes<
DataViewContentType,
DataViewAttributes,
DataViewCreateOptions,
DataViewUpdateOptions,
DataViewSearchOptions
>;

View file

@ -6,14 +6,12 @@
* Side Public License, v 1.
*/
import { SavedObject } from '@kbn/core/types';
import { stubFieldSpecMap, stubLogstashFieldSpecMap } from './field.stub';
import { createStubDataView } from './data_views/data_view.stub';
export {
createStubDataView,
createStubDataView as createStubIndexPattern,
} from './data_views/data_view.stub';
import { DataViewAttributes } from './types';
export const stubDataView = createStubDataView({
spec: {
@ -43,9 +41,7 @@ export const stubLogstashDataView = createStubDataView({
},
});
export function stubbedSavedObjectDataView(
id: string | null = null
): SavedObject<DataViewAttributes> {
export function stubbedSavedObjectDataView(id: string | null = null) {
return {
id: id ?? '',
type: 'index-pattern',

View file

@ -77,7 +77,7 @@ describe('IndexPatterns', () => {
savedObjectsClient.find = jest.fn(
() => Promise.resolve([indexPatternObj]) as Promise<Array<SavedObject<any>>>
);
savedObjectsClient.delete = jest.fn(() => Promise.resolve({}) as Promise<any>);
savedObjectsClient.delete = jest.fn(() => Promise.resolve() as Promise<any>);
savedObjectsClient.create = jest.fn();
savedObjectsClient.get = jest.fn().mockImplementation(async (type, id) => {
await new Promise((resolve) => setTimeout(resolve, SOClientGetDelay));
@ -87,23 +87,21 @@ describe('IndexPatterns', () => {
attributes: object.attributes,
};
});
savedObjectsClient.update = jest
.fn()
.mockImplementation(async (type, id, body, { version }) => {
if (object.version !== version) {
throw new Object({
res: {
status: 409,
},
});
}
object.attributes.title = body.title;
object.version += 'a';
return {
id: object.id,
version: object.version,
};
});
savedObjectsClient.update = jest.fn().mockImplementation(async (id, body, { version }) => {
if (object.version !== version) {
throw new Object({
res: {
status: 409,
},
});
}
object.attributes.title = body.title;
object.version += 'a';
return {
id: object.id,
version: object.version,
};
});
apiClient = createFieldsFetcher();
@ -267,7 +265,6 @@ describe('IndexPatterns', () => {
test('savedObjectCache pre-fetches title, type, typeMeta', async () => {
expect(await indexPatterns.getIds()).toEqual(['id']);
expect(savedObjectsClient.find).toHaveBeenCalledWith({
type: 'index-pattern',
fields: ['title', 'type', 'typeMeta', 'name'],
perPage: 10000,
});
@ -376,7 +373,6 @@ describe('IndexPatterns', () => {
await indexPatterns.find('kibana*', size);
expect(savedObjectsClient.find).lastCalledWith({
type: 'index-pattern',
fields: ['title'],
search,
searchFields: ['title'],
@ -449,13 +445,13 @@ describe('IndexPatterns', () => {
dataView.setFieldFormat('field', { id: 'formatId' });
await indexPatterns.updateSavedObject(dataView);
let lastCall = (savedObjectsClient.update as jest.Mock).mock.calls.pop() ?? [];
let [, , attrs] = lastCall;
let [, attrs] = lastCall;
expect(attrs).toHaveProperty('fieldFormatMap');
expect(attrs.fieldFormatMap).toMatchInlineSnapshot(`"{\\"field\\":{\\"id\\":\\"formatId\\"}}"`);
dataView.deleteFieldFormat('field');
await indexPatterns.updateSavedObject(dataView);
lastCall = (savedObjectsClient.update as jest.Mock).mock.calls.pop() ?? [];
[, , attrs] = lastCall;
[, attrs] = lastCall;
// https://github.com/elastic/kibana/issues/134873: must keep an empty object and not delete it
expect(attrs).toHaveProperty('fieldFormatMap');
@ -554,7 +550,7 @@ describe('IndexPatterns', () => {
savedObjectsClient.get = jest
.fn()
.mockImplementation((type: string, id: string) =>
.mockImplementation((id: string) =>
Promise.resolve({ id, version: 'a', attributes: { title: 'title' } })
);
@ -586,7 +582,7 @@ describe('IndexPatterns', () => {
savedObjectsClient.get = jest
.fn()
.mockImplementation((type: string, id: string) =>
.mockImplementation((id: string) =>
Promise.resolve({ id, version: 'a', attributes: { title: '1' } })
);

View file

@ -10,9 +10,7 @@ import { i18n } from '@kbn/i18n';
import type { PublicMethodsOf } from '@kbn/utility-types';
import { castEsToKbnFieldTypeName } from '@kbn/field-types';
import { FieldFormatsStartCommon, FORMATS_UI_SETTINGS } from '@kbn/field-formats-plugin/common';
import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/common';
import { v4 as uuidv4 } from 'uuid';
import { DATA_VIEW_SAVED_OBJECT_TYPE } from '..';
import { SavedObjectsClientCommon } from '../types';
import { createDataViewCache } from '.';
@ -31,6 +29,7 @@ import {
DataViewFieldMap,
TypeMeta,
} from '../types';
import { META_FIELDS, SavedObject } from '..';
import { DataViewMissingIndices } from '../lib';
import { findByName } from '../utils';
@ -175,7 +174,7 @@ export interface DataViewsServicePublicMethods {
* Delete data view
* @param indexPatternId - Id of the data view to delete.
*/
delete: (indexPatternId: string) => Promise<{}>;
delete: (indexPatternId: string) => Promise<void>;
/**
* Takes field array and field attributes and returns field map by name.
* @param fields - Array of fieldspecs
@ -349,8 +348,7 @@ export class DataViewsService {
* Refresh cache of index pattern ids and titles.
*/
private async refreshSavedObjectsCache() {
const so = await this.savedObjectsClient.find<DataViewSavedObjectAttrs>({
type: DATA_VIEW_SAVED_OBJECT_TYPE,
const so = await this.savedObjectsClient.find({
fields: ['title', 'type', 'typeMeta', 'name'],
perPage: 10000,
});
@ -392,8 +390,7 @@ export class DataViewsService {
* @returns DataView[]
*/
find = async (search: string, size: number = 10): Promise<DataView[]> => {
const savedObjects = await this.savedObjectsClient.find<DataViewSavedObjectAttrs>({
type: DATA_VIEW_SAVED_OBJECT_TYPE,
const savedObjects = await this.savedObjectsClient.find({
fields: ['title'],
search,
searchFields: ['title'],
@ -726,14 +723,7 @@ export class DataViewsService {
id: string,
displayErrors: boolean = true
): Promise<DataView> => {
const savedObject = await this.savedObjectsClient.get<DataViewAttributes>(
DATA_VIEW_SAVED_OBJECT_TYPE,
id
);
if (!savedObject.version) {
throw new SavedObjectNotFound('data view', id, 'management/kibana/dataViews');
}
const savedObject = await this.savedObjectsClient.get(id);
return this.initFromSavedObject(savedObject, displayErrors);
};
@ -1006,14 +996,11 @@ export class DataViewsService {
}
const body = dataView.getAsSavedObjectBody();
const response: SavedObject<DataViewAttributes> = (await this.savedObjectsClient.create(
DATA_VIEW_SAVED_OBJECT_TYPE,
body,
{
id: dataView.id,
initialNamespaces: dataView.namespaces.length > 0 ? dataView.namespaces : undefined,
}
)) as SavedObject<DataViewAttributes>;
const response: SavedObject<DataViewAttributes> = (await this.savedObjectsClient.create(body, {
id: dataView.id,
initialNamespaces: dataView.namespaces.length > 0 ? dataView.namespaces : undefined,
})) as SavedObject<DataViewAttributes>;
const createdIndexPattern = await this.initFromSavedObject(response, displayErrors);
if (this.savedObjectsCache) {
@ -1055,7 +1042,7 @@ export class DataViewsService {
});
return this.savedObjectsClient
.update(DATA_VIEW_SAVED_OBJECT_TYPE, indexPattern.id, body, {
.update(indexPattern.id, body, {
version: indexPattern.version,
})
.then((response) => {
@ -1136,7 +1123,7 @@ export class DataViewsService {
throw new DataViewInsufficientAccessError(indexPatternId);
}
this.dataViewCache.clear(indexPatternId);
return this.savedObjectsClient.delete(DATA_VIEW_SAVED_OBJECT_TYPE, indexPatternId);
return this.savedObjectsClient.delete(indexPatternId);
}
/**

View file

@ -13,6 +13,8 @@ export {
DATA_VIEW_SAVED_OBJECT_TYPE,
} from './constants';
export { LATEST_VERSION } from './content_management/v1/constants';
export type { ToSpecConfig } from './fields';
export type { IIndexPatternFieldList } from './fields';
export {

View file

@ -7,7 +7,7 @@
*/
import { schema, Type } from '@kbn/config-schema';
import { /* RUNTIME_FIELD_TYPES,*/ RuntimeType } from '../../../common';
import { RuntimeType } from '.';
export const serializedFieldFormatSchema = schema.object({
id: schema.maybe(schema.string()),
@ -16,11 +16,11 @@ export const serializedFieldFormatSchema = schema.object({
export const fieldSpecSchemaFields = {
name: schema.string({
maxLength: 1_000,
maxLength: 1000,
}),
type: schema.string({
defaultValue: 'string',
maxLength: 1_000,
maxLength: 1000,
}),
count: schema.maybe(
schema.number({
@ -29,7 +29,7 @@ export const fieldSpecSchemaFields = {
),
script: schema.maybe(
schema.string({
maxLength: 1_000_000,
maxLength: 1000000,
})
),
format: schema.maybe(serializedFieldFormatSchema),

View file

@ -7,11 +7,7 @@
*/
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import type {
SavedObject,
SavedObjectsCreateOptions,
SavedObjectsUpdateOptions,
} from '@kbn/core/public';
import type { SavedObject } from '@kbn/core/server';
import type { ErrorToastOptions, ToastInputFields } from '@kbn/core-notifications-browser';
import type { DataViewFieldBase } from '@kbn/es-query';
import type { SerializedFieldFormat } from '@kbn/field-formats-plugin/common';
@ -119,7 +115,7 @@ export interface DataViewAttributes {
/**
* Fields as a serialized array of field specs
*/
fields: string;
fields?: string;
/**
* Data view title
*/
@ -236,10 +232,6 @@ export interface UiSettingsCommon {
* @public
*/
export interface SavedObjectsClientCommonFindArgs {
/**
* Saved object type
*/
type: string | string[];
/**
* Saved object fields
*/
@ -267,13 +259,23 @@ export interface SavedObjectsClientCommon {
* Search for saved objects
* @param options - options for search
*/
find: <T = unknown>(options: SavedObjectsClientCommonFindArgs) => Promise<Array<SavedObject<T>>>;
find: (
options: SavedObjectsClientCommonFindArgs
) => Promise<Array<SavedObject<DataViewAttributes>>>;
/**
* Get a single saved object by id
* @param type - type of saved object
* @param id - id of saved object
*/
get: <T = unknown>(type: string, id: string) => Promise<SavedObject<T>>;
get: (id: string) => Promise<SavedObject<DataViewAttributes>>;
/**
* Update a saved object by id
* @param type - type of saved object
* @param id - id of saved object
* @param attributes - attributes to update
* @param options - client options
*/
getSavedSearch: (id: string) => Promise<SavedObject>;
/**
* Update a saved object by id
* @param type - type of saved object
@ -282,28 +284,26 @@ export interface SavedObjectsClientCommon {
* @param options - client options
*/
update: (
type: string,
id: string,
attributes: DataViewAttributes,
options: SavedObjectsUpdateOptions
options: { version?: string }
) => Promise<SavedObject>;
/**
* Create a saved object
* @param type - type of saved object
* @param attributes - attributes to set
* @param options - client options
*/
create: (
type: string,
attributes: DataViewAttributes,
options: SavedObjectsCreateOptions & { initialNamespaces?: string[] }
// SavedObjectsCreateOptions
options: { id?: string; initialNamespaces?: string[] }
) => Promise<SavedObject>;
/**
* Delete a saved object by id
* @param type - type of saved object
* @param id - id of saved object
*/
delete: (type: string, id: string) => Promise<{}>;
delete: (id: string) => Promise<void>;
}
export interface GetFieldsOptions {

View file

@ -6,11 +6,8 @@
* Side Public License, v 1.
*/
import type { DataViewSavedObjectAttrs } from './data_views';
import type { SavedObjectsClientCommon } from './types';
import { DATA_VIEW_SAVED_OBJECT_TYPE } from './constants';
/**
* Returns an object matching a given name
*
@ -20,8 +17,7 @@ import { DATA_VIEW_SAVED_OBJECT_TYPE } from './constants';
*/
export async function findByName(client: SavedObjectsClientCommon, name: string) {
if (name) {
const savedObjects = await client.find<{ name: DataViewSavedObjectAttrs['name'] }>({
type: DATA_VIEW_SAVED_OBJECT_TYPE,
const savedObjects = await client.find({
perPage: 10,
search: `"${name}"`,
searchFields: ['name.keyword'],

View file

@ -9,7 +9,8 @@
"browser": true,
"requiredPlugins": [
"fieldFormats",
"expressions"
"expressions",
"contentManagement"
],
"optionalPlugins": [
"usageCollection"

View file

@ -7,6 +7,7 @@
*/
import { CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
import { getIndexPatternLoad } from './expressions';
import {
DataViewsPublicPluginSetup,
@ -25,6 +26,9 @@ import { getIndices, HasData } from './services';
import { debounceByKey } from './debounce_by_key';
import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../common/constants';
import { LATEST_VERSION } from '../common/content_management/v1/constants';
export class DataViewsPublicPlugin
implements
Plugin<
@ -38,18 +42,28 @@ export class DataViewsPublicPlugin
public setup(
core: CoreSetup<DataViewsPublicStartDependencies, DataViewsPublicPluginStart>,
{ expressions }: DataViewsPublicSetupDependencies
{ expressions, contentManagement }: DataViewsPublicSetupDependencies
): DataViewsPublicPluginSetup {
expressions.registerFunction(getIndexPatternLoad({ getStartServices: core.getStartServices }));
contentManagement.registry.register({
id: DATA_VIEW_SAVED_OBJECT_TYPE,
version: {
latest: LATEST_VERSION,
},
name: i18n.translate('dataViews.contentManagementType', {
defaultMessage: 'Data view',
}),
});
return {};
}
public start(
core: CoreStart,
{ fieldFormats }: DataViewsPublicStartDependencies
{ fieldFormats, contentManagement }: DataViewsPublicStartDependencies
): DataViewsPublicPluginStart {
const { uiSettings, http, notifications, savedObjects, application } = core;
const { uiSettings, http, notifications, application, savedObjects } = core;
const onNotifDebounced = debounceByKey(
notifications.toasts.add.bind(notifications.toasts),
@ -63,7 +77,10 @@ export class DataViewsPublicPlugin
return new DataViewsServicePublic({
hasData: this.hasData.start(core),
uiSettings: new UiSettingsPublicToCommon(uiSettings),
savedObjectsClient: new SavedObjectsClientPublicToCommon(savedObjects.client),
savedObjectsClient: new SavedObjectsClientPublicToCommon(
contentManagement.client,
savedObjects.client
),
apiClient: new DataViewsApiClient(http),
fieldFormats,
onNotification: (toastInputFields, key) => {

View file

@ -7,22 +7,23 @@
*/
import { SavedObjectsClientPublicToCommon } from './saved_objects_client_wrapper';
import { ContentClient } from '@kbn/content-management-plugin/public';
import { savedObjectsServiceMock } from '@kbn/core/public/mocks';
import { DataViewSavedObjectConflictError } from '../common';
describe('SavedObjectsClientPublicToCommon', () => {
const soClient = savedObjectsServiceMock.createStartContract().client;
const cmClient = {} as ContentClient;
test('get saved object - exactMatch', async () => {
const mockedSavedObject = {
version: 'abc',
};
soClient.resolve = jest
cmClient.get = jest
.fn()
.mockResolvedValue({ outcome: 'exactMatch', saved_object: mockedSavedObject });
const service = new SavedObjectsClientPublicToCommon(soClient);
const result = await service.get('index-pattern', '1');
.mockResolvedValue({ meta: { outcome: 'exactMatch' }, item: mockedSavedObject });
const service = new SavedObjectsClientPublicToCommon(cmClient, soClient);
const result = await service.get('1');
expect(result).toStrictEqual(mockedSavedObject);
});
@ -30,11 +31,11 @@ describe('SavedObjectsClientPublicToCommon', () => {
const mockedSavedObject = {
version: 'def',
};
soClient.resolve = jest
cmClient.get = jest
.fn()
.mockResolvedValue({ outcome: 'aliasMatch', saved_object: mockedSavedObject });
const service = new SavedObjectsClientPublicToCommon(soClient);
const result = await service.get('index-pattern', '1');
.mockResolvedValue({ meta: { outcome: 'aliasMatch' }, item: mockedSavedObject });
const service = new SavedObjectsClientPublicToCommon(cmClient, soClient);
const result = await service.get('1');
expect(result).toStrictEqual(mockedSavedObject);
});
@ -43,13 +44,11 @@ describe('SavedObjectsClientPublicToCommon', () => {
version: 'ghi',
};
soClient.resolve = jest
cmClient.get = jest
.fn()
.mockResolvedValue({ outcome: 'conflict', saved_object: mockedSavedObject });
const service = new SavedObjectsClientPublicToCommon(soClient);
.mockResolvedValue({ meta: { outcome: 'conflict' }, item: mockedSavedObject });
const service = new SavedObjectsClientPublicToCommon(cmClient, soClient);
await expect(service.get('index-pattern', '1')).rejects.toThrow(
DataViewSavedObjectConflictError
);
await expect(service.get('1')).rejects.toThrow(DataViewSavedObjectConflictError);
});
});

View file

@ -6,13 +6,9 @@
* Side Public License, v 1.
*/
import {
SavedObjectsClientContract,
SavedObjectsCreateOptions,
SavedObjectsUpdateOptions,
SimpleSavedObject,
} from '@kbn/core/public';
import { omit } from 'lodash';
import type { ContentClient } from '@kbn/content-management-plugin/public';
import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/common';
import type { SavedObjectsClientContract } from '@kbn/core/public';
import { DataViewSavedObjectConflictError } from '../common/errors';
import {
DataViewAttributes,
@ -21,53 +17,113 @@ import {
SavedObjectsClientCommonFindArgs,
} from '../common/types';
type SOClient = Pick<
SavedObjectsClientContract,
'find' | 'resolve' | 'update' | 'create' | 'delete'
>;
import type { DataViewCrudTypes } from '../common/content_management';
const simpleSavedObjectToSavedObject = <T>(simpleSavedObject: SimpleSavedObject): SavedObject<T> =>
({
version: simpleSavedObject._version,
...omit(simpleSavedObject, '_version'),
} as SavedObject<T>);
import { DataViewSOType } from '../common/content_management';
type SOClient = Pick<SavedObjectsClientContract, 'resolve'>;
export class SavedObjectsClientPublicToCommon implements SavedObjectsClientCommon {
private contentManagementClient: ContentClient;
private savedObjectClient: SOClient;
constructor(savedObjectClient: SOClient) {
constructor(contentManagementClient: ContentClient, savedObjectClient: SOClient) {
this.contentManagementClient = contentManagementClient;
this.savedObjectClient = savedObjectClient;
}
async find<T = unknown>(options: SavedObjectsClientCommonFindArgs) {
const response = (await this.savedObjectClient.find<T>(options)).savedObjects;
return response.map<SavedObject<T>>(simpleSavedObjectToSavedObject);
async find(options: SavedObjectsClientCommonFindArgs) {
const results = await this.contentManagementClient.search<
DataViewCrudTypes['SearchIn'],
DataViewCrudTypes['SearchOut']
>({
contentTypeId: DataViewSOType,
query: {
text: options.search,
limit: options.perPage,
},
options: {
searchFields: options.searchFields,
fields: options.fields,
},
});
return results.hits;
}
async get<T = unknown>(type: string, id: string) {
const response = await this.savedObjectClient.resolve<T>(type, id);
async get(id: string) {
let response: DataViewCrudTypes['GetOut'];
try {
response = await this.contentManagementClient.get<
DataViewCrudTypes['GetIn'],
DataViewCrudTypes['GetOut']
>({
contentTypeId: DataViewSOType,
id,
});
} catch (e) {
if (e.body?.statusCode === 404) {
throw new SavedObjectNotFound('data view', id, 'management/kibana/dataViews');
} else {
throw e;
}
}
if (response.meta.outcome === 'conflict') {
throw new DataViewSavedObjectConflictError(id);
}
return response.item;
}
async getSavedSearch(id: string) {
const response = await this.savedObjectClient.resolve('search', id);
if (response.outcome === 'conflict') {
throw new DataViewSavedObjectConflictError(id);
}
return simpleSavedObjectToSavedObject<T>(response.saved_object);
return response.saved_object;
}
async update(
type: string,
id: string,
attributes: DataViewAttributes,
options: SavedObjectsUpdateOptions<unknown>
options: DataViewCrudTypes['UpdateOptions']
) {
const response = await this.savedObjectClient.update(type, id, attributes, options);
return simpleSavedObjectToSavedObject(response);
const response = await this.contentManagementClient.update<
DataViewCrudTypes['UpdateIn'],
DataViewCrudTypes['UpdateOut']
>({
contentTypeId: DataViewSOType,
id,
data: attributes,
options,
});
// cast is necessary since its the full object and not just the changes
return response.item as SavedObject<DataViewAttributes>;
}
async create(type: string, attributes: DataViewAttributes, options?: SavedObjectsCreateOptions) {
const response = await this.savedObjectClient.create(type, attributes, options);
return simpleSavedObjectToSavedObject(response);
async create(attributes: DataViewAttributes, options: DataViewCrudTypes['CreateOptions']) {
const result = await this.contentManagementClient.create<
DataViewCrudTypes['CreateIn'],
DataViewCrudTypes['CreateOut']
>({
contentTypeId: DataViewSOType,
data: attributes,
options,
});
return result.item;
}
delete(type: string, id: string) {
return this.savedObjectClient.delete(type, id, { force: true });
async delete(id: string) {
await this.contentManagementClient.delete<
DataViewCrudTypes['DeleteIn'],
DataViewCrudTypes['DeleteOut']
>({
contentTypeId: DataViewSOType,
id,
options: { force: true },
});
}
}

View file

@ -8,6 +8,10 @@
import { ExpressionsSetup } from '@kbn/expressions-plugin/public';
import { FieldFormatsSetup, FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import type {
ContentManagementPublicSetup,
ContentManagementPublicStart,
} from '@kbn/content-management-plugin/public';
import { DataViewsServicePublicMethods } from './data_views';
import { HasDataService } from '../common';
@ -82,6 +86,10 @@ export interface DataViewsPublicSetupDependencies {
* Field formats
*/
fieldFormats: FieldFormatsSetup;
/**
* Content management
*/
contentManagement: ContentManagementPublicSetup;
}
/**
@ -92,6 +100,10 @@ export interface DataViewsPublicStartDependencies {
* Field formats
*/
fieldFormats: FieldFormatsStart;
/**
* Content management
*/
contentManagement: ContentManagementPublicStart;
}
/**

View file

@ -0,0 +1,36 @@
/*
* 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 '@kbn/content-management-utils';
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() {
super({
savedObjectType: DataViewSOType,
cmServicesDefinition,
enableMSearch: true,
allowedSavedObjectAttributes: [
'fields',
'title',
'type',
'typeMeta',
'timeFieldName',
'sourceFilters',
'fieldFormatMap',
'fieldAttrs',
'runtimeFieldMap',
'allowNoIndex',
'name',
],
});
}
}

View file

@ -0,0 +1,9 @@
/*
* 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.
*/
export { DataViewsStorage } from './data_views_storage';

View file

@ -14,12 +14,14 @@ import { capabilitiesProvider } from './capabilities_provider';
import { getIndexPatternLoad } from './expressions';
import { registerIndexPatternsUsageCollector } from './register_index_pattern_usage_collection';
import { createScriptedFieldsDeprecationsConfig } from './deprecations';
import { DATA_VIEW_SAVED_OBJECT_TYPE, LATEST_VERSION } from '../common';
import {
DataViewsServerPluginSetup,
DataViewsServerPluginStart,
DataViewsServerPluginSetupDependencies,
DataViewsServerPluginStartDependencies,
} from './types';
import { DataViewsStorage } from './content_management';
export class DataViewsServerPlugin
implements
@ -38,7 +40,7 @@ export class DataViewsServerPlugin
public setup(
core: CoreSetup<DataViewsServerPluginStartDependencies, DataViewsServerPluginStart>,
{ expressions, usageCollection }: DataViewsServerPluginSetupDependencies
{ expressions, usageCollection, contentManagement }: DataViewsServerPluginSetupDependencies
) {
core.savedObjects.registerType(dataViewSavedObjectType);
core.capabilities.registerProvider(capabilitiesProvider);
@ -50,6 +52,14 @@ export class DataViewsServerPlugin
registerIndexPatternsUsageCollector(core.getStartServices, usageCollection);
core.deprecations.registerDeprecations(createScriptedFieldsDeprecationsConfig(core));
contentManagement.register({
id: DATA_VIEW_SAVED_OBJECT_TYPE,
storage: new DataViewsStorage(),
version: {
latest: LATEST_VERSION,
},
});
return {};
}

View file

@ -12,7 +12,11 @@ import { IRouter, StartServicesAccessor } from '@kbn/core/server';
import { DataViewSpec } from '../../common/types';
import { DataViewsService } from '../../common/data_views';
import { handleErrors } from './util/handle_errors';
import { fieldSpecSchema, runtimeFieldSchema, serializedFieldFormatSchema } from './util/schemas';
import {
fieldSpecSchema,
runtimeFieldSchema,
serializedFieldFormatSchema,
} from '../../common/schemas';
import type { DataViewsServerPluginStartDependencies, DataViewsServerPluginStart } from '../types';
import {
DATA_VIEW_PATH,

View file

@ -12,7 +12,7 @@ import { IRouter, StartServicesAccessor } from '@kbn/core/server';
import { SerializedFieldFormat } from '@kbn/field-formats-plugin/common';
import { DataViewsService } from '../../../common';
import { handleErrors } from '../util/handle_errors';
import { serializedFieldFormatSchema } from '../util/schemas';
import { serializedFieldFormatSchema } from '../../../common/schemas';
import type {
DataViewsServerPluginStartDependencies,
DataViewsServerPluginStart,

View file

@ -12,7 +12,7 @@ import { IRouter, StartServicesAccessor } from '@kbn/core/server';
import { DataViewsService } from '../../../common/data_views';
import { RuntimeField } from '../../../common/types';
import { handleErrors } from '../util/handle_errors';
import { runtimeFieldSchema } from '../util/schemas';
import { runtimeFieldSchema } from '../../../common/schemas';
import type {
DataViewsServerPluginStart,
DataViewsServerPluginStartDependencies,

View file

@ -12,7 +12,7 @@ import { IRouter, StartServicesAccessor } from '@kbn/core/server';
import { DataViewsService } from '../../../common/data_views';
import { RuntimeField } from '../../../common/types';
import { handleErrors } from '../util/handle_errors';
import { runtimeFieldSchema } from '../util/schemas';
import { runtimeFieldSchema } from '../../../common/schemas';
import type {
DataViewsServerPluginStart,
DataViewsServerPluginStartDependencies,

View file

@ -13,7 +13,7 @@ import { DataViewsService } from '../../../common/data_views';
import { RuntimeField } from '../../../common/types';
import { ErrorIndexPatternFieldNotFound } from '../../error';
import { handleErrors } from '../util/handle_errors';
import { runtimeFieldSchema } from '../util/schemas';
import { runtimeFieldSchema } from '../../../common/schemas';
import type {
DataViewsServerPluginStart,
DataViewsServerPluginStartDependencies,

View file

@ -9,7 +9,7 @@
import { schema } from '@kbn/config-schema';
import { IRouter, StartServicesAccessor } from '@kbn/core/server';
import { handleErrors } from '../util/handle_errors';
import { fieldSpecSchema } from '../util/schemas';
import { fieldSpecSchema } from '../../../common/schemas';
import type {
DataViewsServerPluginStart,
DataViewsServerPluginStartDependencies,

View file

@ -9,7 +9,7 @@
import { schema } from '@kbn/config-schema';
import { IRouter, StartServicesAccessor } from '@kbn/core/server';
import { handleErrors } from '../util/handle_errors';
import { fieldSpecSchema } from '../util/schemas';
import { fieldSpecSchema } from '../../../common/schemas';
import type {
DataViewsServerPluginStart,
DataViewsServerPluginStartDependencies,

View file

@ -11,7 +11,7 @@ import { IRouter, StartServicesAccessor } from '@kbn/core/server';
import { FieldSpec } from '../../../common';
import { ErrorIndexPatternFieldNotFound } from '../../error';
import { handleErrors } from '../util/handle_errors';
import { fieldSpecSchemaFields } from '../util/schemas';
import { fieldSpecSchemaFields } from '../../../common/schemas';
import type {
DataViewsServerPluginStart,
DataViewsServerPluginStartDependencies,

View file

@ -12,7 +12,11 @@ import { IRouter, StartServicesAccessor } from '@kbn/core/server';
import { DataViewsService, DataView } from '../../common/data_views';
import { DataViewSpec } from '../../common/types';
import { handleErrors } from './util/handle_errors';
import { fieldSpecSchema, runtimeFieldSchema, serializedFieldFormatSchema } from './util/schemas';
import {
fieldSpecSchema,
runtimeFieldSchema,
serializedFieldFormatSchema,
} from '../../common/schemas';
import type { DataViewsServerPluginStartDependencies, DataViewsServerPluginStart } from '../types';
import {
SPECIFIC_DATA_VIEW_PATH,

View file

@ -8,6 +8,7 @@
import Boom from '@hapi/boom';
import type { RequestHandler, RouteMethod, RequestHandlerContext } from '@kbn/core/server';
import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/common';
import { ErrorIndexPatternNotFound } from '../../error';
interface ErrorResponseBody {
@ -49,7 +50,8 @@ export const handleErrors =
const is404 =
(error as ErrorIndexPatternNotFound).is404 ||
(error as Boom.Boom)?.output?.statusCode === 404;
(error as Boom.Boom)?.output?.statusCode === 404 ||
error instanceof SavedObjectNotFound;
if (is404) {
return response.notFound({

View file

@ -22,7 +22,7 @@ describe('SavedObjectsClientPublicToCommon', () => {
.fn()
.mockResolvedValue({ outcome: 'exactMatch', saved_object: mockedSavedObject });
const service = new SavedObjectsClientServerToCommon(soClient);
const result = await service.get('index-pattern', '1');
const result = await service.get('1');
expect(result).toStrictEqual(mockedSavedObject);
});
@ -34,7 +34,7 @@ describe('SavedObjectsClientPublicToCommon', () => {
.fn()
.mockResolvedValue({ outcome: 'aliasMatch', saved_object: mockedSavedObject });
const service = new SavedObjectsClientServerToCommon(soClient);
const result = await service.get('index-pattern', '1');
const result = await service.get('1');
expect(result).toStrictEqual(mockedSavedObject);
});
@ -48,8 +48,6 @@ describe('SavedObjectsClientPublicToCommon', () => {
.mockResolvedValue({ outcome: 'conflict', saved_object: mockedSavedObject });
const service = new SavedObjectsClientServerToCommon(soClient);
await expect(service.get('index-pattern', '1')).rejects.toThrow(
DataViewSavedObjectConflictError
);
await expect(service.get('1')).rejects.toThrow(DataViewSavedObjectConflictError);
});
});

View file

@ -14,30 +14,50 @@ import {
} from '../common/types';
import { DataViewSavedObjectConflictError } from '../common/errors';
import type { DataViewCrudTypes } from '../common/content_management';
import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../common';
export class SavedObjectsClientServerToCommon implements SavedObjectsClientCommon {
private savedObjectClient: SavedObjectsClientContract;
constructor(savedObjectClient: SavedObjectsClientContract) {
this.savedObjectClient = savedObjectClient;
}
async find<T = unknown>(options: SavedObjectsClientCommonFindArgs) {
const result = await this.savedObjectClient.find<T>(options);
const result = await this.savedObjectClient.find<T>({
...options,
type: DATA_VIEW_SAVED_OBJECT_TYPE,
});
return result.saved_objects;
}
async get<T = unknown>(type: string, id: string) {
const response = await this.savedObjectClient.resolve<T>(type, id);
async get<T = unknown>(id: string) {
const response = await this.savedObjectClient.resolve<T>('index-pattern', id);
if (response.outcome === 'conflict') {
throw new DataViewSavedObjectConflictError(id);
}
return response.saved_object;
}
async update(type: string, id: string, attributes: DataViewAttributes, options: {}) {
return (await this.savedObjectClient.update(type, id, attributes, options)) as SavedObject;
async getSavedSearch<T = unknown>(id: string) {
const response = await this.savedObjectClient.resolve<T>('search', id);
if (response.outcome === 'conflict') {
throw new DataViewSavedObjectConflictError(id);
}
return response.saved_object;
}
async create(type: string, attributes: DataViewAttributes, options: {}) {
return await this.savedObjectClient.create(type, attributes, options);
async update(id: string, attributes: DataViewAttributes, options: {}) {
return (await this.savedObjectClient.update(
DATA_VIEW_SAVED_OBJECT_TYPE,
id,
attributes,
options
)) as SavedObject;
}
delete(type: string, id: string) {
return this.savedObjectClient.delete(type, id, { force: true });
async create(attributes: DataViewAttributes, options: DataViewCrudTypes['CreateOptions']) {
return await this.savedObjectClient.create(DATA_VIEW_SAVED_OBJECT_TYPE, attributes, options);
}
async delete(id: string) {
await this.savedObjectClient.delete(DATA_VIEW_SAVED_OBJECT_TYPE, id, { force: true });
}
}

View file

@ -15,6 +15,7 @@ import {
import { ExpressionsServerSetup } from '@kbn/expressions-plugin/server';
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
import { FieldFormatsSetup, FieldFormatsStart } from '@kbn/field-formats-plugin/server';
import type { ContentManagementServerSetup } from '@kbn/content-management-plugin/server';
import { DataViewsService } from '../common';
/**
@ -72,6 +73,10 @@ export interface DataViewsServerPluginSetupDependencies {
* Usage collection
*/
usageCollection?: UsageCollectionSetup;
/**
* Content management
*/
contentManagement: ContentManagementServerSetup;
}
/**

View file

@ -16,7 +16,7 @@ export const getFieldByName = (
fieldName: string,
indexPattern: SavedObject<DataViewAttributes>
): FieldSpec | undefined => {
const fields: FieldSpec[] = indexPattern && JSON.parse(indexPattern.attributes.fields);
const fields: FieldSpec[] = indexPattern && JSON.parse(indexPattern.attributes?.fields || '[]');
const field = fields && fields.find((f) => f.name === fieldName);
return field;

View file

@ -28,6 +28,9 @@
"@kbn/utility-types-jest",
"@kbn/safer-lodash-set",
"@kbn/core-http-server",
"@kbn/content-management-plugin",
"@kbn/content-management-utils",
"@kbn/object-versioning",
"@kbn/core-saved-objects-server",
],
"exclude": [

View file

@ -33,6 +33,13 @@ export class MapsStorage extends SOContentStorage<MapCrudTypes> {
cmServicesDefinition,
searchArgsToSOFindOptions,
enableMSearch: true,
allowedSavedObjectAttributes: [
'title',
'description',
'mapStateJSON',
'layerListJSON',
'uiStateJSON',
],
});
}
}