Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[content mgmt / maps] Saved Object wrapper for Content Management API #155680

Merged
merged 36 commits into from
May 16, 2023
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
d601dbd
abstract types
mattkime Apr 15, 2023
9e06b50
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Apr 15, 2023
83b1ff5
[CI] Auto-commit changed files from 'node scripts/generate codeowners'
kibanamachine Apr 15, 2023
86e3a85
fix types
mattkime Apr 15, 2023
8daec60
Merge branch 'content_management_api_so_type_abstraction' of github.c…
mattkime Apr 15, 2023
aea2173
remove comment
mattkime Apr 15, 2023
28548f7
fix partial types
mattkime Apr 15, 2023
44ca8ee
add readme content
mattkime Apr 15, 2023
5b8eb84
Merge branch 'main' into content_management_api_so_type_abstraction
mattkime Apr 15, 2023
3ab8198
simplify type export
mattkime Apr 17, 2023
e10b297
simplify type export
mattkime Apr 17, 2023
055679a
simplify type export
mattkime Apr 17, 2023
ac841b5
Merge branch 'main' into content_management_api_so_type_abstraction
mattkime Apr 17, 2023
df3fd85
Merge branch 'main' into content_management_api_so_type_abstraction
mattkime Apr 18, 2023
eee1744
move Option types external to main interface
mattkime Apr 19, 2023
8f9c939
schema abstraction
mattkime Apr 20, 2023
5303a6b
schema abstraction
mattkime Apr 20, 2023
203362d
partial progress
mattkime Apr 21, 2023
83fe904
partial progress
mattkime Apr 21, 2023
2dabe2d
partial progress
mattkime Apr 21, 2023
a2ffd87
partial progress
mattkime Apr 25, 2023
861b8af
saved object content storage class seems to work
mattkime Apr 25, 2023
6eb14be
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Apr 25, 2023
6700bf1
type fix
mattkime Apr 25, 2023
5c6f76b
Merge branch 'content_mgmt_so_content_storage' of github.com:mattkime…
mattkime Apr 25, 2023
eaaf689
typefix
mattkime Apr 25, 2023
e9e392b
type fix
mattkime Apr 25, 2023
70ec400
fix references for map type
mattkime Apr 25, 2023
b300734
enable msearch for maps
mattkime Apr 25, 2023
5742da2
cleanup
mattkime Apr 25, 2023
b885823
Merge branch 'main' into content_mgmt_so_content_storage
mattkime Apr 25, 2023
5685fd4
Merge branch 'main' into content_mgmt_so_content_storage
mattkime Apr 28, 2023
aa728d9
Merge branch 'main' into content_mgmt_so_content_storage
mattkime Apr 28, 2023
770327d
Merge branch 'main' into content_mgmt_so_content_storage
mattkime May 8, 2023
9e09aed
Merge branch 'main' into content_mgmt_so_content_storage
mattkime May 15, 2023
b2282fc
use tagsToFindOptions
mattkime May 16, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/kbn-content-management-utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@

