mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[content mgmt / maps] Saved Object wrapper for Content Management API (#155680)
## Summary Abstract class for implementing content storage for saved objects. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
7e93f1efba
commit
a4a2e86faa
10 changed files with 540 additions and 419 deletions
|
@ -8,3 +8,5 @@
|
|||
|
||||
export * from './src/types';
|
||||
export * from './src/schema';
|
||||
export * from './src/saved_object_content_storage';
|
||||
export * from './src/utils';
|
||||
|
|
|
@ -0,0 +1,376 @@
|
|||
/*
|
||||
* 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';
|
||||
import { tagsToFindOptions } from './utils';
|
||||
|
||||
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[];
|
||||
}
|
||||
|
||||
export const searchArgsToSOFindOptionsDefault = <T extends string>(
|
||||
params: SearchIn<T, SearchArgsToSOFindOptionsOptionsDefault>
|
||||
): SavedObjectsFindOptions => {
|
||||
const { query, contentTypeId, options } = params;
|
||||
|
||||
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'],
|
||||
...tagsToFindOptions(query.tags),
|
||||
};
|
||||
};
|
||||
|
||||
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>
|
||||
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 = {
|
||||
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'],
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -20,6 +20,15 @@ import type {
|
|||
} from '@kbn/content-management-plugin/common';
|
||||
|
||||
import type {
|
||||
ContentManagementServicesDefinition as ServicesDefinition,
|
||||
Version,
|
||||
} from '@kbn/object-versioning';
|
||||
|
||||
export interface ServicesDefinitionSet {
|
||||
[version: Version]: ServicesDefinition;
|
||||
}
|
||||
|
||||
import {
|
||||
SortOrder,
|
||||
AggregationsAggregationContainer,
|
||||
SortResults,
|
||||
|
@ -179,7 +188,7 @@ export type GetResultSO<T extends object> = GetResult<
|
|||
/**
|
||||
* Saved object with metadata
|
||||
*/
|
||||
export interface SOWithMetadata<Attributes extends object> {
|
||||
export interface SOWithMetadata<Attributes extends object = object> {
|
||||
id: string;
|
||||
type: string;
|
||||
version?: string;
|
||||
|
@ -197,7 +206,7 @@ export interface SOWithMetadata<Attributes extends object> {
|
|||
originId?: string;
|
||||
}
|
||||
|
||||
type PartialItem<Attributes extends object> = Omit<
|
||||
export type SOWithMetadataPartial<Attributes extends object = object> = Omit<
|
||||
SOWithMetadata<Attributes>,
|
||||
'attributes' | 'references'
|
||||
> & {
|
||||
|
@ -205,6 +214,77 @@ type PartialItem<Attributes extends object> = Omit<
|
|||
references: Reference[] | undefined;
|
||||
};
|
||||
|
||||
export interface CMCrudTypes {
|
||||
/**
|
||||
* Saved object attributes
|
||||
*/
|
||||
Attributes: object;
|
||||
/**
|
||||
* Complete saved object
|
||||
*/
|
||||
Item: SOWithMetadata;
|
||||
/**
|
||||
* Partial saved object, used as output for update
|
||||
*/
|
||||
PartialItem: SOWithMetadataPartial;
|
||||
|
||||
/**
|
||||
* Get item params
|
||||
*/
|
||||
GetIn: GetIn;
|
||||
/**
|
||||
* Get item result
|
||||
*/
|
||||
GetOut: GetResultSO<SOWithMetadata>;
|
||||
/**
|
||||
* Create item params
|
||||
*/
|
||||
CreateIn: CreateIn;
|
||||
/**
|
||||
* Create item result
|
||||
*/
|
||||
CreateOut: CreateResult<SOWithMetadata>;
|
||||
/**
|
||||
*
|
||||
*/
|
||||
CreateOptions: object;
|
||||
|
||||
/**
|
||||
* Search item params
|
||||
*/
|
||||
SearchIn: SearchIn;
|
||||
/**
|
||||
* Search item result
|
||||
*/
|
||||
SearchOut: SearchResult<SOWithMetadata>;
|
||||
/**
|
||||
*
|
||||
*/
|
||||
SearchOptions: object;
|
||||
|
||||
/**
|
||||
* Update item params
|
||||
*/
|
||||
UpdateIn: UpdateIn;
|
||||
/**
|
||||
* Update item result
|
||||
*/
|
||||
UpdateOut: UpdateResult<SOWithMetadataPartial>;
|
||||
/**
|
||||
*
|
||||
*/
|
||||
UpdateOptions: object;
|
||||
|
||||
/**
|
||||
* Delete item params
|
||||
*/
|
||||
DeleteIn: DeleteIn;
|
||||
/**
|
||||
* Delete item result
|
||||
*/
|
||||
DeleteOut: DeleteResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Types used by content management storage
|
||||
* @argument ContentType - content management type. assumed to be the same as saved object type
|
||||
|
@ -217,6 +297,7 @@ export interface ContentManagementCrudTypes<
|
|||
UpdateOptions extends object,
|
||||
SearchOptions extends object
|
||||
> {
|
||||
Attributes: Attributes;
|
||||
/**
|
||||
* Complete saved object
|
||||
*/
|
||||
|
@ -224,7 +305,7 @@ export interface ContentManagementCrudTypes<
|
|||
/**
|
||||
* Partial saved object, used as output for update
|
||||
*/
|
||||
PartialItem: PartialItem<Attributes>;
|
||||
PartialItem: SOWithMetadataPartial<Attributes>;
|
||||
/**
|
||||
* Create options
|
||||
*/
|
||||
|
@ -270,7 +351,7 @@ export interface ContentManagementCrudTypes<
|
|||
/**
|
||||
* Update item result
|
||||
*/
|
||||
UpdateOut: UpdateResult<PartialItem<Attributes>>;
|
||||
UpdateOut: UpdateResult<SOWithMetadataPartial<Attributes>>;
|
||||
|
||||
/**
|
||||
* Delete item params
|
||||
|
|
32
packages/kbn-content-management-utils/src/utils.ts
Normal file
32
packages/kbn-content-management-utils/src/utils.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { SavedObjectsFindOptions } from '@kbn/core-saved-objects-api-server';
|
||||
|
||||
export const tagsToFindOptions = ({
|
||||
included,
|
||||
excluded,
|
||||
}: {
|
||||
included?: string[];
|
||||
excluded?: string[];
|
||||
} = {}) => {
|
||||
const hasReference: SavedObjectsFindOptions['hasReference'] = included
|
||||
? included.map((id) => ({
|
||||
id,
|
||||
type: 'tag',
|
||||
}))
|
||||
: undefined;
|
||||
|
||||
const hasNoReference: SavedObjectsFindOptions['hasNoReference'] = excluded
|
||||
? excluded.map((id) => ({
|
||||
id,
|
||||
type: 'tag',
|
||||
}))
|
||||
: undefined;
|
||||
return { hasReference, hasNoReference };
|
||||
};
|
|
@ -19,5 +19,7 @@
|
|||
"@kbn/content-management-plugin",
|
||||
"@kbn/config-schema",
|
||||
"@kbn/core-saved-objects-api-server",
|
||||
"@kbn/config-schema",
|
||||
"@kbn/object-versioning",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -9,24 +9,7 @@ export { LATEST_VERSION, CONTENT_ID } from './constants';
|
|||
|
||||
export type { MapContentType } from './types';
|
||||
|
||||
export type {
|
||||
MapAttributes,
|
||||
MapItem,
|
||||
PartialMapItem,
|
||||
MapGetIn,
|
||||
MapGetOut,
|
||||
MapCreateIn,
|
||||
MapCreateOut,
|
||||
MapCreateOptions,
|
||||
MapUpdateIn,
|
||||
MapUpdateOut,
|
||||
MapUpdateOptions,
|
||||
MapDeleteIn,
|
||||
MapDeleteOut,
|
||||
MapSearchIn,
|
||||
MapSearchOptions,
|
||||
MapSearchOut,
|
||||
} from './latest';
|
||||
export type { MapCrudTypes, MapAttributes, MapItem } from './latest';
|
||||
|
||||
// Today "v1" === "latest" so the export under MapV1 namespace is not really useful
|
||||
// We leave it as a reference for future version when it will be needed to export/support older types
|
||||
|
|
|
@ -5,21 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export type {
|
||||
MapAttributes,
|
||||
MapItem,
|
||||
PartialMapItem,
|
||||
MapGetIn,
|
||||
MapGetOut,
|
||||
MapCreateIn,
|
||||
MapCreateOut,
|
||||
MapCreateOptions,
|
||||
MapUpdateIn,
|
||||
MapUpdateOut,
|
||||
MapUpdateOptions,
|
||||
MapDeleteIn,
|
||||
MapDeleteOut,
|
||||
MapSearchIn,
|
||||
MapSearchOptions,
|
||||
MapSearchOut,
|
||||
} from './types';
|
||||
import { MapCrudTypes } from './types';
|
||||
export type { MapCrudTypes, MapAttributes } from './types';
|
||||
export type MapItem = MapCrudTypes['Item'];
|
||||
|
|
|
@ -31,34 +31,3 @@ export type MapAttributes = {
|
|||
layerListJSON?: string;
|
||||
uiStateJSON?: string;
|
||||
};
|
||||
|
||||
export type MapItem = MapCrudTypes['Item'];
|
||||
export type PartialMapItem = MapCrudTypes['PartialItem'];
|
||||
|
||||
// ----------- GET --------------
|
||||
|
||||
export type MapGetIn = MapCrudTypes['GetIn'];
|
||||
export type MapGetOut = MapCrudTypes['GetOut'];
|
||||
|
||||
// ----------- CREATE --------------
|
||||
|
||||
export type MapCreateIn = MapCrudTypes['CreateIn'];
|
||||
export type MapCreateOut = MapCrudTypes['CreateOut'];
|
||||
export type MapCreateOptions = MapCrudTypes['CreateOptions'];
|
||||
|
||||
// ----------- UPDATE --------------
|
||||
|
||||
export type MapUpdateIn = MapCrudTypes['UpdateIn'];
|
||||
export type MapUpdateOut = MapCrudTypes['UpdateOut'];
|
||||
export type MapUpdateOptions = MapCrudTypes['UpdateOptions'];
|
||||
|
||||
// ----------- DELETE --------------
|
||||
|
||||
export type MapDeleteIn = MapCrudTypes['DeleteIn'];
|
||||
export type MapDeleteOut = MapCrudTypes['DeleteOut'];
|
||||
|
||||
// ----------- SEARCH --------------
|
||||
|
||||
export type MapSearchIn = MapCrudTypes['SearchIn'];
|
||||
export type MapSearchOut = MapCrudTypes['SearchOut'];
|
||||
export type MapSearchOptions = MapCrudTypes['SearchOptions'];
|
||||
|
|
|
@ -6,31 +6,22 @@
|
|||
*/
|
||||
import type { SearchQuery } from '@kbn/content-management-plugin/common';
|
||||
|
||||
import type {
|
||||
MapGetIn,
|
||||
MapGetOut,
|
||||
MapCreateIn,
|
||||
MapCreateOut,
|
||||
MapUpdateIn,
|
||||
MapUpdateOut,
|
||||
MapDeleteIn,
|
||||
MapDeleteOut,
|
||||
MapSearchIn,
|
||||
MapSearchOut,
|
||||
MapSearchOptions,
|
||||
} from '../../common/content_management';
|
||||
import type { MapCrudTypes } from '../../common/content_management';
|
||||
import { CONTENT_ID as contentTypeId } from '../../common/content_management';
|
||||
import { getContentManagement } from '../kibana_services';
|
||||
|
||||
const get = async (id: string) => {
|
||||
return getContentManagement().client.get<MapGetIn, MapGetOut>({
|
||||
return getContentManagement().client.get<MapCrudTypes['GetIn'], MapCrudTypes['GetOut']>({
|
||||
contentTypeId,
|
||||
id,
|
||||
});
|
||||
};
|
||||
|
||||
const create = async ({ data, options }: Omit<MapCreateIn, 'contentTypeId'>) => {
|
||||
const res = await getContentManagement().client.create<MapCreateIn, MapCreateOut>({
|
||||
const create = async ({ data, options }: Omit<MapCrudTypes['CreateIn'], 'contentTypeId'>) => {
|
||||
const res = await getContentManagement().client.create<
|
||||
MapCrudTypes['CreateIn'],
|
||||
MapCrudTypes['CreateOut']
|
||||
>({
|
||||
contentTypeId,
|
||||
data,
|
||||
options,
|
||||
|
@ -38,8 +29,11 @@ const create = async ({ data, options }: Omit<MapCreateIn, 'contentTypeId'>) =>
|
|||
return res;
|
||||
};
|
||||
|
||||
const update = async ({ id, data, options }: Omit<MapUpdateIn, 'contentTypeId'>) => {
|
||||
const res = await getContentManagement().client.update<MapUpdateIn, MapUpdateOut>({
|
||||
const update = async ({ id, data, options }: Omit<MapCrudTypes['UpdateIn'], 'contentTypeId'>) => {
|
||||
const res = await getContentManagement().client.update<
|
||||
MapCrudTypes['UpdateIn'],
|
||||
MapCrudTypes['UpdateOut']
|
||||
>({
|
||||
contentTypeId,
|
||||
id,
|
||||
data,
|
||||
|
@ -49,14 +43,14 @@ const update = async ({ id, data, options }: Omit<MapUpdateIn, 'contentTypeId'>)
|
|||
};
|
||||
|
||||
const deleteMap = async (id: string) => {
|
||||
await getContentManagement().client.delete<MapDeleteIn, MapDeleteOut>({
|
||||
await getContentManagement().client.delete<MapCrudTypes['DeleteIn'], MapCrudTypes['DeleteOut']>({
|
||||
contentTypeId,
|
||||
id,
|
||||
});
|
||||
};
|
||||
|
||||
const search = async (query: SearchQuery = {}, options?: MapSearchOptions) => {
|
||||
return getContentManagement().client.search<MapSearchIn, MapSearchOut>({
|
||||
const search = async (query: SearchQuery = {}, options?: MapCrudTypes['SearchOptions']) => {
|
||||
return getContentManagement().client.search<MapCrudTypes['SearchIn'], MapCrudTypes['SearchOut']>({
|
||||
contentTypeId,
|
||||
query,
|
||||
options,
|
||||
|
|
|
@ -4,338 +4,35 @@
|
|||
* 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,
|
||||
MSearchConfig,
|
||||
} from '@kbn/content-management-plugin/server';
|
||||
import type {
|
||||
SavedObject,
|
||||
SavedObjectReference,
|
||||
SavedObjectsFindOptions,
|
||||
SavedObjectsFindResult,
|
||||
} from '@kbn/core-saved-objects-api-server';
|
||||
|
||||
import { SOContentStorage, tagsToFindOptions } from '@kbn/content-management-utils';
|
||||
import { SavedObjectsFindOptions } from '@kbn/core-saved-objects-api-server';
|
||||
import { CONTENT_ID } from '../../common/content_management';
|
||||
import { cmServicesDefinition } from '../../common/content_management/cm_services';
|
||||
import type {
|
||||
MapItem,
|
||||
PartialMapItem,
|
||||
MapContentType,
|
||||
MapAttributes,
|
||||
MapGetOut,
|
||||
MapCreateIn,
|
||||
MapCreateOut,
|
||||
MapCreateOptions,
|
||||
MapUpdateIn,
|
||||
MapUpdateOut,
|
||||
MapUpdateOptions,
|
||||
MapDeleteOut,
|
||||
MapSearchOptions,
|
||||
MapSearchOut,
|
||||
} from '../../common/content_management';
|
||||
import type { MapCrudTypes } 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 savedObjectToMapItem(savedObject: SavedObject<MapAttributes>, partial: false): MapItem;
|
||||
|
||||
function savedObjectToMapItem(
|
||||
savedObject: PartialSavedObject<MapAttributes>,
|
||||
partial: true
|
||||
): PartialMapItem;
|
||||
|
||||
function savedObjectToMapItem(
|
||||
savedObject: SavedObject<MapAttributes> | PartialSavedObject<MapAttributes>
|
||||
): MapItem | PartialMapItem {
|
||||
const {
|
||||
id,
|
||||
type,
|
||||
updated_at: updatedAt,
|
||||
created_at: createdAt,
|
||||
attributes: { title, description, layerListJSON, mapStateJSON, uiStateJSON },
|
||||
references,
|
||||
error,
|
||||
namespaces,
|
||||
} = savedObject;
|
||||
const searchArgsToSOFindOptions = (args: MapCrudTypes['SearchIn']): SavedObjectsFindOptions => {
|
||||
const { query, contentTypeId, options } = args;
|
||||
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
updatedAt,
|
||||
createdAt,
|
||||
attributes: {
|
||||
title,
|
||||
description,
|
||||
layerListJSON,
|
||||
mapStateJSON,
|
||||
uiStateJSON,
|
||||
},
|
||||
references,
|
||||
error,
|
||||
namespaces,
|
||||
};
|
||||
}
|
||||
|
||||
const SO_TYPE: MapContentType = 'map';
|
||||
|
||||
export class MapsStorage
|
||||
implements ContentStorage<MapItem, PartialMapItem, MSearchConfig<MapItem, MapAttributes>>
|
||||
{
|
||||
constructor() {}
|
||||
|
||||
async get(ctx: StorageContext, id: string): Promise<MapGetOut> {
|
||||
const {
|
||||
utils: { getTransforms },
|
||||
} = ctx;
|
||||
const transforms = getTransforms(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<MapAttributes>(SO_TYPE, id);
|
||||
|
||||
const response: MapGetOut = {
|
||||
item: savedObjectToMapItem(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<MapGetOut, MapGetOut>(
|
||||
response
|
||||
);
|
||||
|
||||
if (resultError) {
|
||||
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
async bulkGet(): Promise<never> {
|
||||
// Not implemented. Maps does not use bulkGet
|
||||
throw new Error(`[bulkGet] has not been implemented. See MapsStorage class.`);
|
||||
}
|
||||
|
||||
async create(
|
||||
ctx: StorageContext,
|
||||
data: MapCreateIn['data'],
|
||||
options: MapCreateOptions
|
||||
): Promise<MapCreateOut> {
|
||||
const {
|
||||
utils: { getTransforms },
|
||||
} = ctx;
|
||||
const transforms = getTransforms(cmServicesDefinition);
|
||||
|
||||
// Validate input (data & options) & UP transform them to the latest version
|
||||
const { value: dataToLatest, error: dataError } = transforms.create.in.data.up<
|
||||
MapAttributes,
|
||||
MapAttributes
|
||||
>(data);
|
||||
if (dataError) {
|
||||
throw Boom.badRequest(`Invalid data. ${dataError.message}`);
|
||||
}
|
||||
|
||||
const { value: optionsToLatest, error: optionsError } = transforms.create.in.options.up<
|
||||
MapCreateOptions,
|
||||
MapCreateOptions
|
||||
>(options);
|
||||
if (optionsError) {
|
||||
throw Boom.badRequest(`Invalid options. ${optionsError.message}`);
|
||||
}
|
||||
|
||||
// Save data in DB
|
||||
const soClient = await savedObjectClientFromRequest(ctx);
|
||||
const savedObject = await soClient.create<MapAttributes>(
|
||||
SO_TYPE,
|
||||
dataToLatest,
|
||||
optionsToLatest
|
||||
);
|
||||
|
||||
// Validate DB response and DOWN transform to the request version
|
||||
const { value, error: resultError } = transforms.create.out.result.down<
|
||||
MapCreateOut,
|
||||
MapCreateOut
|
||||
>({
|
||||
item: savedObjectToMapItem(savedObject, false),
|
||||
});
|
||||
|
||||
if (resultError) {
|
||||
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
async update(
|
||||
ctx: StorageContext,
|
||||
id: string,
|
||||
data: MapUpdateIn['data'],
|
||||
options: MapUpdateOptions
|
||||
): Promise<MapUpdateOut> {
|
||||
const {
|
||||
utils: { getTransforms },
|
||||
} = ctx;
|
||||
const transforms = getTransforms(cmServicesDefinition);
|
||||
|
||||
// Validate input (data & options) & UP transform them to the latest version
|
||||
const { value: dataToLatest, error: dataError } = transforms.update.in.data.up<
|
||||
MapAttributes,
|
||||
MapAttributes
|
||||
>(data);
|
||||
if (dataError) {
|
||||
throw Boom.badRequest(`Invalid data. ${dataError.message}`);
|
||||
}
|
||||
|
||||
const { value: optionsToLatest, error: optionsError } = transforms.update.in.options.up<
|
||||
MapCreateOptions,
|
||||
MapCreateOptions
|
||||
>(options);
|
||||
if (optionsError) {
|
||||
throw Boom.badRequest(`Invalid options. ${optionsError.message}`);
|
||||
}
|
||||
|
||||
// Save data in DB
|
||||
const soClient = await savedObjectClientFromRequest(ctx);
|
||||
const partialSavedObject = await soClient.update<MapAttributes>(
|
||||
SO_TYPE,
|
||||
id,
|
||||
dataToLatest,
|
||||
optionsToLatest
|
||||
);
|
||||
|
||||
// Validate DB response and DOWN transform to the request version
|
||||
const { value, error: resultError } = transforms.update.out.result.down<
|
||||
MapUpdateOut,
|
||||
MapUpdateOut
|
||||
>({
|
||||
item: savedObjectToMapItem(partialSavedObject, true),
|
||||
});
|
||||
|
||||
if (resultError) {
|
||||
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
async delete(ctx: StorageContext, id: string): Promise<MapDeleteOut> {
|
||||
const soClient = await savedObjectClientFromRequest(ctx);
|
||||
await soClient.delete(SO_TYPE, id);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async search(
|
||||
ctx: StorageContext,
|
||||
query: SearchQuery,
|
||||
options: MapSearchOptions = {}
|
||||
): Promise<MapSearchOut> {
|
||||
const {
|
||||
utils: { getTransforms },
|
||||
} = ctx;
|
||||
const transforms = getTransforms(cmServicesDefinition);
|
||||
const soClient = await savedObjectClientFromRequest(ctx);
|
||||
|
||||
// Validate and UP transform the options
|
||||
const { value: optionsToLatest, error: optionsError } = transforms.search.in.options.up<
|
||||
MapSearchOptions,
|
||||
MapSearchOptions
|
||||
>(options);
|
||||
if (optionsError) {
|
||||
throw Boom.badRequest(`Invalid payload. ${optionsError.message}`);
|
||||
}
|
||||
const { onlyTitle = false } = 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: CONTENT_ID,
|
||||
search: query.text,
|
||||
perPage: query.limit,
|
||||
page: query.cursor ? +query.cursor : undefined,
|
||||
defaultSearchOperator: 'AND',
|
||||
searchFields: onlyTitle ? ['title'] : ['title^3', 'description'],
|
||||
fields: ['description', 'title'],
|
||||
hasReference,
|
||||
hasNoReference,
|
||||
};
|
||||
|
||||
// Execute the query in the DB
|
||||
const response = await soClient.find<MapAttributes>(soQuery);
|
||||
|
||||
// Validate the response and DOWN transform to the request version
|
||||
const { value, error: resultError } = transforms.search.out.result.down<
|
||||
MapSearchOut,
|
||||
MapSearchOut
|
||||
>({
|
||||
hits: response.saved_objects.map((so) => savedObjectToMapItem(so, false)),
|
||||
pagination: {
|
||||
total: response.total,
|
||||
},
|
||||
});
|
||||
|
||||
if (resultError) {
|
||||
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
// Configure `mSearch` to opt-in maps into the multi content type search API
|
||||
mSearch = {
|
||||
savedObjectType: SO_TYPE,
|
||||
toItemResult: (
|
||||
ctx: StorageContext,
|
||||
savedObject: SavedObjectsFindResult<MapAttributes>
|
||||
): MapItem => {
|
||||
const {
|
||||
utils: { getTransforms },
|
||||
} = ctx;
|
||||
const transforms = getTransforms(cmServicesDefinition);
|
||||
|
||||
// Validate DB response and DOWN transform to the request version
|
||||
const { value, error: resultError } = transforms.mSearch.out.result.down<MapItem, MapItem>(
|
||||
savedObjectToMapItem(savedObject, false)
|
||||
);
|
||||
|
||||
if (resultError) {
|
||||
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
type: contentTypeId,
|
||||
searchFields: options?.onlyTitle ? ['title'] : ['title^3', 'description'],
|
||||
fields: ['description', 'title'],
|
||||
search: query.text,
|
||||
perPage: query.limit,
|
||||
page: query.cursor ? +query.cursor : undefined,
|
||||
defaultSearchOperator: 'AND',
|
||||
...tagsToFindOptions(query.tags),
|
||||
};
|
||||
};
|
||||
|
||||
export class MapsStorage extends SOContentStorage<MapCrudTypes> {
|
||||
constructor() {
|
||||
super({
|
||||
savedObjectType: CONTENT_ID,
|
||||
cmServicesDefinition,
|
||||
searchArgsToSOFindOptions,
|
||||
enableMSearch: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue