[content management / maps] Create abstract types for saved object usage with content management api (#154985)

## Summary

Abstract types for using Saved Objects with the content management api.
This should significantly reduce the amount of code to use additional
saved object types.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Matthew Kime 2023-04-24 20:58:22 -05:00 committed by GitHub
parent 4020858827
commit 273eec0f64
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 452 additions and 92 deletions

1
.github/CODEOWNERS vendored
View file

@ -88,6 +88,7 @@ packages/content-management/content_editor @elastic/appex-sharedux
examples/content_management_examples @elastic/appex-sharedux
src/plugins/content_management @elastic/appex-sharedux
packages/content-management/table_list @elastic/appex-sharedux
packages/kbn-content-management-utils @elastic/kibana-data-discovery
examples/controls_example @elastic/kibana-presentation
src/plugins/controls @elastic/kibana-presentation
src/core @elastic/kibana-core

View file

@ -187,6 +187,7 @@
"@kbn/content-management-examples-plugin": "link:examples/content_management_examples",
"@kbn/content-management-plugin": "link:src/plugins/content_management",
"@kbn/content-management-table-list": "link:packages/content-management/table_list",
"@kbn/content-management-utils": "link:packages/kbn-content-management-utils",
"@kbn/controls-example-plugin": "link:examples/controls_example",
"@kbn/controls-plugin": "link:src/plugins/controls",
"@kbn/core": "link:src/core",

View file

@ -0,0 +1,58 @@
# Content management utils
Utilities to ease the implementation of the Content Management API with Saved Objects.
```ts
import type {
ContentManagementCrudTypes,
CreateOptions,
SearchOptions,
UpdateOptions,
} from '@kbn/content-management-utils';
import { MapContentType } from '../types';
export type MapCrudTypes = ContentManagementCrudTypes<MapContentType, MapAttributes>;
/* eslint-disable-next-line @typescript-eslint/consistent-type-definitions */
export type MapAttributes = {
title: string;
description?: string;
mapStateJSON?: string;
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 = CreateOptions;
// ----------- UPDATE --------------
export type MapUpdateIn = MapCrudTypes['UpdateIn'];
export type MapUpdateOut = MapCrudTypes['UpdateOut'];
export type MapUpdateOptions = 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 = SearchOptions;
```

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export * from './types';

View file

@ -0,0 +1,13 @@
/*
* 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.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-content-management-utils'],
};

View file

@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/content-management-utils",
"owner": "@elastic/kibana-data-discovery"
}

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/content-management-utils",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -0,0 +1,22 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node",
"react"
]
},
"include": [
"**/*.ts",
"**/*.tsx",
],
"exclude": [
"target/**/*"
],
"kbn_references": [
"@kbn/content-management-plugin",
"@kbn/core-saved-objects-api-server",
]
}

View file