export * from './src/types';
export * from './src/schema';
export * from './src/saved_object_content_storage';
export * from './src/utils';
Original file line number Diff line number Diff line change
@@ -0,0 +1,391 @@
/*
* 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 Boom from '@hapi/boom';
import type { SearchQuery, SearchIn } from '@kbn/content-management-plugin/common';
import type {
ContentStorage,
StorageContext,
MSearchConfig,
} from '@kbn/content-management-plugin/server';
import type {
SavedObject,
SavedObjectReference,
SavedObjectsFindOptions,
SavedObjectsCreateOptions,
SavedObjectsUpdateOptions,
SavedObjectsFindResult,
} from '@kbn/core-saved-objects-api-server';
import type {
CMCrudTypes,
ServicesDefinitionSet,
SOWithMetadata,
SOWithMetadataPartial,
} from './types';

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 savedObjectToItem<Attributes extends object, Item extends SOWithMetadata>(
savedObject: SavedObject<Attributes>,
partial: false
): Item;

function savedObjectToItem<Attributes extends object, PartialItem extends SOWithMetadata>(
savedObject: PartialSavedObject<Attributes>,
partial: true
): PartialItem;

function savedObjectToItem<Attributes extends object>(
savedObject: SavedObject<Attributes> | PartialSavedObject<Attributes>
): SOWithMetadata | SOWithMetadataPartial {
const {
id,
type,
updated_at: updatedAt,
created_at: createdAt,
attributes,
references,
error,
namespaces,
} = savedObject;

return {
id,
type,
updatedAt,
createdAt,
attributes,
references,
error,
namespaces,
};
}

export interface SearchArgsToSOFindOptionsOptionsDefault {
fields?: string[];
searchFields?: string[];
sebelga marked this conversation as resolved.
Show resolved Hide resolved
}

export const searchArgsToSOFindOptionsDefault = <T extends string>(
params: SearchIn<T, SearchArgsToSOFindOptionsOptionsDefault>
): SavedObjectsFindOptions => {
const { query, contentTypeId, options } = params;
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use tagsToFindOptions here too?


return {
type: contentTypeId,
search: query.text,
perPage: query.limit,
page: query.cursor ? +query.cursor : undefined,
defaultSearchOperator: 'AND',
searchFields: options?.searchFields ?? ['description', 'title'],
fields: options?.fields ?? ['description', 'title'],
hasReference,
hasNoReference,
};
};

export const createArgsToSoCreateOptionsDefault = (
params: SavedObjectsCreateOptions
): SavedObjectsCreateOptions => params;

export const updateArgsToSoUpdateOptionsDefault = <Types extends CMCrudTypes>(
params: SavedObjectsUpdateOptions<Types['Attributes']>
): SavedObjectsUpdateOptions<Types['Attributes']> => params;

export type CreateArgsToSoCreateOptions<Types extends CMCrudTypes> = (
params: Types['CreateOptions']
) => SavedObjectsCreateOptions;

export type SearchArgsToSOFindOptions<Types extends CMCrudTypes> = (
params: Types['SearchIn']
) => SavedObjectsFindOptions;

export type UpdateArgsToSoUpdateOptions<Types extends CMCrudTypes> = (
params: Types['UpdateOptions']
) => SavedObjectsUpdateOptions<Types['Attributes']>;

export interface SOContentStorageConstrutorParams<Types extends CMCrudTypes> {
savedObjectType: string;
cmServicesDefinition: ServicesDefinitionSet;
createArgsToSoCreateOptions?: CreateArgsToSoCreateOptions<Types>;
updateArgsToSoUpdateOptions?: UpdateArgsToSoUpdateOptions<Types>;
searchArgsToSOFindOptions?: SearchArgsToSOFindOptions<Types>;
enableMSearch?: boolean;
}

export abstract class SOContentStorage<Types extends CMCrudTypes>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class is where the work happens.

implements
ContentStorage<
Types['Item'],
Types['PartialItem'],
MSearchConfig<Types['Item'], Types['Attributes']>
>
{
constructor({
savedObjectType,
cmServicesDefinition,
createArgsToSoCreateOptions,
updateArgsToSoUpdateOptions,
searchArgsToSOFindOptions,
enableMSearch,
}: SOContentStorageConstrutorParams<Types>) {
this.savedObjectType = savedObjectType;
this.cmServicesDefinition = cmServicesDefinition;
this.createArgsToSoCreateOptions =
createArgsToSoCreateOptions || createArgsToSoCreateOptionsDefault;
this.updateArgsToSoUpdateOptions =
updateArgsToSoUpdateOptions || updateArgsToSoUpdateOptionsDefault;
this.searchArgsToSOFindOptions = searchArgsToSOFindOptions || searchArgsToSOFindOptionsDefault;

if (enableMSearch) {
this.mSearch = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Dosant We should find a better api for this. I'd prefer to use some sort of boolean to determine whether mSearch is enabled. As best I can tell this is largely a reconfiguration of code used elsewhere

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will improve. #155718
hopefully non blocking

savedObjectType: this.savedObjectType,
toItemResult: (ctx: StorageContext, savedObject: SavedObjectsFindResult): Types['Item'] => {
const transforms = ctx.utils.getTransforms(this.cmServicesDefinition);

// Validate DB response and DOWN transform to the request version
const { value, error: resultError } = transforms.mSearch.out.result.down<
Types['Item'],
Types['Item']
>(savedObjectToItem(savedObject as SavedObjectsFindResult<Types['Attributes']>, false));

if (resultError) {
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
}

return value;
},
};
}
}

private savedObjectType: SOContentStorageConstrutorParams<Types>['savedObjectType'];
private cmServicesDefinition: SOContentStorageConstrutorParams<Types>['cmServicesDefinition'];
private createArgsToSoCreateOptions: CreateArgsToSoCreateOptions<Types>;
private updateArgsToSoUpdateOptions: UpdateArgsToSoUpdateOptions<Types>;
private searchArgsToSOFindOptions: SearchArgsToSOFindOptions<Types>;

mSearch?: {
savedObjectType: string;
toItemResult: (ctx: StorageContext, savedObject: SavedObjectsFindResult) => Types['Item'];
};

async get(ctx: StorageContext, id: string): Promise<Types['GetOut']> {
const transforms = ctx.utils.getTransforms(this.cmServicesDefinition);
const soClient = await savedObjectClientFromRequest(ctx);

// Save data in DB
const {
saved_object: savedObject,
alias_purpose: aliasPurpose,
alias_target_id: aliasTargetId,
outcome,
} = await soClient.resolve<Types['Attributes']>(this.savedObjectType, id);

const response: Types['GetOut'] = {
item: savedObjectToItem(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<
Types['GetOut'],
Types['GetOut']
>(response);

if (resultError) {
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
}

return value;
}

async bulkGet(): Promise<never> {
// Not implemented
throw new Error(`[bulkGet] has not been implemented. See ${this.constructor.name} class.`);
}

async create(
ctx: StorageContext,
data: Types['Attributes'],
options: Types['CreateOptions']
): Promise<Types['CreateOut']> {
const transforms = ctx.utils.getTransforms(this.cmServicesDefinition);
const soClient = await savedObjectClientFromRequest(ctx);

// Validate input (data & options) & UP transform them to the latest version
const { value: dataToLatest, error: dataError } = transforms.create.in.data.up<
Types['Attributes'],
Types['Attributes']
>(data);
if (dataError) {
throw Boom.badRequest(`Invalid data. ${dataError.message}`);
}

const { value: optionsToLatest, error: optionsError } = transforms.create.in.options.up<
Types['CreateOptions'],
Types['CreateOptions']
>(options);
if (optionsError) {
throw Boom.badRequest(`Invalid options. ${optionsError.message}`);
}

const createOptions = this.createArgsToSoCreateOptions(optionsToLatest);

// Save data in DB
const savedObject = await soClient.create<Types['Attributes']>(
this.savedObjectType,
dataToLatest,
createOptions
);

// Validate DB response and DOWN transform to the request version
const { value, error: resultError } = transforms.create.out.result.down<
Types['CreateOut'],
Types['CreateOut']
>({
item: savedObjectToItem(savedObject, false),
});

if (resultError) {
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
}

return value;
}

async update(
ctx: StorageContext,
id: string,
data: Types['Attributes'],
options: Types['UpdateOptions']
): Promise<Types['UpdateOut']> {
const transforms = ctx.utils.getTransforms(this.cmServicesDefinition);
const soClient = await savedObjectClientFromRequest(ctx);

// Validate input (data & options) & UP transform them to the latest version
const { value: dataToLatest, error: dataError } = transforms.update.in.data.up<
Types['Attributes'],
sebelga marked this conversation as resolved.
Show resolved Hide resolved
Types['Attributes']
>(data);
if (dataError) {
throw Boom.badRequest(`Invalid data. ${dataError.message}`);
}

const { value: optionsToLatest, error: optionsError } = transforms.update.in.options.up<
Types['CreateOptions'],
Types['CreateOptions']
>(options);
if (optionsError) {
throw Boom.badRequest(`Invalid options. ${optionsError.message}`);
}

const updateOptions = this.updateArgsToSoUpdateOptions(optionsToLatest);

// Save data in DB
const partialSavedObject = await soClient.update<Types['Attributes']>(
this.savedObjectType,
id,
dataToLatest,
updateOptions
);

// Validate DB response and DOWN transform to the request version
const { value, error: resultError } = transforms.update.out.result.down<
Types['UpdateOut'],
Types['UpdateOut']
>({
item: savedObjectToItem(partialSavedObject, true),
});

if (resultError) {
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
}

return value;
}

async delete(ctx: StorageContext, id: string): Promise<Types['DeleteOut']> {
const soClient = await savedObjectClientFromRequest(ctx);
await soClient.delete(this.savedObjectType, id);
return { success: true };
}

async search(
ctx: StorageContext,
query: SearchQuery,
options: Types['SearchOptions'] = {}
): Promise<Types['SearchOut']> {
const transforms = ctx.utils.getTransforms(this.cmServicesDefinition);
const soClient = await savedObjectClientFromRequest(ctx);

// Validate and UP transform the options
const { value: optionsToLatest, error: optionsError } = transforms.search.in.options.up<
Types['SearchOptions'],
Types['SearchOptions']
>(options);
if (optionsError) {
throw Boom.badRequest(`Invalid payload. ${optionsError.message}`);
}

const soQuery: SavedObjectsFindOptions = this.searchArgsToSOFindOptions({
contentTypeId: this.savedObjectType,
query,
options: optionsToLatest,
});
// Execute the query in the DB
const response = await soClient.find<Types['Attributes']>(soQuery);

// Validate the response and DOWN transform to the request version
const { value, error: resultError } = transforms.search.out.result.down<
Types['SearchOut'],
Types['SearchOut']
>({
hits: response.saved_objects.map((so) => savedObjectToItem(so, false)),
pagination: {
total: response.total,
},
});

if (resultError) {
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
}

return value;
}
}
Loading