diff --git a/src/plugins/discover/common/index.ts b/src/plugins/discover/common/index.ts index 63f03655d935b..b30fcf972eda5 100644 --- a/src/plugins/discover/common/index.ts +++ b/src/plugins/discover/common/index.ts @@ -20,3 +20,4 @@ export const MODIFY_COLUMNS_ON_SWITCH = 'discover:modifyColumnsOnSwitch'; export const SEARCH_FIELDS_FROM_SOURCE = 'discover:searchFieldsFromSource'; export const MAX_DOC_FIELDS_DISPLAYED = 'discover:maxDocFieldsDisplayed'; export const SHOW_MULTIFIELDS = 'discover:showMultiFields'; +export const SEARCH_EMBEDDABLE_TYPE = 'search'; diff --git a/src/plugins/discover/kibana.json b/src/plugins/discover/kibana.json index 0e97b16200cca..46eeb5af1470d 100644 --- a/src/plugins/discover/kibana.json +++ b/src/plugins/discover/kibana.json @@ -17,6 +17,7 @@ ], "optionalPlugins": ["home", "share", "usageCollection"], "requiredBundles": ["kibanaUtils", "home", "kibanaReact", "fieldFormats"], + "extraPublicDirs": ["common"], "owner": { "name": "Data Discovery", "githubTeam": "kibana-data-discovery" diff --git a/src/plugins/discover/public/application/embeddable/constants.ts b/src/plugins/discover/public/application/embeddable/constants.ts index 8fe927928065a..57ff91049cd0d 100644 --- a/src/plugins/discover/public/application/embeddable/constants.ts +++ b/src/plugins/discover/public/application/embeddable/constants.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export const SEARCH_EMBEDDABLE_TYPE = 'search'; +export { SEARCH_EMBEDDABLE_TYPE } from '../../../common/index'; diff --git a/src/plugins/embeddable/kibana.json b/src/plugins/embeddable/kibana.json index 7ef6896d53a2a..276d17db3e88e 100644 --- a/src/plugins/embeddable/kibana.json +++ b/src/plugins/embeddable/kibana.json @@ -8,5 +8,6 @@ "githubTeam": "kibana-app-services" }, "requiredPlugins": ["inspector", "uiActions"], + "extraPublicDirs": ["common"], "requiredBundles": ["savedObjects", "kibanaReact", "kibanaUtils"] } diff --git a/src/plugins/expressions/common/service/expressions_services.ts b/src/plugins/expressions/common/service/expressions_services.ts index b4dda3de5c93c..2be4f5207bb82 100644 --- a/src/plugins/expressions/common/service/expressions_services.ts +++ b/src/plugins/expressions/common/service/expressions_services.ts @@ -54,6 +54,8 @@ export type ExpressionsServiceSetup = Pick< | 'registerType' | 'run' | 'fork' + | 'extract' + | 'inject' >; export interface ExpressionExecutionParams { diff --git a/src/plugins/expressions/public/mocks.tsx b/src/plugins/expressions/public/mocks.tsx index 84287aefe046b..3a5450fc02837 100644 --- a/src/plugins/expressions/public/mocks.tsx +++ b/src/plugins/expressions/public/mocks.tsx @@ -16,6 +16,7 @@ export type Start = jest.Mocked; const createSetupContract = (): Setup => { const setupContract: Setup = { + extract: jest.fn(), fork: jest.fn(), getFunction: jest.fn(), getFunctions: jest.fn(), @@ -23,6 +24,7 @@ const createSetupContract = (): Setup => { getRenderers: jest.fn(), getType: jest.fn(), getTypes: jest.fn(), + inject: jest.fn(), registerFunction: jest.fn(), registerRenderer: jest.fn(), registerType: jest.fn(), diff --git a/src/plugins/expressions/server/mocks.ts b/src/plugins/expressions/server/mocks.ts index 9bc25d89a04ca..f4379145f6a6c 100644 --- a/src/plugins/expressions/server/mocks.ts +++ b/src/plugins/expressions/server/mocks.ts @@ -15,6 +15,7 @@ export type Start = jest.Mocked; const createSetupContract = (): Setup => { const setupContract: Setup = { + extract: jest.fn(), fork: jest.fn(), getFunction: jest.fn(), getFunctions: jest.fn(), @@ -22,6 +23,7 @@ const createSetupContract = (): Setup => { getRenderers: jest.fn(), getType: jest.fn(), getTypes: jest.fn(), + inject: jest.fn(), registerFunction: jest.fn(), registerRenderer: jest.fn(), registerType: jest.fn(), diff --git a/src/plugins/visualizations/common/constants.ts b/src/plugins/visualizations/common/constants.ts index a8a0963ac8948..96f97b4b81936 100644 --- a/src/plugins/visualizations/common/constants.ts +++ b/src/plugins/visualizations/common/constants.ts @@ -7,3 +7,4 @@ */ export const VISUALIZE_ENABLE_LABS_SETTING = 'visualize:enableLabs'; +export const VISUALIZE_EMBEDDABLE_TYPE = 'visualization'; diff --git a/src/plugins/visualizations/public/embeddable/constants.ts b/src/plugins/visualizations/public/embeddable/constants.ts index 7bee811cddd6c..cec3bd6cdfc88 100644 --- a/src/plugins/visualizations/public/embeddable/constants.ts +++ b/src/plugins/visualizations/public/embeddable/constants.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export const VISUALIZE_EMBEDDABLE_TYPE = 'visualization'; +export { VISUALIZE_EMBEDDABLE_TYPE } from '../../common/constants'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts b/x-pack/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts index 76383007adb7b..ac2e8e8babee1 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts @@ -6,7 +6,7 @@ */ import { ExpressionTypeDefinition } from '../../../../../src/plugins/expressions'; -import { EmbeddableInput } from '../../../../../src/plugins/embeddable/public'; +import { EmbeddableInput } from '../../../../../src/plugins/embeddable/common/'; import { EmbeddableTypes } from './embeddable_types'; export const EmbeddableExpressionType = 'embeddable'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/expression_types/embeddable_types.ts b/x-pack/plugins/canvas/canvas_plugin_src/expression_types/embeddable_types.ts index 1e340567825b9..78fc82393994b 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/expression_types/embeddable_types.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/expression_types/embeddable_types.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { MAP_SAVED_OBJECT_TYPE } from '../../../../plugins/maps/public'; -import { VISUALIZE_EMBEDDABLE_TYPE } from '../../../../../src/plugins/visualizations/public'; +import { MAP_SAVED_OBJECT_TYPE } from '../../../../plugins/maps/common/constants'; +import { VISUALIZE_EMBEDDABLE_TYPE } from '../../../../../src/plugins/visualizations/common/constants'; import { LENS_EMBEDDABLE_TYPE } from '../../../../plugins/lens/common/constants'; -import { SEARCH_EMBEDDABLE_TYPE } from '../../../../../src/plugins/discover/public'; +import { SEARCH_EMBEDDABLE_TYPE } from '../../../../../src/plugins/discover/common'; export const EmbeddableTypes: { lens: string; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.test.ts index 55ad7fd4d3cef..e3e6afae8bd69 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.test.ts @@ -6,7 +6,7 @@ */ import { savedLens } from './saved_lens'; -import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; +import { getQueryFilters } from '../../../common/lib/build_embeddable_filters'; import { ExpressionValueFilter } from '../../../types'; const filterContext: ExpressionValueFilter = { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts index 3ffa20de55aaf..bd844dd3335ef 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts @@ -7,9 +7,10 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { PaletteOutput } from 'src/plugins/charts/common'; -import { TimeRange, Filter as DataFilter } from 'src/plugins/data/public'; -import { EmbeddableInput } from 'src/plugins/embeddable/public'; -import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; +import { Filter as DataFilter } from '@kbn/es-query'; +import { TimeRange } from 'src/plugins/data/common'; +import { EmbeddableInput } from 'src/plugins/embeddable/common'; +import { getQueryFilters } from '../../../common/lib/build_embeddable_filters'; import { ExpressionValueFilter, TimeRange as TimeRangeArg } from '../../../types'; import { EmbeddableTypes, @@ -18,7 +19,6 @@ import { } from '../../expression_types'; import { getFunctionHelp } from '../../../i18n'; import { SavedObjectReference } from '../../../../../../src/core/types'; - interface Arguments { id: string; title: string | null; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.test.ts index 02c0af2984520..88acf0deabb35 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.test.ts @@ -6,7 +6,7 @@ */ import { savedMap } from './saved_map'; -import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; +import { getQueryFilters } from '../../../common/lib/build_embeddable_filters'; import { ExpressionValueFilter } from '../../../types'; const filterContext: ExpressionValueFilter = { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.ts index 395c6e112f753..538ed3f919823 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.ts @@ -6,7 +6,7 @@ */ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; -import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; +import { getQueryFilters } from '../../../common/lib/build_embeddable_filters'; import { ExpressionValueFilter, MapCenter, TimeRange as TimeRangeArg } from '../../../types'; import { EmbeddableTypes, @@ -14,7 +14,7 @@ import { EmbeddableExpression, } from '../../expression_types'; import { getFunctionHelp } from '../../../i18n'; -import { MapEmbeddableInput } from '../../../../../plugins/maps/public/embeddable'; +import { MapEmbeddableInput } from '../../../../../plugins/maps/public'; import { SavedObjectReference } from '../../../../../../src/core/types'; interface Arguments { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_search.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_search.test.ts index f7cf8c47803ac..da0ffcd8f3886 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_search.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_search.test.ts @@ -6,7 +6,7 @@ */ import { savedSearch } from './saved_search'; -import { buildEmbeddableFilters } from '../../../public/lib/build_embeddable_filters'; +import { buildEmbeddableFilters } from '../../../common/lib/build_embeddable_filters'; import { ExpressionValueFilter } from '../../../types'; const filterContext: ExpressionValueFilter = { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_search.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_search.ts index 37c49b62786c1..d709622055d10 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_search.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_search.ts @@ -13,7 +13,7 @@ import { EmbeddableExpression, } from '../../expression_types'; -import { buildEmbeddableFilters } from '../../../public/lib/build_embeddable_filters'; +import { buildEmbeddableFilters } from '../../../common/lib/build_embeddable_filters'; import { ExpressionValueFilter } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; import { SavedObjectReference } from '../../../../../../src/core/types'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.test.ts index 3b1d431f2d641..52c452e61bd55 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.test.ts @@ -6,7 +6,7 @@ */ import { savedVisualization } from './saved_visualization'; -import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; +import { getQueryFilters } from '../../../common/lib/build_embeddable_filters'; import { ExpressionValueFilter } from '../../../types'; const filterContext: ExpressionValueFilter = { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.ts index 64252fbf92b45..d2d93f1633e20 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.ts @@ -12,7 +12,7 @@ import { EmbeddableExpressionType, EmbeddableExpression, } from '../../expression_types'; -import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; +import { getQueryFilters } from '../../../common/lib/build_embeddable_filters'; import { ExpressionValueFilter, TimeRange as TimeRangeArg, SeriesStyle } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; import { SavedObjectReference } from '../../../../../../src/core/types'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts b/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts index a30b3bf9b2121..91c573fc4148b 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts @@ -15,6 +15,7 @@ import { Start as InspectorStart } from '../../../../src/plugins/inspector/publi import { functions } from './functions/browser'; import { typeFunctions } from './expression_types'; import { renderFunctions, renderFunctionFactories } from './renderers'; + interface SetupDeps { canvas: CanvasSetup; } diff --git a/x-pack/plugins/canvas/public/lib/build_bool_array.js b/x-pack/plugins/canvas/common/lib/build_bool_array.js similarity index 100% rename from x-pack/plugins/canvas/public/lib/build_bool_array.js rename to x-pack/plugins/canvas/common/lib/build_bool_array.js diff --git a/x-pack/plugins/canvas/public/lib/build_embeddable_filters.test.ts b/x-pack/plugins/canvas/common/lib/build_embeddable_filters.test.ts similarity index 100% rename from x-pack/plugins/canvas/public/lib/build_embeddable_filters.test.ts rename to x-pack/plugins/canvas/common/lib/build_embeddable_filters.test.ts diff --git a/x-pack/plugins/canvas/public/lib/build_embeddable_filters.ts b/x-pack/plugins/canvas/common/lib/build_embeddable_filters.ts similarity index 95% rename from x-pack/plugins/canvas/public/lib/build_embeddable_filters.ts rename to x-pack/plugins/canvas/common/lib/build_embeddable_filters.ts index 9dc9357719257..57fdc7d7309ce 100644 --- a/x-pack/plugins/canvas/public/lib/build_embeddable_filters.ts +++ b/x-pack/plugins/canvas/common/lib/build_embeddable_filters.ts @@ -9,7 +9,7 @@ import { buildQueryFilter, Filter } from '@kbn/es-query'; import { ExpressionValueFilter } from '../../types'; // @ts-expect-error untyped local import { buildBoolArray } from './build_bool_array'; -import { TimeRange } from '../../../../../src/plugins/data/public'; +import { TimeRange } from '../../../../../src/plugins/data/common'; export interface EmbeddableFilterInput { filters: Filter[]; diff --git a/x-pack/plugins/canvas/public/lib/filters.js b/x-pack/plugins/canvas/common/lib/filters.js similarity index 100% rename from x-pack/plugins/canvas/public/lib/filters.js rename to x-pack/plugins/canvas/common/lib/filters.js diff --git a/x-pack/plugins/canvas/public/lib/get_es_filter.js b/x-pack/plugins/canvas/common/lib/get_es_filter.js similarity index 100% rename from x-pack/plugins/canvas/public/lib/get_es_filter.js rename to x-pack/plugins/canvas/common/lib/get_es_filter.js diff --git a/x-pack/plugins/canvas/public/functions/timelion.ts b/x-pack/plugins/canvas/public/functions/timelion.ts index 99bd1b72434c8..81893db1bbd4b 100644 --- a/x-pack/plugins/canvas/public/functions/timelion.ts +++ b/x-pack/plugins/canvas/public/functions/timelion.ts @@ -13,7 +13,7 @@ import { TimeRange } from 'src/plugins/data/common'; import { ExpressionFunctionDefinition, DatatableRow } from 'src/plugins/expressions/public'; import { fetch } from '../../common/lib/fetch'; // @ts-expect-error untyped local -import { buildBoolArray } from '../../public/lib/build_bool_array'; +import { buildBoolArray } from '../../common/lib/build_bool_array'; import { Datatable, ExpressionValueFilter } from '../../types'; import { getFunctionHelp } from '../../i18n'; import { InitializeArguments } from './'; diff --git a/x-pack/plugins/canvas/server/mocks/index.ts b/x-pack/plugins/canvas/server/mocks/index.ts new file mode 100644 index 0000000000000..1cb39c690df97 --- /dev/null +++ b/x-pack/plugins/canvas/server/mocks/index.ts @@ -0,0 +1,8 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { workpadRouteContextMock, MockWorkpadRouteContext } from './workpad_route_context'; diff --git a/x-pack/plugins/canvas/server/mocks/workpad_route_context.ts b/x-pack/plugins/canvas/server/mocks/workpad_route_context.ts new file mode 100644 index 0000000000000..abba97639a4c9 --- /dev/null +++ b/x-pack/plugins/canvas/server/mocks/workpad_route_context.ts @@ -0,0 +1,28 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CanvasRouteHandlerContext } from '../workpad_route_context'; + +export interface MockWorkpadRouteContext extends CanvasRouteHandlerContext { + canvas: { + workpad: { + create: jest.Mock; + get: jest.Mock; + update: jest.Mock; + }; + }; +} + +export const workpadRouteContextMock = { + create: (): MockWorkpadRouteContext['canvas'] => ({ + workpad: { + create: jest.fn(), + get: jest.fn(), + update: jest.fn(), + }, + }), +}; diff --git a/x-pack/plugins/canvas/server/plugin.ts b/x-pack/plugins/canvas/server/plugin.ts index f0b7c0243000f..35b1d0025ea5f 100644 --- a/x-pack/plugins/canvas/server/plugin.ts +++ b/x-pack/plugins/canvas/server/plugin.ts @@ -26,6 +26,7 @@ import { customElementType, workpadType, workpadTemplateType } from './saved_obj import { initializeTemplates } from './templates'; import { essqlSearchStrategyProvider } from './lib/essql_strategy'; import { getUISettings } from './ui_settings'; +import { CanvasRouteHandlerContext, createWorkpadRouteContext } from './workpad_route_context'; interface PluginsSetup { expressions: ExpressionsServerSetup; @@ -48,6 +49,8 @@ export class CanvasPlugin implements Plugin { } public setup(coreSetup: CoreSetup, plugins: PluginsSetup) { + const expressionsFork = plugins.expressions.fork(); + coreSetup.uiSettings.register(getUISettings()); coreSetup.savedObjects.registerType(customElementType); coreSetup.savedObjects.registerType(workpadType); @@ -55,11 +58,17 @@ export class CanvasPlugin implements Plugin { plugins.features.registerKibanaFeature(getCanvasFeature(plugins)); - const canvasRouter = coreSetup.http.createRouter(); + const contextProvider = createWorkpadRouteContext({ expressions: expressionsFork }); + coreSetup.http.registerRouteHandlerContext( + 'canvas', + contextProvider + ); + + const canvasRouter = coreSetup.http.createRouter(); initRoutes({ router: canvasRouter, - expressions: plugins.expressions, + expressions: expressionsFork, bfetch: plugins.bfetch, logger: this.logger, }); @@ -73,7 +82,7 @@ export class CanvasPlugin implements Plugin { const globalConfig = this.initializerContext.config.legacy.get(); registerCanvasUsageCollector(plugins.usageCollection, globalConfig.kibana.index); - setupInterpreter(plugins.expressions); + setupInterpreter(expressionsFork); coreSetup.getStartServices().then(([_, depsStart]) => { const strategy = essqlSearchStrategyProvider(); diff --git a/x-pack/plugins/canvas/server/routes/catch_error_handler.ts b/x-pack/plugins/canvas/server/routes/catch_error_handler.ts index b1fe4bc798f6b..f565c911274f8 100644 --- a/x-pack/plugins/canvas/server/routes/catch_error_handler.ts +++ b/x-pack/plugins/canvas/server/routes/catch_error_handler.ts @@ -5,11 +5,11 @@ * 2.0. */ -import { RequestHandler } from 'src/core/server'; +import { RequestHandler, RequestHandlerContext } from 'src/core/server'; -export const catchErrorHandler: ( - fn: RequestHandler -) => RequestHandler = (fn) => { +export const catchErrorHandler: ( + fn: RequestHandler +) => RequestHandler = (fn) => { return async (context, request, response) => { try { return await fn(context, request, response); diff --git a/x-pack/plugins/canvas/server/routes/index.ts b/x-pack/plugins/canvas/server/routes/index.ts index ccc8f7e278266..b7ba5637119f8 100644 --- a/x-pack/plugins/canvas/server/routes/index.ts +++ b/x-pack/plugins/canvas/server/routes/index.ts @@ -14,9 +14,10 @@ import { initShareablesRoutes } from './shareables'; import { initWorkpadRoutes } from './workpad'; import { initTemplateRoutes } from './templates'; import { initFunctionsRoutes } from './functions'; +import { CanvasRouteHandlerContext } from '../workpad_route_context'; export interface RouteInitializerDeps { - router: IRouter; + router: IRouter; logger: Logger; expressions: ExpressionsServerSetup; bfetch: BfetchServerSetup; diff --git a/x-pack/plugins/canvas/server/routes/workpad/create.test.ts b/x-pack/plugins/canvas/server/routes/workpad/create.test.ts index f6a16a2a20af7..158c68f0ea0a0 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/create.test.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/create.test.ts @@ -5,11 +5,10 @@ * 2.0. */ -import sinon from 'sinon'; import { savedObjectsClientMock, httpServerMock } from 'src/core/server/mocks'; -import { CANVAS_TYPE } from '../../../common/lib/constants'; +import { workpadRouteContextMock, MockWorkpadRouteContext } from '../../mocks'; import { initializeCreateWorkpadRoute } from './create'; -import { kibanaResponseFactory, RequestHandlerContext, RequestHandler } from 'src/core/server'; +import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; import { getMockedRouterDeps } from '../test_helpers'; let mockRouteContext = ({ @@ -18,17 +17,13 @@ let mockRouteContext = ({ client: savedObjectsClientMock.create(), }, }, -} as unknown) as RequestHandlerContext; - -const mockedUUID = '123abc'; -const now = new Date(); -const nowIso = now.toISOString(); + canvas: workpadRouteContextMock.create(), +} as unknown) as MockWorkpadRouteContext; jest.mock('uuid/v4', () => jest.fn().mockReturnValue('123abc')); describe('POST workpad', () => { let routeHandler: RequestHandler; - let clock: sinon.SinonFakeTimers; beforeEach(() => { mockRouteContext = ({ @@ -37,9 +32,8 @@ describe('POST workpad', () => { client: savedObjectsClientMock.create(), }, }, - } as unknown) as RequestHandlerContext; - - clock = sinon.useFakeTimers(now); + canvas: workpadRouteContextMock.create(), + } as unknown) as MockWorkpadRouteContext; const routerDeps = getMockedRouterDeps(); initializeCreateWorkpadRoute(routerDeps); @@ -47,11 +41,12 @@ describe('POST workpad', () => { routeHandler = routerDeps.router.post.mock.calls[0][1]; }); - afterEach(() => { - clock.restore(); - }); - it(`returns 200 when the workpad is created`, async () => { + const id = 'my-id'; + mockRouteContext.canvas.workpad.create.mockResolvedValue({ + id, + }); + const mockWorkpad = { pages: [], }; @@ -65,18 +60,8 @@ describe('POST workpad', () => { const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); expect(response.status).toBe(200); - expect(response.payload).toEqual({ ok: true, id: `workpad-${mockedUUID}` }); - expect(mockRouteContext.core.savedObjects.client.create).toBeCalledWith( - CANVAS_TYPE, - { - ...mockWorkpad, - '@timestamp': nowIso, - '@created': nowIso, - }, - { - id: `workpad-${mockedUUID}`, - } - ); + expect(response.payload).toEqual({ ok: true, id }); + expect(mockRouteContext.canvas.workpad.create).toBeCalledWith(mockWorkpad); }); it(`returns bad request if create is unsuccessful`, async () => { @@ -86,7 +71,7 @@ describe('POST workpad', () => { body: {}, }); - (mockRouteContext.core.savedObjects.client.create as jest.Mock).mockImplementation(() => { + mockRouteContext.canvas.workpad.create.mockImplementation(() => { throw mockRouteContext.core.savedObjects.client.errors.createBadRequestError('bad request'); }); @@ -109,6 +94,11 @@ describe('POST workpad', () => { }, }; + const id = 'my-id'; + mockRouteContext.canvas.workpad.create.mockResolvedValue({ + id, + }); + (mockRouteContext.core.savedObjects.client.get as jest.Mock).mockResolvedValue( mockTemplateResponse ); @@ -122,17 +112,9 @@ describe('POST workpad', () => { const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); expect(response.status).toBe(200); - expect(response.payload).toEqual({ ok: true, id: `workpad-${mockedUUID}` }); - expect(mockRouteContext.core.savedObjects.client.create).toBeCalledWith( - CANVAS_TYPE, - { - ...mockTemplateResponse.attributes.template, - '@timestamp': nowIso, - '@created': nowIso, - }, - { - id: `workpad-${mockedUUID}`, - } + expect(response.payload).toEqual({ ok: true, id }); + expect(mockRouteContext.canvas.workpad.create).toBeCalledWith( + mockTemplateResponse.attributes.template ); }); }); diff --git a/x-pack/plugins/canvas/server/routes/workpad/create.ts b/x-pack/plugins/canvas/server/routes/workpad/create.ts index 1fa8ab4412aca..2a0c47fee4c9e 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/create.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/create.ts @@ -7,10 +7,8 @@ import { schema } from '@kbn/config-schema'; import { RouteInitializerDeps } from '../'; -import { CANVAS_TYPE, API_ROUTE_WORKPAD, TEMPLATE_TYPE } from '../../../common/lib/constants'; +import { API_ROUTE_WORKPAD, TEMPLATE_TYPE } from '../../../common/lib/constants'; import { CanvasWorkpad } from '../../../types'; -import { getId } from '../../../common/lib/get_id'; -import { WorkpadAttributes } from './workpad_attributes'; import { WorkpadSchema } from './workpad_schema'; import { okResponse } from '../ok_response'; import { catchErrorHandler } from '../catch_error_handler'; @@ -59,23 +57,10 @@ export function initializeCreateWorkpadRoute(deps: RouteInitializerDeps) { workpad = templateSavedObject.attributes.template; } - const now = new Date().toISOString(); - const { id: maybeId, ...payload } = workpad; - - const id = maybeId ? maybeId : getId('workpad'); - - await context.core.savedObjects.client.create( - CANVAS_TYPE, - { - ...payload, - '@timestamp': now, - '@created': now, - }, - { id } - ); + const createdObject = await context.canvas.workpad.create(workpad); return response.ok({ - body: { ...okResponse, id }, + body: { ...okResponse, id: createdObject.id }, }); }) ); diff --git a/x-pack/plugins/canvas/server/routes/workpad/get.test.ts b/x-pack/plugins/canvas/server/routes/workpad/get.test.ts index 5934ece23448f..e19a20cc3f543 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/get.test.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/get.test.ts @@ -7,19 +7,16 @@ import { CANVAS_TYPE } from '../../../common/lib/constants'; import { initializeGetWorkpadRoute } from './get'; -import { kibanaResponseFactory, RequestHandlerContext, RequestHandler } from 'src/core/server'; +import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; import { savedObjectsClientMock, httpServerMock } from 'src/core/server/mocks'; import { workpadWithGroupAsElement } from '../../../__fixtures__/workpads'; import { CanvasWorkpad } from '../../../types'; import { getMockedRouterDeps } from '../test_helpers'; +import { workpadRouteContextMock, MockWorkpadRouteContext } from '../../mocks'; const mockRouteContext = ({ - core: { - savedObjects: { - client: savedObjectsClientMock.create(), - }, - }, -} as unknown) as RequestHandlerContext; + canvas: workpadRouteContextMock.create(), +} as unknown) as MockWorkpadRouteContext; describe('GET workpad', () => { let routeHandler: RequestHandler; @@ -31,6 +28,10 @@ describe('GET workpad', () => { routeHandler = routerDeps.router.get.mock.calls[0][1]; }); + afterEach(() => { + jest.resetAllMocks(); + }); + it(`returns 200 when the workpad is found`, async () => { const request = httpServerMock.createKibanaRequest({ method: 'get', @@ -40,16 +41,13 @@ describe('GET workpad', () => { }, }); - const savedObjectsClient = savedObjectsClientMock.create(); - savedObjectsClient.get.mockResolvedValueOnce({ + mockRouteContext.canvas.workpad.get.mockResolvedValue({ id: '123', type: CANVAS_TYPE, attributes: { foo: true }, references: [], }); - mockRouteContext.core.savedObjects.client = savedObjectsClient; - const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); expect(response.status).toBe(200); @@ -60,10 +58,9 @@ describe('GET workpad', () => { } `); - expect(savedObjectsClient.get.mock.calls).toMatchInlineSnapshot(` + expect(mockRouteContext.canvas.workpad.get.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "canvas-workpad", "123", ], ] @@ -79,16 +76,13 @@ describe('GET workpad', () => { }, }); - const savedObjectsClient = savedObjectsClientMock.create(); - savedObjectsClient.get.mockResolvedValueOnce({ + mockRouteContext.canvas.workpad.get.mockResolvedValue({ id: '123', type: CANVAS_TYPE, attributes: workpadWithGroupAsElement as any, references: [], }); - mockRouteContext.core.savedObjects.client = savedObjectsClient; - const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); const workpad = response.payload as CanvasWorkpad; @@ -110,10 +104,9 @@ describe('GET workpad', () => { }); const savedObjectsClient = savedObjectsClientMock.create(); - savedObjectsClient.get.mockImplementation(() => { + mockRouteContext.canvas.workpad.get.mockImplementation(() => { throw savedObjectsClient.errors.createGenericNotFoundError(CANVAS_TYPE, id); }); - mockRouteContext.core.savedObjects.client = savedObjectsClient; const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); diff --git a/x-pack/plugins/canvas/server/routes/workpad/get.ts b/x-pack/plugins/canvas/server/routes/workpad/get.ts index 19c01ae1f01ef..ff3ed4bad55b9 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/get.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/get.ts @@ -7,8 +7,7 @@ import { schema } from '@kbn/config-schema'; import { RouteInitializerDeps } from '../'; -import { CANVAS_TYPE, API_ROUTE_WORKPAD } from '../../../common/lib/constants'; -import { WorkpadAttributes } from './workpad_attributes'; +import { API_ROUTE_WORKPAD } from '../../../common/lib/constants'; import { catchErrorHandler } from '../catch_error_handler'; export function initializeGetWorkpadRoute(deps: RouteInitializerDeps) { @@ -23,10 +22,7 @@ export function initializeGetWorkpadRoute(deps: RouteInitializerDeps) { }, }, catchErrorHandler(async (context, request, response) => { - const workpad = await context.core.savedObjects.client.get( - CANVAS_TYPE, - request.params.id - ); + const workpad = await context.canvas.workpad.get(request.params.id); if ( // not sure if we need to be this defensive diff --git a/x-pack/plugins/canvas/server/routes/workpad/update.test.ts b/x-pack/plugins/canvas/server/routes/workpad/update.test.ts index 1f3143a5d93d1..8feadf433f07f 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/update.test.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/update.test.ts @@ -8,11 +8,12 @@ import sinon from 'sinon'; import { CANVAS_TYPE } from '../../../common/lib/constants'; import { initializeUpdateWorkpadRoute, initializeUpdateWorkpadAssetsRoute } from './update'; -import { kibanaResponseFactory, RequestHandlerContext, RequestHandler } from 'src/core/server'; +import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; import { savedObjectsClientMock, httpServerMock } from 'src/core/server/mocks'; import { workpads } from '../../../__fixtures__/workpads'; import { okResponse } from '../ok_response'; import { getMockedRouterDeps } from '../test_helpers'; +import { workpadRouteContextMock, MockWorkpadRouteContext } from '../../mocks'; const mockRouteContext = ({ core: { @@ -20,11 +21,11 @@ const mockRouteContext = ({ client: savedObjectsClientMock.create(), }, }, -} as unknown) as RequestHandlerContext; + canvas: workpadRouteContextMock.create(), +} as unknown) as MockWorkpadRouteContext; const workpad = workpads[0]; const now = new Date(); -const nowIso = now.toISOString(); jest.mock('uuid/v4', () => jest.fn().mockReturnValue('123abc')); @@ -48,7 +49,7 @@ describe('PUT workpad', () => { it(`returns 200 ok when the workpad is updated`, async () => { const updatedWorkpad = { name: 'new name' }; - const { id, ...workpadAttributes } = workpad; + const { id } = workpad; const request = httpServerMock.createKibanaRequest({ method: 'put', @@ -59,33 +60,13 @@ describe('PUT workpad', () => { body: updatedWorkpad, }); - const savedObjectsClient = savedObjectsClientMock.create(); - savedObjectsClient.get.mockResolvedValueOnce({ - id, - type: CANVAS_TYPE, - attributes: workpadAttributes as any, - references: [], - }); - - mockRouteContext.core.savedObjects.client = savedObjectsClient; + mockRouteContext.canvas.workpad.update.mockResolvedValue(true); const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); expect(response.status).toBe(200); expect(response.payload).toEqual(okResponse); - expect(mockRouteContext.core.savedObjects.client.create).toBeCalledWith( - CANVAS_TYPE, - { - ...workpadAttributes, - ...updatedWorkpad, - '@timestamp': nowIso, - '@created': workpad['@created'], - }, - { - overwrite: true, - id, - } - ); + expect(mockRouteContext.canvas.workpad.update).toBeCalledWith(id, updatedWorkpad); }); it(`returns not found if existing workpad is not found`, async () => { @@ -98,7 +79,7 @@ describe('PUT workpad', () => { body: {}, }); - (mockRouteContext.core.savedObjects.client.get as jest.Mock).mockImplementationOnce(() => { + mockRouteContext.canvas.workpad.update.mockImplementationOnce(() => { throw mockRouteContext.core.savedObjects.client.errors.createGenericNotFoundError( 'not found' ); @@ -119,17 +100,7 @@ describe('PUT workpad', () => { body: {}, }); - const savedObjectsClient = savedObjectsClientMock.create(); - savedObjectsClient.get.mockResolvedValueOnce({ - id: 'some-id', - type: CANVAS_TYPE, - attributes: {}, - references: [], - }); - - mockRouteContext.core.savedObjects.client = savedObjectsClient; - - (mockRouteContext.core.savedObjects.client.create as jest.Mock).mockImplementationOnce(() => { + mockRouteContext.canvas.workpad.update.mockImplementationOnce(() => { throw mockRouteContext.core.savedObjects.client.errors.createBadRequestError('bad request'); }); @@ -182,7 +153,7 @@ describe('update assets', () => { body: assets, }); - (mockRouteContext.core.savedObjects.client.get as jest.Mock).mockResolvedValueOnce({ + mockRouteContext.canvas.workpad.update.mockResolvedValueOnce({ id, type: CANVAS_TYPE, attributes: attributes as any, @@ -192,17 +163,8 @@ describe('update assets', () => { const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); expect(response.status).toBe(200); - expect(mockRouteContext.core.savedObjects.client.create).toBeCalledWith( - CANVAS_TYPE, - { - ...attributes, - '@timestamp': nowIso, - assets, - }, - { - id, - overwrite: true, - } - ); + expect(mockRouteContext.canvas.workpad.update).toBeCalledWith(id, { + assets, + }); }); }); diff --git a/x-pack/plugins/canvas/server/routes/workpad/update.ts b/x-pack/plugins/canvas/server/routes/workpad/update.ts index bb1a347437c03..2fe3c8fc9e3ed 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/update.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/update.ts @@ -5,13 +5,10 @@ * 2.0. */ -import { schema, TypeOf } from '@kbn/config-schema'; -import { omit } from 'lodash'; -import { KibanaResponseFactory, SavedObjectsClientContract } from 'src/core/server'; +import { schema } from '@kbn/config-schema'; import { CanvasWorkpad } from '../../../types'; import { RouteInitializerDeps } from '../'; import { - CANVAS_TYPE, API_ROUTE_WORKPAD, API_ROUTE_WORKPAD_STRUCTURES, API_ROUTE_WORKPAD_ASSETS, @@ -22,35 +19,6 @@ import { catchErrorHandler } from '../catch_error_handler'; const AssetsRecordSchema = schema.recordOf(schema.string(), WorkpadAssetSchema); -const AssetPayloadSchema = schema.object({ - assets: AssetsRecordSchema, -}); - -const workpadUpdateHandler = async ( - payload: TypeOf | TypeOf, - id: string, - savedObjectsClient: SavedObjectsClientContract, - response: KibanaResponseFactory -) => { - const now = new Date().toISOString(); - - const workpadObject = await savedObjectsClient.get(CANVAS_TYPE, id); - await savedObjectsClient.create( - CANVAS_TYPE, - { - ...workpadObject.attributes, - ...omit(payload, 'id'), // never write the id property - '@timestamp': now, // always update the modified time - '@created': workpadObject.attributes['@created'], // ensure created is not modified - }, - { overwrite: true, id } - ); - - return response.ok({ - body: okResponse, - }); -}; - export function initializeUpdateWorkpadRoute(deps: RouteInitializerDeps) { const { router } = deps; // TODO: This route is likely deprecated and everything is using the workpad_structures @@ -72,12 +40,11 @@ export function initializeUpdateWorkpadRoute(deps: RouteInitializerDeps) { }, }, catchErrorHandler(async (context, request, response) => { - return workpadUpdateHandler( - request.body, - request.params.id, - context.core.savedObjects.client, - response - ); + await context.canvas.workpad.update(request.params.id, request.body as CanvasWorkpad); + + return response.ok({ + body: okResponse, + }); }) ); @@ -98,12 +65,11 @@ export function initializeUpdateWorkpadRoute(deps: RouteInitializerDeps) { }, }, catchErrorHandler(async (context, request, response) => { - return workpadUpdateHandler( - request.body, - request.params.id, - context.core.savedObjects.client, - response - ); + await context.canvas.workpad.update(request.params.id, request.body as CanvasWorkpad); + + return response.ok({ + body: okResponse, + }); }) ); } @@ -131,12 +97,15 @@ export function initializeUpdateWorkpadAssetsRoute(deps: RouteInitializerDeps) { }, }, async (context, request, response) => { - return workpadUpdateHandler( - { assets: AssetsRecordSchema.validate(request.body) }, - request.params.id, - context.core.savedObjects.client, - response - ); + const workpadAssets = { + assets: AssetsRecordSchema.validate(request.body), + }; + + await context.canvas.workpad.update(request.params.id, workpadAssets as CanvasWorkpad); + + return response.ok({ + body: okResponse, + }); } ); } diff --git a/x-pack/plugins/canvas/server/saved_objects/workpad_references.ts b/x-pack/plugins/canvas/server/saved_objects/workpad_references.ts new file mode 100644 index 0000000000000..b0d20add2f79a --- /dev/null +++ b/x-pack/plugins/canvas/server/saved_objects/workpad_references.ts @@ -0,0 +1,68 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { fromExpression, toExpression } from '@kbn/interpreter/common'; +import { SavedObjectReference } from '../../../../../src/core/server'; +import { WorkpadAttributes } from '../routes/workpad/workpad_attributes'; + +import { ExpressionsServerSetup } from '../../../../../src/plugins/expressions/server'; + +export const extractReferences = ( + workpad: WorkpadAttributes, + expressions: ExpressionsServerSetup +): { workpad: WorkpadAttributes; references: SavedObjectReference[] } => { + // We need to find every element in the workpad and extract references + const references: SavedObjectReference[] = []; + + const pages = workpad.pages.map((page) => { + const elements = page.elements.map((element) => { + const extract = expressions.extract(fromExpression(element.expression)); + + // Prefix references with the element id so we will know later which element it goes with + references.push( + ...extract.references.map((reference) => ({ + ...reference, + name: `${element.id}:${reference.name}`, + })) + ); + + return { ...element, expression: toExpression(extract.state) }; + }); + + return { ...page, elements }; + }); + + return { workpad: { ...workpad, pages }, references }; +}; + +export const injectReferences = ( + workpad: WorkpadAttributes, + references: SavedObjectReference[], + expressions: ExpressionsServerSetup +) => { + const pages = workpad.pages.map((page) => { + const elements = page.elements.map((element) => { + const referencesForElement = references + .filter(({ name }) => name.indexOf(element.id) === 0) + .map((reference) => ({ + ...reference, + name: reference.name.replace(`${element.id}:`, ''), + })); + + const injectedAst = expressions.inject( + fromExpression(element.expression), + referencesForElement + ); + + return { ...element, expression: toExpression(injectedAst) }; + }); + + return { ...page, elements }; + }); + + return { ...workpad, pages }; +}; diff --git a/x-pack/plugins/canvas/server/setup_interpreter.ts b/x-pack/plugins/canvas/server/setup_interpreter.ts index b61689d7c861c..2fe23eb86c086 100644 --- a/x-pack/plugins/canvas/server/setup_interpreter.ts +++ b/x-pack/plugins/canvas/server/setup_interpreter.ts @@ -7,7 +7,9 @@ import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { functions } from '../canvas_plugin_src/functions/server'; +import { functions as externalFunctions } from '../canvas_plugin_src/functions/external'; export function setupInterpreter(expressions: ExpressionsServerSetup) { functions.forEach((f) => expressions.registerFunction(f)); + externalFunctions.forEach((f) => expressions.registerFunction(f)); } diff --git a/x-pack/plugins/canvas/server/workpad_route_context.test.ts b/x-pack/plugins/canvas/server/workpad_route_context.test.ts new file mode 100644 index 0000000000000..ec09a364b594a --- /dev/null +++ b/x-pack/plugins/canvas/server/workpad_route_context.test.ts @@ -0,0 +1,216 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import sinon from 'sinon'; +import { fromExpression } from '@kbn/interpreter/common'; +import { createWorkpadRouteContext } from './workpad_route_context'; +import { RequestHandlerContext, SavedObjectReference } from 'src/core/server'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { CanvasWorkpad } from '../types'; +import { CANVAS_TYPE } from '../common/lib/constants'; + +const mockedExpressionService = { + inject: jest.fn(), + extract: jest.fn(), +}; + +const savedObjectsClient = savedObjectsClientMock.create(); + +const mockContext = ({ + core: { + savedObjects: { + client: savedObjectsClient, + }, + }, +} as unknown) as RequestHandlerContext; + +const workpadRouteContext = createWorkpadRouteContext({ + expressions: mockedExpressionService as any, +}); + +const now = new Date(); + +const injectedExpression = 'fn extracted=false'; +const extractedExpression = 'fn extracted=true'; + +const injectedWorkpad = { + id: 'workpad-id', + pages: [ + { + elements: [ + { + id: 'element-id', + expression: injectedExpression, + }, + ], + }, + ], +}; + +const extractedWorkpad = { + pages: [ + { + elements: [ + { + id: 'element-id', + expression: extractedExpression, + }, + ], + }, + ], +}; + +const references: SavedObjectReference[] = [{ id: 'my-id', name: 'name', type: 'type' }]; + +describe('workpad route context', () => { + let clock: sinon.SinonFakeTimers; + beforeEach(() => { + jest.resetAllMocks(); + clock = sinon.useFakeTimers(now); + }); + + afterEach(() => { + clock.restore(); + }); + + describe('CREATE', () => { + it('extracts references before saving', async () => { + const expectedBody = { + '@created': now.toISOString(), + '@timestamp': now.toISOString(), + ...extractedWorkpad, + }; + + const canvasContext = await workpadRouteContext( + mockContext, + undefined as any, + undefined as any + ); + + mockedExpressionService.extract.mockReturnValue({ + state: fromExpression(extractedExpression), + references, + }); + + const soResponse = {}; + (mockContext.core.savedObjects.client.create as jest.Mock).mockResolvedValue(soResponse); + + const result = await canvasContext.workpad.create(injectedWorkpad as CanvasWorkpad); + + expect(mockContext.core.savedObjects.client.create).toBeCalledWith( + CANVAS_TYPE, + expectedBody, + { + id: injectedWorkpad.id, + references: references.map((r) => ({ + ...r, + name: `element-id:${r.name}`, + })), + } + ); + expect(result).toBe(soResponse); + }); + }); + + describe('GET', () => { + it('injects references to the saved object', async () => { + const id = 'so-id'; + const canvasContext = await workpadRouteContext( + mockContext, + undefined as any, + undefined as any + ); + + (mockContext.core.savedObjects.client.get as jest.Mock).mockResolvedValue({ + attributes: extractedWorkpad, + references, + }); + + mockedExpressionService.inject.mockReturnValue(fromExpression(injectedExpression)); + + const result = await canvasContext.workpad.get(id); + const { id: ingnoredId, ...expectedAttributes } = injectedWorkpad; + + expect(mockContext.core.savedObjects.client.get).toBeCalledWith(CANVAS_TYPE, id); + + expect(result.attributes).toEqual(expectedAttributes); + }); + }); + + describe('UPDATE', () => { + it('extracts from the given attributes', async () => { + const id = 'workpad-id'; + const createdDate = new Date(2020, 1, 1).toISOString(); + + const canvasContext = await workpadRouteContext( + mockContext, + undefined as any, + undefined as any + ); + + (mockContext.core.savedObjects.client.get as jest.Mock).mockReturnValue({ + attributes: { + ...extractedWorkpad, + '@created': createdDate, + }, + references, + }); + + const updatedInjectedExpression = 'fn ref="my-value"'; + const updatedExtractedExpression = 'fn ref="extracted"'; + const updatedWorkpad = { + id: 'workpad-id', + pages: [ + { + elements: [ + { + id: 'new-element-id', + expression: updatedInjectedExpression, + }, + ], + }, + ], + }; + + const expectedWorkpad = { + '@created': createdDate, + '@timestamp': now.toISOString(), + pages: [ + { + elements: [ + { + id: 'new-element-id', + expression: updatedExtractedExpression, + }, + ], + }, + ], + }; + + mockedExpressionService.inject.mockReturnValue(fromExpression(injectedExpression)); + mockedExpressionService.extract.mockReturnValue({ + state: fromExpression(updatedExtractedExpression), + references, + }); + + await canvasContext.workpad.update(id, updatedWorkpad as CanvasWorkpad); + + expect(mockContext.core.savedObjects.client.create).toBeCalledWith( + CANVAS_TYPE, + expectedWorkpad, + { + id, + references: references.map((r) => ({ + ...r, + name: `new-element-id:${r.name}`, + })), + overwrite: true, + } + ); + }); + }); +}); diff --git a/x-pack/plugins/canvas/server/workpad_route_context.ts b/x-pack/plugins/canvas/server/workpad_route_context.ts new file mode 100644 index 0000000000000..5689bf9961f76 --- /dev/null +++ b/x-pack/plugins/canvas/server/workpad_route_context.ts @@ -0,0 +1,100 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RequestHandlerContext, RequestHandlerContextProvider, SavedObject } from 'kibana/server'; +import { ExpressionsService } from 'src/plugins/expressions'; +import { WorkpadAttributes } from './routes/workpad/workpad_attributes'; +import { CANVAS_TYPE } from '../common/lib/constants'; +import { injectReferences, extractReferences } from './saved_objects/workpad_references'; +import { getId } from '../common/lib/get_id'; +import { CanvasWorkpad } from '../types'; + +export interface CanvasRouteHandlerContext extends RequestHandlerContext { + canvas: { + workpad: { + create: (attributes: CanvasWorkpad) => Promise>; + get: (id: string) => Promise>; + update: ( + id: string, + attributes: Partial + ) => Promise>; + }; + }; +} + +interface Deps { + expressions: ExpressionsService; +} + +export const createWorkpadRouteContext: ( + deps: Deps +) => RequestHandlerContextProvider = ({ expressions }) => { + return (context) => ({ + workpad: { + create: async (workpad: CanvasWorkpad) => { + const now = new Date().toISOString(); + const { id: maybeId, ...attributes } = workpad; + + const id = maybeId ? maybeId : getId('workpad'); + + const { workpad: extractedAttributes, references } = extractReferences( + attributes, + expressions + ); + + return await context.core.savedObjects.client.create( + CANVAS_TYPE, + { + ...extractedAttributes, + '@timestamp': now, + '@created': now, + }, + { id, references } + ); + }, + get: async (id: string) => { + const workpad = await context.core.savedObjects.client.get( + CANVAS_TYPE, + id + ); + + workpad.attributes = injectReferences(workpad.attributes, workpad.references, expressions); + + return workpad; + }, + update: async (id: string, { id: omittedId, ...workpad }: Partial) => { + const now = new Date().toISOString(); + + const workpadObject = await context.core.savedObjects.client.get( + CANVAS_TYPE, + id + ); + + const injectedAttributes = injectReferences( + workpadObject.attributes, + workpadObject.references, + expressions + ); + + const updatedAttributes = { + ...injectedAttributes, + ...workpad, + '@timestamp': now, // always update the modified time + '@created': workpadObject.attributes['@created'], // ensure created is not modified + } as WorkpadAttributes; + + const extracted = extractReferences(updatedAttributes, expressions); + + return await context.core.savedObjects.client.create(CANVAS_TYPE, extracted.workpad, { + overwrite: true, + id, + references: extracted.references, + }); + }, + }, + }); +};