[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:
Matthew Kime 2023-05-16 09:11:21 -05:00 committed by GitHub
parent 7e93f1efba
commit a4a2e86faa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 540 additions and 419 deletions

View file

@ -8,3 +8,5 @@
export * from './src/types';
export * from './src/schema';
export * from './src/saved_object_content_storage';
export * from './src/utils';

View file

@ -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;
}
}

View file

@ -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

View 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 };
};

View file

@ -19,5 +19,7 @@
"@kbn/content-management-plugin",
"@kbn/config-schema",
"@kbn/core-saved-objects-api-server",
"@kbn/config-schema",
"@kbn/object-versioning",
]
}

View file

@ -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

View file

@ -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'];

View file

@ -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'];

View file

@ -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,

View file

@ -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,
});
}
}