@ -0,0 +1,283 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type {
GetIn,
GetResult,
CreateIn,
CreateResult,
SearchIn,
SearchResult,
UpdateIn,
UpdateResult,
DeleteIn,
DeleteResult,
} from '@kbn/content-management-plugin/common';
import type {
SortOrder,
AggregationsAggregationContainer,
SortResults,
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import {
MutatingOperationRefreshSetting,
SavedObjectsPitParams,
SavedObjectsFindOptionsReference,
} from '@kbn/core-saved-objects-api-server';
type KueryNode = any;
export interface Reference {
type: string;
id: string;
name: string;
}
/** Saved Object create options - Pick and Omit to customize */
export interface SavedObjectCreateOptions {
/** (not recommended) Specify an id for the document */
id?: string;
/** Overwrite existing documents (defaults to false) */
overwrite?: boolean;
/**
* An opaque version number which changes on each successful write operation.
* Can be used in conjunction with `overwrite` for implementing optimistic concurrency control.
**/
version?: string;
/** Array of referenced saved objects. */
references?: Reference[];
/** The Elasticsearch Refresh setting for this operation */
refresh?: boolean;
/**
* Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in
* {@link SavedObjectsCreateOptions}.
*
* * For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces,
* including the "All spaces" identifier (`'*'`).
* * For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only
* be used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed.
* * For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used.
*/
initialNamespaces?: string[];
}
/** Saved Object search options - Pick and Omit to customize */
export interface SavedObjectSearchOptions {
/** the page of results to return */
page?: number;
/** the number of objects per page */
perPage?: number;
/** which field to sort by */
sortField?: string;
/** sort order, ascending or descending */
sortOrder?: SortOrder;
/**
* An array of fields to include in the results
* @example
* SavedObjects.find({type: 'dashboard', fields: ['attributes.name', 'attributes.location']})
*/
fields?: string[];
/** Search documents using the Elasticsearch Simple Query String syntax. See Elasticsearch Simple Query String `query` argument for more information */
search?: string;
/** The fields to perform the parsed query against. See Elasticsearch Simple Query String `fields` argument for more information */
searchFields?: string[];
/**
* Use the sort values from the previous page to retrieve the next page of results.
*/
searchAfter?: SortResults;
/**
* The fields to perform the parsed query against. Unlike the `searchFields` argument, these are expected to be root fields and will not
* be modified. If used in conjunction with `searchFields`, both are concatenated together.
*/
rootSearchFields?: string[];
/**
* Search for documents having a reference to the specified objects.
* Use `hasReferenceOperator` to specify the operator to use when searching for multiple references.
*/
hasReference?: SavedObjectsFindOptionsReference | SavedObjectsFindOptionsReference[];
/**
* The operator to use when searching by multiple references using the `hasReference` option. Defaults to `OR`
*/
hasReferenceOperator?: 'AND' | 'OR';
/**
* Search for documents *not* having a reference to the specified objects.
* Use `hasNoReferenceOperator` to specify the operator to use when searching for multiple references.
*/
hasNoReference?: SavedObjectsFindOptionsReference | SavedObjectsFindOptionsReference[];
/**
* The operator to use when searching by multiple references using the `hasNoReference` option. Defaults to `OR`
*/
hasNoReferenceOperator?: 'AND' | 'OR';
/**
* The search operator to use with the provided filter. Defaults to `OR`
*/
defaultSearchOperator?: 'AND' | 'OR';
/** filter string for the search query */
filter?: string | KueryNode;
/**
* A record of aggregations to perform.
* The API currently only supports a limited set of metrics and bucket aggregation types.
* Additional aggregation types can be contributed to Core.
*
* @example
* Aggregating on SO attribute field
* ```ts
* const aggs = { latest_version: { max: { field: 'dashboard.attributes.version' } } };
* return client.find({ type: 'dashboard', aggs })
* ```
*
* @example
* Aggregating on SO root field
* ```ts
* const aggs = { latest_update: { max: { field: 'dashboard.updated_at' } } };
* return client.find({ type: 'dashboard', aggs })
* ```
*
* @alpha
*/
aggs?: Record<string, AggregationsAggregationContainer>;
/** array of namespaces to search */
namespaces?: string[];
/**
* Search against a specific Point In Time (PIT) that you've opened with {@link SavedObjectsClient.openPointInTimeForType}.
*/
pit?: SavedObjectsPitParams;
}
/** Saved Object update options - Pick and Omit to customize */
export interface SavedObjectUpdateOptions<Attributes = unknown> {
/** Array of referenced saved objects. */
references?: Reference[];
version?: string;
/** The Elasticsearch Refresh setting for this operation */
refresh?: MutatingOperationRefreshSetting;
/** If specified, will be used to perform an upsert if the object doesn't exist */
upsert?: Attributes;
/**
* The Elasticsearch `retry_on_conflict` setting for this operation.
* Defaults to `0` when `version` is provided, `3` otherwise.
*/
retryOnConflict?: number;
}
/** Return value for Saved Object get, T is item returned */
export type GetResultSO<T extends object> = GetResult<
T,
{
outcome: 'exactMatch' | 'aliasMatch' | 'conflict';
aliasTargetId?: string;
aliasPurpose?: 'savedObjectConversion' | 'savedObjectImport';
}
>;
/**
* Saved object with metadata
*/
export interface SOWithMetadata<Attributes extends object> {
id: string;
type: string;
version?: string;
createdAt?: string;
updatedAt?: string;
error?: {
error: string;
message: string;
statusCode: number;
metadata?: Record<string, unknown>;
};
attributes: Attributes;
references: Reference[];
namespaces?: string[];
originId?: string;
}
type PartialItem<Attributes extends object> = Omit<
SOWithMetadata<Attributes>,
'attributes' | 'references'
> & {
attributes: Partial<Attributes>;
references: Reference[] | undefined;
};
/**
* Types used by content management storage
* @argument ContentType - content management type. assumed to be the same as saved object type
* @argument Attributes - attributes of the saved object
*/
export interface ContentManagementCrudTypes<
ContentType extends string,
Attributes extends object,
CreateOptions extends object,
UpdateOptions extends object,
SearchOptions extends object
> {
/**
* Complete saved object
*/
Item: SOWithMetadata<Attributes>;
/**
* Partial saved object, used as output for update
*/
PartialItem: PartialItem<Attributes>;
/**
* Create options
*/
CreateOptions: CreateOptions;
/**
* Update options
*/
UpdateOptions: UpdateOptions;
/**
* Search options
*/
SearchOptions: SearchOptions;
/**
* Get item params
*/
GetIn: GetIn<ContentType>;
/**
* Get item result
*/
GetOut: GetResultSO<SOWithMetadata<Attributes>>;
/**
* Create item params
*/
CreateIn: CreateIn<ContentType, Attributes, CreateOptions>;
/**
* Create item result
*/
CreateOut: CreateResult<SOWithMetadata<Attributes>>;
/**
* Search item params
*/
SearchIn: SearchIn<ContentType, SearchOptions>;
/**
* Search item result
*/
SearchOut: SearchResult<SOWithMetadata<Attributes>>;
/**
* Update item params
*/
UpdateIn: UpdateIn<ContentType, Attributes, UpdateOptions>;
/**
* Update item result
*/
UpdateOut: UpdateResult<PartialItem<Attributes>>;
/**
* Delete item params
*/
DeleteIn: DeleteIn<ContentType>;
/**
* Delete item result
*/
DeleteOut: DeleteResult;
}

View file

@ -170,6 +170,8 @@
"@kbn/content-management-plugin/*": ["src/plugins/content_management/*"],
"@kbn/content-management-table-list": ["packages/content-management/table_list"],
"@kbn/content-management-table-list/*": ["packages/content-management/table_list/*"],
"@kbn/content-management-utils": ["packages/kbn-content-management-utils"],
"@kbn/content-management-utils/*": ["packages/kbn-content-management-utils/*"],
"@kbn/controls-example-plugin": ["examples/controls_example"],
"@kbn/controls-example-plugin/*": ["examples/controls_example/*"],
"@kbn/controls-plugin": ["src/plugins/controls"],

View file

@ -17,10 +17,10 @@ export type {
MapGetOut,
MapCreateIn,
MapCreateOut,
CreateOptions,
MapCreateOptions,
MapUpdateIn,
MapUpdateOut,
UpdateOptions,
MapUpdateOptions,
MapDeleteIn,
MapDeleteOut,
MapSearchIn,

View file

@ -13,10 +13,10 @@ export type {
MapGetOut,
MapCreateIn,
MapCreateOut,
CreateOptions,
MapCreateOptions,
MapUpdateIn,
MapUpdateOut,
UpdateOptions,
MapUpdateOptions,
MapDeleteIn,
MapDeleteOut,
MapSearchIn,

View file

@ -6,24 +6,22 @@
*/
import type {
GetIn,
GetResult,
CreateIn,
CreateResult,
SearchIn,
SearchResult,
UpdateIn,
UpdateResult,
DeleteIn,
DeleteResult,
} from '@kbn/content-management-plugin/common';
ContentManagementCrudTypes,
SavedObjectCreateOptions,
SavedObjectUpdateOptions,
} from '@kbn/content-management-utils';
import { MapContentType } from '../types';
interface Reference {
type: string;
id: string;
name: string;
}
export type MapCrudTypes = ContentManagementCrudTypes<
MapContentType,
MapAttributes,
Pick<SavedObjectCreateOptions, 'references'>,
Pick<SavedObjectUpdateOptions, 'references'>,
{
/** Flag to indicate to only search the text on the "title" field */
onlyTitle?: boolean;
}
>;
/* eslint-disable-next-line @typescript-eslint/consistent-type-definitions */
export type MapAttributes = {
@ -34,77 +32,33 @@ export type MapAttributes = {
uiStateJSON?: string;
};
export interface MapItem {
id: string;
type: string;
version?: string;
createdAt?: string;
updatedAt?: string;
error?: {
error: string;
message: string;
statusCode: number;
metadata?: Record<string, unknown>;
};
attributes: MapAttributes;
references: Reference[];
namespaces?: string[];
originId?: string;
}
export type PartialMapItem = Omit<MapItem, 'attributes' | 'references'> & {
attributes: Partial<MapAttributes>;
references: Reference[] | undefined;
};
export type MapItem = MapCrudTypes['Item'];
export type PartialMapItem = MapCrudTypes['PartialItem'];
// ----------- GET --------------
export type MapGetIn = GetIn<MapContentType>;
export type MapGetOut = GetResult<
MapItem,
{
outcome: 'exactMatch' | 'aliasMatch' | 'conflict';
aliasTargetId?: string;
aliasPurpose?: 'savedObjectConversion' | 'savedObjectImport';
}
>;
export type MapGetIn = MapCrudTypes['GetIn'];
export type MapGetOut = MapCrudTypes['GetOut'];
// ----------- CREATE --------------
export interface CreateOptions {
/** Array of referenced saved objects. */
references?: Reference[];
}
export type MapCreateIn = CreateIn<MapContentType, MapAttributes, CreateOptions>;
export type MapCreateOut = CreateResult<MapItem>;
export type MapCreateIn = MapCrudTypes['CreateIn'];
export type MapCreateOut = MapCrudTypes['CreateOut'];
export type MapCreateOptions = MapCrudTypes['CreateOptions'];
// ----------- UPDATE --------------
export interface UpdateOptions {
/** Array of referenced saved objects. */
references?: Reference[];
}
export type MapUpdateIn = UpdateIn<MapContentType, MapAttributes, UpdateOptions>;
export type MapUpdateOut = UpdateResult<PartialMapItem>;
export type MapUpdateIn = MapCrudTypes['UpdateIn'];
export type MapUpdateOut = MapCrudTypes['UpdateOut'];
export type MapUpdateOptions = MapCrudTypes['UpdateOptions'];
// ----------- DELETE --------------
export type MapDeleteIn = DeleteIn<MapContentType>;
export type MapDeleteOut = DeleteResult;
export type MapDeleteIn = MapCrudTypes['DeleteIn'];
export type MapDeleteOut = MapCrudTypes['DeleteOut'];
// ----------- SEARCH --------------
export interface MapSearchOptions {
/** Flag to indicate to only search the text on the "title" field */
onlyTitle?: boolean;
}
export type MapSearchIn = SearchIn<MapContentType, MapSearchOptions>;
export type MapSearchOut = SearchResult<MapItem>;
export type MapSearchIn = MapCrudTypes['SearchIn'];
export type MapSearchOut = MapCrudTypes['SearchOut'];
export type MapSearchOptions = MapCrudTypes['SearchOptions'];

View file

@ -19,18 +19,19 @@ import type {
MapSearchOut,
MapSearchOptions,
} 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>({
contentTypeId: 'map',
contentTypeId,
id,
});
};
const create = async ({ data, options }: Omit<MapCreateIn, 'contentTypeId'>) => {
const res = await getContentManagement().client.create<MapCreateIn, MapCreateOut>({
contentTypeId: 'map',
contentTypeId,
data,
options,
});
@ -39,7 +40,7 @@ const create = async ({ data, options }: Omit<MapCreateIn, 'contentTypeId'>) =>
const update = async ({ id, data, options }: Omit<MapUpdateIn, 'contentTypeId'>) => {
const res = await getContentManagement().client.update<MapUpdateIn, MapUpdateOut>({
contentTypeId: 'map',
contentTypeId,
id,
data,
options,
@ -49,14 +50,14 @@ const update = async ({ id, data, options }: Omit<MapUpdateIn, 'contentTypeId'>)
const deleteMap = async (id: string) => {
await getContentManagement().client.delete<MapDeleteIn, MapDeleteOut>({
contentTypeId: 'map',
contentTypeId,
id,
});
};
const search = async (query: SearchQuery = {}, options?: MapSearchOptions) => {
return getContentManagement().client.search<MapSearchIn, MapSearchOut>({
contentTypeId: 'map',
contentTypeId,
query,
options,
});

View file

@ -28,10 +28,10 @@ import type {
MapGetOut,
MapCreateIn,
MapCreateOut,
CreateOptions,
MapCreateOptions,
MapUpdateIn,
MapUpdateOut,
UpdateOptions,
MapUpdateOptions,
MapDeleteOut,
MapSearchOptions,
MapSearchOut,
@ -140,7 +140,7 @@ export class MapsStorage
async create(
ctx: StorageContext,
data: MapCreateIn['data'],
options: CreateOptions
options: MapCreateOptions
): Promise<MapCreateOut> {
const {
utils: { getTransforms },
@ -157,8 +157,8 @@ export class MapsStorage
}
const { value: optionsToLatest, error: optionsError } = transforms.create.in.options.up<
CreateOptions,
CreateOptions
MapCreateOptions,
MapCreateOptions
>(options);
if (optionsError) {
throw Boom.badRequest(`Invalid options. ${optionsError.message}`);
@ -191,7 +191,7 @@ export class MapsStorage
ctx: StorageContext,
id: string,
data: MapUpdateIn['data'],
options: UpdateOptions
options: MapUpdateOptions
): Promise<MapUpdateOut> {
const {
utils: { getTransforms },
@ -208,8 +208,8 @@ export class MapsStorage
}
const { value: optionsToLatest, error: optionsError } = transforms.update.in.options.up<
CreateOptions,
CreateOptions
MapCreateOptions,
MapCreateOptions
>(options);
if (optionsError) {
throw Boom.badRequest(`Invalid options. ${optionsError.message}`);

View file

@ -67,6 +67,7 @@
"@kbn/core-saved-objects-api-server",
"@kbn/object-versioning",
"@kbn/field-types",
"@kbn/content-management-utils",
],
"exclude": [
"target/**/*",

View file

@ -3077,6 +3077,10 @@
version "0.0.0"
uid ""
"@kbn/content-management-utils@link:packages/kbn-content-management-utils":
version "0.0.0"
uid ""
"@kbn/controls-example-plugin@link:examples/controls_example":
version "0.0.0"
uid ""