Migrate visualization, annotation, graph to saved_object_content_storage (#168520)

## Summary

Close #167421

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Drew Tate 2023-10-13 20:57:26 -06:00 committed by GitHub
parent fe7afbbcb3
commit bab28dc95d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 142 additions and 969 deletions

View file

@ -129,7 +129,7 @@ export type UpdateArgsToSoUpdateOptions<Types extends CMCrudTypes> = (
params: Types['UpdateOptions']
) => SavedObjectsUpdateOptions<Types['Attributes']>;
export interface SOContentStorageConstrutorParams<Types extends CMCrudTypes> {
export interface SOContentStorageConstructorParams<Types extends CMCrudTypes> {
savedObjectType: string;
cmServicesDefinition: ServicesDefinitionSet;
// this is necessary since unexpected saved object attributes could cause schema validation to fail
@ -137,6 +137,12 @@ export interface SOContentStorageConstrutorParams<Types extends CMCrudTypes> {
createArgsToSoCreateOptions?: CreateArgsToSoCreateOptions<Types>;
updateArgsToSoUpdateOptions?: UpdateArgsToSoUpdateOptions<Types>;
searchArgsToSOFindOptions?: SearchArgsToSOFindOptions<Types>;
/**
* MSearch is a feature that allows searching across multiple content types
* (for example, could be used in a general content finder or the like)
*
* defaults to false
*/
enableMSearch?: boolean;
mSearchAdditionalSearchFields?: string[];
@ -163,7 +169,7 @@ export abstract class SOContentStorage<Types extends CMCrudTypes>
mSearchAdditionalSearchFields,
logger,
throwOnResultValidationError,
}: SOContentStorageConstrutorParams<Types>) {
}: SOContentStorageConstructorParams<Types>) {
this.logger = logger;
this.throwOnResultValidationError = throwOnResultValidationError ?? false;
this.savedObjectType = savedObjectType;
@ -219,8 +225,8 @@ export abstract class SOContentStorage<Types extends CMCrudTypes>
private throwOnResultValidationError: boolean;
private logger: Logger;
private savedObjectType: SOContentStorageConstrutorParams<Types>['savedObjectType'];
private cmServicesDefinition: SOContentStorageConstrutorParams<Types>['cmServicesDefinition'];
private savedObjectType: SOContentStorageConstructorParams<Types>['savedObjectType'];
private cmServicesDefinition: SOContentStorageConstructorParams<Types>['cmServicesDefinition'];
private createArgsToSoCreateOptions: CreateArgsToSoCreateOptions<Types>;
private updateArgsToSoUpdateOptions: UpdateArgsToSoUpdateOptions<Types>;
private searchArgsToSOFindOptions: SearchArgsToSOFindOptions<Types>;

View file

@ -27,6 +27,7 @@ export type {
EventAnnotationGroupSearchIn,
EventAnnotationGroupSearchOut,
EventAnnotationGroupSearchQuery,
EventAnnotationGroupCrudTypes,
} from './latest';
export * as EventAnnotationGroupV1 from './v1';

View file

@ -23,5 +23,6 @@ export type {
EventAnnotationGroupSearchIn,
EventAnnotationGroupSearchOut,
EventAnnotationGroupSearchQuery,
EventAnnotationGroupCrudTypes,
Reference,
} from './types';

View file

@ -18,6 +18,7 @@ import {
CreateResult,
UpdateResult,
} from '@kbn/content-management-plugin/common';
import { ContentManagementCrudTypes } from '@kbn/content-management-utils';
import type { DataViewSpec } from '@kbn/data-views-plugin/common';
import type { EventAnnotationConfig } from '@kbn/event-annotation-common';
@ -125,3 +126,13 @@ export type EventAnnotationGroupSearchIn = SearchIn<
>;
export type EventAnnotationGroupSearchOut = SearchResult<EventAnnotationGroupSavedObject>;
// ----------- CRUD TYPES --------------
export type EventAnnotationGroupCrudTypes = ContentManagementCrudTypes<
EventAnnotationGroupContentType,
EventAnnotationGroupSavedObjectAttributes,
CreateOptions,
UpdateOptions,
{}
>;

View file

@ -37,6 +37,7 @@ export type {
EventAnnotationGroupSearchOut,
EventAnnotationGroupDeleteIn,
EventAnnotationGroupDeleteOut,
EventAnnotationGroupCrudTypes,
} from './content_management';
export { CONTENT_ID } from './content_management';
export { ANNOTATIONS_LISTING_VIEW_ID } from './constants';

View file

@ -5,336 +5,34 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import Boom from '@hapi/boom';
import type { SearchQuery } from '@kbn/content-management-plugin/common';
import type { ContentStorage, StorageContext } from '@kbn/content-management-plugin/server';
import type {
SavedObject,
SavedObjectReference,
SavedObjectsFindOptions,
} from '@kbn/core-saved-objects-api-server';
import { getMSearch, type GetMSearchType } from '@kbn/content-management-utils';
import { SOContentStorage } from '@kbn/content-management-utils';
import { EVENT_ANNOTATION_GROUP_TYPE } from '@kbn/event-annotation-common';
import { Logger } from '@kbn/logging';
import { cmServicesDefinition } from '../../common/content_management/cm_services';
import type {
EventAnnotationGroupSavedObjectAttributes,
EventAnnotationGroupSavedObject,
PartialEventAnnotationGroupSavedObject,
EventAnnotationGroupGetOut,
EventAnnotationGroupCreateIn,
EventAnnotationGroupCreateOut,
CreateOptions,
EventAnnotationGroupUpdateIn,
EventAnnotationGroupUpdateOut,
UpdateOptions,
EventAnnotationGroupDeleteOut,
EventAnnotationGroupSearchQuery,
EventAnnotationGroupSearchOut,
} from '../../common/content_management';
import type { EventAnnotationGroupCrudTypes } from '../../common/content_management';
const savedObjectClientFromRequest = async (ctx: StorageContext) => {
if (!ctx.requestHandlerContext) {
throw new Error('Storage context.requestHandlerContext missing.');
}
const { savedObjects } = await ctx.requestHandlerContext.core;
return savedObjects.client;
};
type PartialSavedObject<T> = Omit<SavedObject<Partial<T>>, 'references'> & {
references: SavedObjectReference[] | undefined;
};
function savedObjectToEventAnnotationGroupSavedObject(
savedObject: SavedObject<EventAnnotationGroupSavedObjectAttributes>,
partial: false
): EventAnnotationGroupSavedObject;
function savedObjectToEventAnnotationGroupSavedObject(
savedObject: PartialSavedObject<EventAnnotationGroupSavedObjectAttributes>,
partial: true
): PartialEventAnnotationGroupSavedObject;
function savedObjectToEventAnnotationGroupSavedObject(
savedObject:
| SavedObject<EventAnnotationGroupSavedObjectAttributes>
| PartialSavedObject<EventAnnotationGroupSavedObjectAttributes>
): EventAnnotationGroupSavedObject | PartialEventAnnotationGroupSavedObject {
const {
id,
type,
updated_at: updatedAt,
created_at: createdAt,
attributes: { title, description, annotations, ignoreGlobalFilters, dataViewSpec },
references,
error,
namespaces,
} = savedObject;
return {
id,
type,
updatedAt,
createdAt,
attributes: {
title,
description,
annotations,
ignoreGlobalFilters,
dataViewSpec,
},
references,
error,
namespaces,
};
}
const SO_TYPE = EVENT_ANNOTATION_GROUP_TYPE;
export class EventAnnotationGroupStorage
implements
ContentStorage<EventAnnotationGroupSavedObject, PartialEventAnnotationGroupSavedObject>
{
mSearch: GetMSearchType<EventAnnotationGroupSavedObject>;
constructor() {
this.mSearch = getMSearch<EventAnnotationGroupSavedObject, EventAnnotationGroupSearchOut>({
savedObjectType: SO_TYPE,
export class EventAnnotationGroupStorage extends SOContentStorage<EventAnnotationGroupCrudTypes> {
constructor({
logger,
throwOnResultValidationError,
}: {
logger: Logger;
throwOnResultValidationError: boolean;
}) {
super({
savedObjectType: EVENT_ANNOTATION_GROUP_TYPE,
cmServicesDefinition,
enableMSearch: true,
allowedSavedObjectAttributes: [
'title',
'description',
'ignoreGlobalFilters',
'annotations',
'ignoreGlobalFilters',
'dataViewSpec',
],
logger,
throwOnResultValidationError,
});
}
async get(ctx: StorageContext, id: string): Promise<EventAnnotationGroupGetOut> {
const {
utils: { getTransforms },
version: { request: requestVersion },
} = ctx;
const transforms = getTransforms(cmServicesDefinition, requestVersion);
const soClient = await savedObjectClientFromRequest(ctx);
const {
saved_object: savedObject,
alias_purpose: aliasPurpose,
alias_target_id: aliasTargetId,
outcome,
} = await soClient.resolve<EventAnnotationGroupSavedObjectAttributes>(SO_TYPE, id);
const response: EventAnnotationGroupGetOut = {
item: savedObjectToEventAnnotationGroupSavedObject(savedObject, false),
meta: {
aliasPurpose,
aliasTargetId,
outcome,
},
};
// Validate DB response and DOWN transform to the request version
const { value, error: resultError } = transforms.get.out.result.down<
EventAnnotationGroupGetOut,
EventAnnotationGroupGetOut
>(response);
if (resultError) {
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
}
return value;
}
async bulkGet(): Promise<never> {
// Not implemented. EventAnnotationGroup does not use bulkGet
throw new Error(`[bulkGet] has not been implemented. See EventAnnotationGroupStorage class.`);
}
async create(
ctx: StorageContext,
data: EventAnnotationGroupCreateIn['data'],
options: CreateOptions
): Promise<EventAnnotationGroupCreateOut> {
const {
utils: { getTransforms },
version: { request: requestVersion },
} = ctx;
const transforms = getTransforms(cmServicesDefinition, requestVersion);
// Validate input (data & options) & UP transform them to the latest version
const { value: dataToLatest, error: dataError } = transforms.create.in.data.up<
EventAnnotationGroupSavedObjectAttributes,
EventAnnotationGroupSavedObjectAttributes
>(data);
if (dataError) {
throw Boom.badRequest(`Invalid data. ${dataError.message}`);
}
const { value: optionsToLatest, error: optionsError } = transforms.create.in.options.up<
CreateOptions,
CreateOptions
>(options);
if (optionsError) {
throw Boom.badRequest(`Invalid options. ${optionsError.message}`);
}
// Save data in DB
const soClient = await savedObjectClientFromRequest(ctx);
const savedObject = await soClient.create<EventAnnotationGroupSavedObjectAttributes>(
SO_TYPE,
dataToLatest,
optionsToLatest
);
// Validate DB response and DOWN transform to the request version
const { value, error: resultError } = transforms.create.out.result.down<
EventAnnotationGroupCreateOut,
EventAnnotationGroupCreateOut
>({
item: savedObjectToEventAnnotationGroupSavedObject(savedObject, false),
});
if (resultError) {
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
}
return value;
}
async update(
ctx: StorageContext,
id: string,
data: EventAnnotationGroupUpdateIn['data'],
options: UpdateOptions
): Promise<EventAnnotationGroupUpdateOut> {
const {
utils: { getTransforms },
version: { request: requestVersion },
} = ctx;
const transforms = getTransforms(cmServicesDefinition, requestVersion);
// Validate input (data & options) & UP transform them to the latest version
const { value: dataToLatest, error: dataError } = transforms.update.in.data.up<
EventAnnotationGroupSavedObjectAttributes,
EventAnnotationGroupSavedObjectAttributes
>(data);
if (dataError) {
throw Boom.badRequest(`Invalid data. ${dataError.message}`);
}
const { value: optionsToLatest, error: optionsError } = transforms.update.in.options.up<
CreateOptions,
CreateOptions
>(options);
if (optionsError) {
throw Boom.badRequest(`Invalid options. ${optionsError.message}`);
}
// Save data in DB
const soClient = await savedObjectClientFromRequest(ctx);
const partialSavedObject = await soClient.update<EventAnnotationGroupSavedObjectAttributes>(
SO_TYPE,
id,
dataToLatest,
optionsToLatest
);
// Validate DB response and DOWN transform to the request version
const { value, error: resultError } = transforms.update.out.result.down<
EventAnnotationGroupUpdateOut,
EventAnnotationGroupUpdateOut
>({
item: savedObjectToEventAnnotationGroupSavedObject(partialSavedObject, true),
});
if (resultError) {
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
}
return value;
}
async delete(ctx: StorageContext, id: string): Promise<EventAnnotationGroupDeleteOut> {
const soClient = await savedObjectClientFromRequest(ctx);
await soClient.delete(SO_TYPE, id);
return { success: true };
}
async search(
ctx: StorageContext,
query: SearchQuery,
options: EventAnnotationGroupSearchQuery = {}
): Promise<EventAnnotationGroupSearchOut> {
const {
utils: { getTransforms },
version: { request: requestVersion },
} = ctx;
const transforms = getTransforms(cmServicesDefinition, requestVersion);
const soClient = await savedObjectClientFromRequest(ctx);
// Validate and UP transform the options
const { value: optionsToLatest, error: optionsError } = transforms.search.in.options.up<
EventAnnotationGroupSearchQuery,
EventAnnotationGroupSearchQuery
>(options);
if (optionsError) {
throw Boom.badRequest(`Invalid payload. ${optionsError.message}`);
}
const { searchFields = ['title^3', 'description'], types = [SO_TYPE] } = optionsToLatest;
const { included, excluded } = query.tags ?? {};
const hasReference: SavedObjectsFindOptions['hasReference'] = included
? included.map((id) => ({
id,
type: 'tag',
}))
: undefined;
const hasNoReference: SavedObjectsFindOptions['hasNoReference'] = excluded
? excluded.map((id) => ({
id,
type: 'tag',
}))
: undefined;
const soQuery: SavedObjectsFindOptions = {
type: types,
search: query.text,
perPage: query.limit,
page: query.cursor ? Number(query.cursor) : undefined,
defaultSearchOperator: 'AND',
searchFields,
hasReference,
hasNoReference,
};
// Execute the query in the DB
const response = await soClient.find<EventAnnotationGroupSavedObjectAttributes>(soQuery);
// Validate the response and DOWN transform to the request version
const { value, error: resultError } = transforms.search.out.result.down<
EventAnnotationGroupSearchOut,
EventAnnotationGroupSearchOut
>({
hits: response.saved_objects.map((so) =>
savedObjectToEventAnnotationGroupSavedObject(so, false)
),
pagination: {
total: response.total,
},
});
if (resultError) {
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
}
return value;
}
}

View file

@ -6,5 +6,8 @@
* Side Public License, v 1.
*/
import { PluginInitializerContext } from '@kbn/core-plugins-server';
import { EventAnnotationServerPlugin } from './plugin';
export const plugin = () => new EventAnnotationServerPlugin();
export const plugin = (initializerContext: PluginInitializerContext) =>
new EventAnnotationServerPlugin(initializerContext);

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { CoreSetup, Plugin } from '@kbn/core/server';
import { CoreSetup, Plugin, PluginInitializerContext } from '@kbn/core/server';
import { ExpressionsServerSetup } from '@kbn/expressions-plugin/server';
import { PluginStart as DataPluginStart } from '@kbn/data-plugin/server';
import { ContentManagementServerSetup } from '@kbn/content-management-plugin/server';
@ -29,6 +29,8 @@ export interface EventAnnotationStartDependencies {
}
export class EventAnnotationServerPlugin implements Plugin<object, object> {
constructor(private readonly initializerContext: PluginInitializerContext) {}
public setup(
core: CoreSetup<EventAnnotationStartDependencies, object>,
dependencies: SetupDependencies
@ -42,7 +44,10 @@ export class EventAnnotationServerPlugin implements Plugin<object, object> {
dependencies.contentManagement.register({
id: CONTENT_ID,
storage: new EventAnnotationGroupStorage(),
storage: new EventAnnotationGroupStorage({
throwOnResultValidationError: this.initializerContext.env.mode.dev,
logger: this.initializerContext.logger.get(),
}),
version: {
latest: LATEST_VERSION,
},

View file

@ -31,10 +31,11 @@
"@kbn/object-versioning",
"@kbn/config-schema",
"@kbn/content-management-plugin",
"@kbn/core-saved-objects-api-server",
"@kbn/event-annotation-components",
"@kbn/event-annotation-common",
"@kbn/content-management-utils"
"@kbn/content-management-utils",
"@kbn/logging",
"@kbn/core-plugins-server"
],
"exclude": [
"target/**/*",

View file

@ -27,6 +27,7 @@ export type {
VisualizationSearchIn,
VisualizationSearchOut,
VisualizationSearchQuery,
VisualizationCrudTypes,
} from './latest';
export * as VisualizationV1 from './v1';

View file

@ -23,5 +23,6 @@ export type {
VisualizationSearchIn,
VisualizationSearchOut,
VisualizationSearchQuery,
VisualizationCrudTypes,
Reference,
} from './types';

View file

@ -18,6 +18,7 @@ import {
CreateResult,
UpdateResult,
} from '@kbn/content-management-plugin/common';
import { ContentManagementCrudTypes } from '@kbn/content-management-utils';
import { VisualizationContentType } from '../types';
@ -127,3 +128,13 @@ export interface VisualizationSearchQuery {
export type VisualizationSearchIn = SearchIn<VisualizationContentType, {}>;
export type VisualizationSearchOut = SearchResult<VisualizationSavedObject>;
// ----------- CRUD TYPES --------------
export type VisualizationCrudTypes = ContentManagementCrudTypes<
VisualizationContentType,
VisualizationSavedObjectAttributes,
CreateOptions,
UpdateOptions,
{}
>;

View file

@ -5,343 +5,41 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import Boom from '@hapi/boom';
import type { SearchQuery } from '@kbn/content-management-plugin/common';
import type { ContentStorage, StorageContext } from '@kbn/content-management-plugin/server';
import type {
SavedObject,
SavedObjectReference,
SavedObjectsFindOptions,
} from '@kbn/core-saved-objects-api-server';
import { getMSearch, type GetMSearchType } from '@kbn/content-management-utils';
import { SOContentStorage } from '@kbn/content-management-utils';
import { CONTENT_ID } from '../../common/content_management';
import { Logger } from '@kbn/logging';
import { cmServicesDefinition } from '../../common/content_management/cm_services';
import type {
VisualizationSavedObjectAttributes,
VisualizationSavedObject,
PartialVisualizationSavedObject,
VisualizationContentType,
VisualizationGetOut,
VisualizationCreateIn,
VisualizationCreateOut,
CreateOptions,
VisualizationUpdateIn,
VisualizationUpdateOut,
UpdateOptions,
VisualizationDeleteOut,
VisualizationSearchQuery,
VisualizationSearchOut,
VisualizationCrudTypes,
} from '../../common/content_management';
const savedObjectClientFromRequest = async (ctx: StorageContext) => {
if (!ctx.requestHandlerContext) {
throw new Error('Storage context.requestHandlerContext missing.');
}
const { savedObjects } = await ctx.requestHandlerContext.core;
return savedObjects.client;
};
type PartialSavedObject<T> = Omit<SavedObject<Partial<T>>, 'references'> & {
references: SavedObjectReference[] | undefined;
};
function savedObjectToVisualizationSavedObject(
savedObject: SavedObject<VisualizationSavedObjectAttributes>,
partial: false
): VisualizationSavedObject;
function savedObjectToVisualizationSavedObject(
savedObject: PartialSavedObject<VisualizationSavedObjectAttributes>,
partial: true
): PartialVisualizationSavedObject;
function savedObjectToVisualizationSavedObject(
savedObject:
| SavedObject<VisualizationSavedObjectAttributes>
| PartialSavedObject<VisualizationSavedObjectAttributes>
): VisualizationSavedObject | PartialVisualizationSavedObject {
const {
id,
type,
updated_at: updatedAt,
created_at: createdAt,
attributes: {
title,
description,
visState,
kibanaSavedObjectMeta,
uiStateJSON,
savedSearchRefName,
},
references,
error,
namespaces,
} = savedObject;
return {
id,
type,
updatedAt,
createdAt,
attributes: {
title,
description,
visState,
kibanaSavedObjectMeta,
uiStateJSON,
savedSearchRefName,
},
references,
error,
namespaces,
};
}
const SO_TYPE: VisualizationContentType = 'visualization';
export class VisualizationsStorage
implements ContentStorage<VisualizationSavedObject, PartialVisualizationSavedObject>
{
mSearch: GetMSearchType<VisualizationSavedObject>;
constructor() {
this.mSearch = getMSearch<VisualizationSavedObject, VisualizationSearchOut>({
export class VisualizationsStorage extends SOContentStorage<VisualizationCrudTypes> {
constructor({
logger,
throwOnResultValidationError,
}: {
logger: Logger;
throwOnResultValidationError: boolean;
}) {
super({
savedObjectType: SO_TYPE,
cmServicesDefinition,
enableMSearch: true,
allowedSavedObjectAttributes: [
'title',
'description',
'version',
'visState',
'kibanaSavedObjectMeta',
'uiStateJSON',
'visState',
'savedSearchRefName',
],
logger,
throwOnResultValidationError,
});
}
async get(ctx: StorageContext, id: string): Promise<VisualizationGetOut> {
const {
utils: { getTransforms },
version: { request: requestVersion },
} = ctx;
const transforms = getTransforms(cmServicesDefinition, requestVersion);
const soClient = await savedObjectClientFromRequest(ctx);
// Save data in DB
const {
saved_object: savedObject,
alias_purpose: aliasPurpose,
alias_target_id: aliasTargetId,
outcome,
} = await soClient.resolve<VisualizationSavedObjectAttributes>(SO_TYPE, id);
const response: VisualizationGetOut = {
item: savedObjectToVisualizationSavedObject(savedObject, false),
meta: {
aliasPurpose,
aliasTargetId,
outcome,
},
};
// Validate DB response and DOWN transform to the request version
const { value, error: resultError } = transforms.get.out.result.down<
VisualizationGetOut,
VisualizationGetOut
>(response);
if (resultError) {
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
}
return value;
}
async bulkGet(): Promise<never> {
// Not implemented. Visualizations does not use bulkGet
throw new Error(`[bulkGet] has not been implemented. See VisualizationsStorage class.`);
}
async create(
ctx: StorageContext,
data: VisualizationCreateIn['data'],
options: CreateOptions
): Promise<VisualizationCreateOut> {
const {
utils: { getTransforms },
version: { request: requestVersion },
} = ctx;
const transforms = getTransforms(cmServicesDefinition, requestVersion);
// Validate input (data & options) & UP transform them to the latest version
const { value: dataToLatest, error: dataError } = transforms.create.in.data.up<
VisualizationSavedObjectAttributes,
VisualizationSavedObjectAttributes
>(data);
if (dataError) {
throw Boom.badRequest(`Invalid data. ${dataError.message}`);
}
const { value: optionsToLatest, error: optionsError } = transforms.create.in.options.up<
CreateOptions,
CreateOptions
>(options);
if (optionsError) {
throw Boom.badRequest(`Invalid options. ${optionsError.message}`);
}
// Save data in DB
const soClient = await savedObjectClientFromRequest(ctx);
const savedObject = await soClient.create<VisualizationSavedObjectAttributes>(
SO_TYPE,
dataToLatest,
optionsToLatest
);
// Validate DB response and DOWN transform to the request version
const { value, error: resultError } = transforms.create.out.result.down<
VisualizationCreateOut,
VisualizationCreateOut
>({
item: savedObjectToVisualizationSavedObject(savedObject, false),
});
if (resultError) {
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
}
return value;
}
async update(
ctx: StorageContext,
id: string,
data: VisualizationUpdateIn['data'],
options: UpdateOptions
): Promise<VisualizationUpdateOut> {
const {
utils: { getTransforms },
version: { request: requestVersion },
} = ctx;
const transforms = getTransforms(cmServicesDefinition, requestVersion);
// Validate input (data & options) & UP transform them to the latest version
const { value: dataToLatest, error: dataError } = transforms.update.in.data.up<
VisualizationSavedObjectAttributes,
VisualizationSavedObjectAttributes
>(data);
if (dataError) {
throw Boom.badRequest(`Invalid data. ${dataError.message}`);
}
const { value: optionsToLatest, error: optionsError } = transforms.update.in.options.up<
CreateOptions,
CreateOptions
>(options);
if (optionsError) {
throw Boom.badRequest(`Invalid options. ${optionsError.message}`);
}
// Save data in DB
const soClient = await savedObjectClientFromRequest(ctx);
const partialSavedObject = await soClient.update<VisualizationSavedObjectAttributes>(
SO_TYPE,
id,
dataToLatest,
optionsToLatest
);
// Validate DB response and DOWN transform to the request version
const { value, error: resultError } = transforms.update.out.result.down<
VisualizationUpdateOut,
VisualizationUpdateOut
>({
item: savedObjectToVisualizationSavedObject(partialSavedObject, true),
});
if (resultError) {
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
}
return value;
}
async delete(ctx: StorageContext, id: string): Promise<VisualizationDeleteOut> {
const soClient = await savedObjectClientFromRequest(ctx);
await soClient.delete(SO_TYPE, id);
return { success: true };
}
async search(
ctx: StorageContext,
query: SearchQuery,
options: VisualizationSearchQuery = {}
): Promise<VisualizationSearchOut> {
const {
utils: { getTransforms },
version: { request: requestVersion },
} = ctx;
const transforms = getTransforms(cmServicesDefinition, requestVersion);
const soClient = await savedObjectClientFromRequest(ctx);
// Validate and UP transform the options
const { value: optionsToLatest, error: optionsError } = transforms.search.in.options.up<
VisualizationSearchQuery,
VisualizationSearchQuery
>(options);
if (optionsError) {
throw Boom.badRequest(`Invalid payload. ${optionsError.message}`);
}
const { searchFields = ['title^3', 'description'], types = [CONTENT_ID] } = optionsToLatest;
const { included, excluded } = query.tags ?? {};
const hasReference: SavedObjectsFindOptions['hasReference'] = included
? included.map((id) => ({
id,
type: 'tag',
}))
: undefined;
const hasNoReference: SavedObjectsFindOptions['hasNoReference'] = excluded
? excluded.map((id) => ({
id,
type: 'tag',
}))
: undefined;
const soQuery: SavedObjectsFindOptions = {
type: types,
search: query.text,
perPage: query.limit,
page: query.cursor ? +query.cursor : undefined,
defaultSearchOperator: 'AND',
searchFields,
hasReference,
hasNoReference,
};
// Execute the query in the DB
const response = await soClient.find<VisualizationSavedObjectAttributes>(soQuery);
// Validate the response and DOWN transform to the request version
const { value, error: resultError } = transforms.search.out.result.down<
VisualizationSearchOut,
VisualizationSearchOut
>({
hits: response.saved_objects.map((so) => savedObjectToVisualizationSavedObject(so, false)),
pagination: {
total: response.total,
},
});
if (resultError) {
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
}
return value;
}
}

View file

@ -29,7 +29,7 @@ export class VisualizationsPlugin
{
private readonly logger: Logger;
constructor(initializerContext: PluginInitializerContext) {
constructor(private readonly initializerContext: PluginInitializerContext) {
this.logger = initializerContext.logger.get();
}
@ -55,7 +55,10 @@ export class VisualizationsPlugin
plugins.contentManagement.register({
id: CONTENT_ID,
storage: new VisualizationsStorage(),
storage: new VisualizationsStorage({
logger: this.logger,
throwOnResultValidationError: this.initializerContext.env.mode.dev,
}),
version: {
latest: LATEST_VERSION,
},

View file

@ -54,7 +54,6 @@
"@kbn/saved-objects-management-plugin",
"@kbn/saved-objects-finder-plugin",
"@kbn/content-management-plugin",
"@kbn/core-saved-objects-api-server",
"@kbn/object-versioning",
"@kbn/core-saved-objects-server",
"@kbn/core-saved-objects-utils-server",
@ -64,7 +63,8 @@
"@kbn/content-management-utils",
"@kbn/serverless",
"@kbn/no-data-page-plugin",
"@kbn/search-response-warnings"
"@kbn/search-response-warnings",
"@kbn/logging"
],
"exclude": [
"target/**/*",

View file

@ -26,6 +26,7 @@ export type {
GraphSearchIn,
GraphSearchOut,
GraphSearchQuery,
GraphCrudTypes,
} from './latest';
export * as GraphV1 from './v1';

View file

@ -22,5 +22,6 @@ export type {
GraphSearchIn,
GraphSearchOut,
GraphSearchQuery,
GraphCrudTypes,
Reference,
} from './types';

View file

@ -17,6 +17,7 @@ import {
CreateResult,
UpdateResult,
} from '@kbn/content-management-plugin/common';
import { ContentManagementCrudTypes } from '@kbn/content-management-utils';
import { GraphContentType } from '../types';
@ -113,3 +114,13 @@ export interface GraphSearchQuery {
export type GraphSearchIn = SearchIn<GraphContentType, {}>;
export type GraphSearchOut = SearchResult<GraphSavedObject>;
// ----------- CRUD TYPES --------------
export type GraphCrudTypes = ContentManagementCrudTypes<
GraphContentType,
GraphSavedObjectAttributes,
CreateOptions,
UpdateOptions,
{}
>;

View file

@ -4,325 +4,37 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import Boom from '@hapi/boom';
import type { SearchQuery } from '@kbn/content-management-plugin/common';
import type { ContentStorage, StorageContext } from '@kbn/content-management-plugin/server';
import type {
SavedObject,
SavedObjectReference,
SavedObjectsFindOptions,
} from '@kbn/core-saved-objects-api-server';
import { Logger } from '@kbn/logging';
import { SOContentStorage } from '@kbn/content-management-utils';
import { cmServicesDefinition } from '../../common/content_management/cm_services';
import type {
GraphSavedObjectAttributes,
GraphSavedObject,
PartialGraphSavedObject,
GraphGetOut,
GraphCreateIn,
GraphCreateOut,
CreateOptions,
GraphUpdateIn,
GraphUpdateOut,
UpdateOptions,
GraphDeleteOut,
GraphSearchQuery,
GraphSearchOut,
} from '../../common/content_management';
const savedObjectClientFromRequest = async (ctx: StorageContext) => {
if (!ctx.requestHandlerContext) {
throw new Error('Storage context.requestHandlerContext missing.');
}
const { savedObjects } = await ctx.requestHandlerContext.core;
return savedObjects.client;
};
type PartialSavedObject<T> = Omit<SavedObject<Partial<T>>, 'references'> & {
references: SavedObjectReference[] | undefined;
};
function savedObjectToGraphSavedObject(
savedObject: SavedObject<GraphSavedObjectAttributes>,
partial: false
): GraphSavedObject;
function savedObjectToGraphSavedObject(
savedObject: PartialSavedObject<GraphSavedObjectAttributes>,
partial: true
): PartialGraphSavedObject;
function savedObjectToGraphSavedObject(
savedObject:
| SavedObject<GraphSavedObjectAttributes>
| PartialSavedObject<GraphSavedObjectAttributes>
): GraphSavedObject | PartialGraphSavedObject {
const {
id,
type,
updated_at: updatedAt,
created_at: createdAt,
attributes: {
title,
description,
version,
kibanaSavedObjectMeta,
wsState,
numVertices,
numLinks,
legacyIndexPatternRef,
},
references,
error,
namespaces,
} = savedObject;
return {
id,
type,
updatedAt,
createdAt,
attributes: {
title,
description,
kibanaSavedObjectMeta,
wsState,
version,
numLinks,
numVertices,
legacyIndexPatternRef,
},
references,
error,
namespaces,
};
}
import type { GraphCrudTypes } from '../../common/content_management';
const SO_TYPE = 'graph-workspace';
export class GraphStorage implements ContentStorage<GraphSavedObject, PartialGraphSavedObject> {
constructor() {}
async get(ctx: StorageContext, id: string): Promise<GraphGetOut> {
const {
utils: { getTransforms },
version: { request: requestVersion },
} = ctx;
const transforms = getTransforms(cmServicesDefinition, requestVersion);
const soClient = await savedObjectClientFromRequest(ctx);
// Save data in DB
const {
saved_object: savedObject,
alias_purpose: aliasPurpose,
alias_target_id: aliasTargetId,
outcome,
} = await soClient.resolve<GraphSavedObjectAttributes>(SO_TYPE, id);
const response: GraphGetOut = {
item: savedObjectToGraphSavedObject(savedObject, false),
meta: {
aliasPurpose,
aliasTargetId,
outcome,
},
};
// Validate DB response and DOWN transform to the request version
const { value, error: resultError } = transforms.get.out.result.down<GraphGetOut, GraphGetOut>(
response
);
if (resultError) {
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
}
return value;
}
async bulkGet(): Promise<never> {
// Not implemented. Graph does not use bulkGet
throw new Error(`[bulkGet] has not been implemented. See GraphStorage class.`);
}
async create(
ctx: StorageContext,
data: GraphCreateIn['data'],
options: CreateOptions
): Promise<GraphCreateOut> {
const {
utils: { getTransforms },
version: { request: requestVersion },
} = ctx;
const transforms = getTransforms(cmServicesDefinition, requestVersion);
// Validate input (data & options) & UP transform them to the latest version
const { value: dataToLatest, error: dataError } = transforms.create.in.data.up<
GraphSavedObjectAttributes,
GraphSavedObjectAttributes
>(data);
if (dataError) {
throw Boom.badRequest(`Invalid data. ${dataError.message}`);
}
const { value: optionsToLatest, error: optionsError } = transforms.create.in.options.up<
CreateOptions,
CreateOptions
>(options);
if (optionsError) {
throw Boom.badRequest(`Invalid options. ${optionsError.message}`);
}
// Save data in DB
const soClient = await savedObjectClientFromRequest(ctx);
const savedObject = await soClient.create<GraphSavedObjectAttributes>(
SO_TYPE,
dataToLatest,
optionsToLatest
);
// Validate DB response and DOWN transform to the request version
const { value, error: resultError } = transforms.create.out.result.down<
GraphCreateOut,
GraphCreateOut
>({
item: savedObjectToGraphSavedObject(savedObject, false),
export class GraphStorage extends SOContentStorage<GraphCrudTypes> {
constructor({
logger,
throwOnResultValidationError,
}: {
logger: Logger;
throwOnResultValidationError: boolean;
}) {
super({
savedObjectType: SO_TYPE,
cmServicesDefinition,
allowedSavedObjectAttributes: [
'title',
'description',
'kibanaSavedObjectMeta',
'wsState',
'version',
'numLinks',
'numVertices',
'legacyIndexPatternRef',
],
logger,
throwOnResultValidationError,
});
if (resultError) {
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
}
return value;
}
async update(
ctx: StorageContext,
id: string,
data: GraphUpdateIn['data'],
options: UpdateOptions
): Promise<GraphUpdateOut> {
const {
utils: { getTransforms },
version: { request: requestVersion },
} = ctx;
const transforms = getTransforms(cmServicesDefinition, requestVersion);
// Validate input (data & options) & UP transform them to the latest version
const { value: dataToLatest, error: dataError } = transforms.update.in.data.up<
GraphSavedObjectAttributes,
GraphSavedObjectAttributes
>(data);
if (dataError) {
throw Boom.badRequest(`Invalid data. ${dataError.message}`);
}
const { value: optionsToLatest, error: optionsError } = transforms.update.in.options.up<
CreateOptions,
CreateOptions
>(options);
if (optionsError) {
throw Boom.badRequest(`Invalid options. ${optionsError.message}`);
}
// Save data in DB
const soClient = await savedObjectClientFromRequest(ctx);
const partialSavedObject = await soClient.update<GraphSavedObjectAttributes>(
SO_TYPE,
id,
dataToLatest,
optionsToLatest
);
// Validate DB response and DOWN transform to the request version
const { value, error: resultError } = transforms.update.out.result.down<
GraphUpdateOut,
GraphUpdateOut
>({
item: savedObjectToGraphSavedObject(partialSavedObject, true),
});
if (resultError) {
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
}
return value;
}
async delete(ctx: StorageContext, id: string): Promise<GraphDeleteOut> {
const soClient = await savedObjectClientFromRequest(ctx);
await soClient.delete(SO_TYPE, id);
return { success: true };
}
async search(
ctx: StorageContext,
query: SearchQuery,
options: GraphSearchQuery = {}
): Promise<GraphSearchOut> {
const {
utils: { getTransforms },
version: { request: requestVersion },
} = ctx;
const transforms = getTransforms(cmServicesDefinition, requestVersion);
const soClient = await savedObjectClientFromRequest(ctx);
// Validate and UP transform the options
const { value: optionsToLatest, error: optionsError } = transforms.search.in.options.up<
GraphSearchQuery,
GraphSearchQuery
>(options);
if (optionsError) {
throw Boom.badRequest(`Invalid payload. ${optionsError.message}`);
}
const { searchFields = ['title^3', 'description'], types = ['graph-workspace'] } =
optionsToLatest;
const { included, excluded } = query.tags ?? {};
const hasReference: SavedObjectsFindOptions['hasReference'] = included
? included.map((id) => ({
id,
type: 'tag',
}))
: undefined;
const hasNoReference: SavedObjectsFindOptions['hasNoReference'] = excluded
? excluded.map((id) => ({
id,
type: 'tag',
}))
: undefined;
const soQuery: SavedObjectsFindOptions = {
type: types,
search: query.text,
perPage: query.limit,
page: query.cursor ? Number(query.cursor) : undefined,
defaultSearchOperator: 'AND',
searchFields,
hasReference,
hasNoReference,
};
// Execute the query in the DB
const response = await soClient.find<GraphSavedObjectAttributes>(soQuery);
// Validate the response and DOWN transform to the request version
const { value, error: resultError } = transforms.search.out.result.down<
GraphSearchOut,
GraphSearchOut
>({
hits: response.saved_objects.map((so) => savedObjectToGraphSavedObject(so, false)),
pagination: {
total: response.total,
},
});
if (resultError) {
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
}
return value;
}
}

View file

@ -5,12 +5,13 @@
* 2.0.
*/
import { PluginConfigDescriptor } from '@kbn/core/server';
import { PluginConfigDescriptor, PluginInitializerContext } from '@kbn/core/server';
import { configSchema, ConfigSchema } from '../config';
import { GraphPlugin } from './plugin';
export const plugin = () => new GraphPlugin();
export const plugin = (initializerContext: PluginInitializerContext) =>
new GraphPlugin(initializerContext);
export const config: PluginConfigDescriptor<ConfigSchema> = {
exposeToBrowser: {

View file

@ -6,7 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
import { Plugin, CoreSetup, CoreStart } from '@kbn/core/server';
import { Plugin, CoreSetup, CoreStart, PluginInitializerContext } from '@kbn/core/server';
import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server';
import { LicensingPluginSetup, LicensingPluginStart } from '@kbn/licensing-plugin/server';
import { HomeServerPluginSetup } from '@kbn/home-plugin/server';
@ -23,6 +23,8 @@ import { GraphStorage } from './content_management/graph_storage';
export class GraphPlugin implements Plugin {
private licenseState: LicenseState | null = null;
constructor(private readonly initializerContext: PluginInitializerContext) {}
public setup(
core: CoreSetup,
{
@ -45,7 +47,10 @@ export class GraphPlugin implements Plugin {
contentManagement.register({
id: CONTENT_ID,
storage: new GraphStorage(),
storage: new GraphStorage({
throwOnResultValidationError: this.initializerContext.env.mode.dev,
logger: this.initializerContext.logger.get(),
}),
version: {
latest: LATEST_VERSION,
},

View file

@ -41,12 +41,13 @@
"@kbn/saved-objects-finder-plugin",
"@kbn/core-saved-objects-server",
"@kbn/content-management-plugin",
"@kbn/core-saved-objects-api-server",
"@kbn/object-versioning",
"@kbn/content-management-table-list-view-table",
"@kbn/content-management-table-list-view",
"@kbn/core-ui-settings-browser",
"@kbn/react-kibana-mount",
"@kbn/content-management-utils",
"@kbn/logging",
],
"exclude": [
"target/**/*",