From 6424cef8f47df6ef0dd591064b7fb5784648aee4 Mon Sep 17 00:00:00 2001 From: Poff Poffenberger Date: Tue, 18 Aug 2020 12:39:46 -0500 Subject: [PATCH] [Canvas] Remove dependency on legacy expressions APIs (#74885) * Remove legacy types and function registration * Pull server interpreter functions routes into Canvas and update them to use new expressions API * Clean up comment * Removing boom and doing more cleanup * Add functions test and refactor other router tests * Adding a type and refactoring a forgotten test * more tests Co-authored-by: Elastic Machine --- x-pack/plugins/canvas/common/lib/constants.ts | 1 + x-pack/plugins/canvas/kibana.json | 2 +- x-pack/plugins/canvas/public/application.tsx | 2 +- .../canvas/public/functions/filters.ts | 2 +- .../plugins/canvas/public/functions/index.ts | 2 +- x-pack/plugins/canvas/public/functions/to.ts | 2 +- x-pack/plugins/canvas/public/plugin.tsx | 2 + .../canvas/public/services/expressions.ts | 39 +++++++++- x-pack/plugins/canvas/public/store.ts | 3 +- x-pack/plugins/canvas/server/plugin.ts | 10 ++- .../routes/custom_elements/create.test.ts | 19 ++--- .../routes/custom_elements/delete.test.ts | 18 ++--- .../routes/custom_elements/find.test.ts | 18 ++--- .../server/routes/custom_elements/get.test.ts | 18 ++--- .../routes/custom_elements/update.test.ts | 18 ++--- .../server/routes/es_fields/es_fields.test.ts | 18 ++--- .../server/routes/functions/functions.test.ts | 51 +++++++++++++ .../server/routes/functions/functions.ts | 73 +++++++++++++++++++ .../canvas/server/routes/functions/index.ts | 13 ++++ x-pack/plugins/canvas/server/routes/index.ts | 9 ++- .../server/routes/shareables/download.test.ts | 13 ++-- .../server/routes/shareables/zip.test.ts | 14 ++-- .../server/routes/templates/list.test.ts | 25 ++----- .../canvas/server/routes/test_helpers.ts | 29 ++++++++ .../server/routes/workpad/create.test.ts | 19 ++--- .../server/routes/workpad/delete.test.ts | 18 ++--- .../canvas/server/routes/workpad/find.test.ts | 18 ++--- .../canvas/server/routes/workpad/get.test.ts | 18 ++--- .../server/routes/workpad/update.test.ts | 29 +++----- .../canvas/server/setup_interpreter.ts | 2 +- 30 files changed, 306 insertions(+), 199 deletions(-) create mode 100644 x-pack/plugins/canvas/server/routes/functions/functions.test.ts create mode 100644 x-pack/plugins/canvas/server/routes/functions/functions.ts create mode 100644 x-pack/plugins/canvas/server/routes/functions/index.ts create mode 100644 x-pack/plugins/canvas/server/routes/test_helpers.ts diff --git a/x-pack/plugins/canvas/common/lib/constants.ts b/x-pack/plugins/canvas/common/lib/constants.ts index e960a86bd76dc0..d514fb53b4a4b8 100644 --- a/x-pack/plugins/canvas/common/lib/constants.ts +++ b/x-pack/plugins/canvas/common/lib/constants.ts @@ -42,3 +42,4 @@ export const API_ROUTE_SHAREABLE_RUNTIME = '/public/canvas/runtime'; export const API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD = `/public/canvas/${SHAREABLE_RUNTIME_NAME}.js`; export const CANVAS_EMBEDDABLE_CLASSNAME = `canvasEmbeddable`; export const CONTEXT_MENU_TOP_BORDER_CLASSNAME = 'canvasContextMenu--topBorder'; +export const API_ROUTE_FUNCTIONS = `${API_ROUTE}/fns`; diff --git a/x-pack/plugins/canvas/kibana.json b/x-pack/plugins/canvas/kibana.json index 5f4ea5802cb138..68fbec3e8429d1 100644 --- a/x-pack/plugins/canvas/kibana.json +++ b/x-pack/plugins/canvas/kibana.json @@ -5,7 +5,7 @@ "configPath": ["xpack", "canvas"], "server": true, "ui": true, - "requiredPlugins": ["data", "embeddable", "expressions", "features", "home", "inspector", "uiActions"], + "requiredPlugins": ["bfetch", "data", "embeddable", "expressions", "features", "home", "inspector", "uiActions"], "optionalPlugins": ["usageCollection"], "requiredBundles": ["kibanaReact", "maps", "lens", "visualizations", "kibanaUtils", "kibanaLegacy", "discover", "savedObjects", "reporting"] } diff --git a/x-pack/plugins/canvas/public/application.tsx b/x-pack/plugins/canvas/public/application.tsx index 90173a20500e56..482cd043731055 100644 --- a/x-pack/plugins/canvas/public/application.tsx +++ b/x-pack/plugins/canvas/public/application.tsx @@ -86,7 +86,7 @@ export const initializeCanvas = async ( const canvasFunctions = initFunctions({ timefilter: setupPlugins.data.query.timefilter.timefilter, prependBasePath: coreSetup.http.basePath.prepend, - typesRegistry: setupPlugins.expressions.__LEGACY.types, + types: setupPlugins.expressions.getTypes(), }); for (const fn of canvasFunctions) { diff --git a/x-pack/plugins/canvas/public/functions/filters.ts b/x-pack/plugins/canvas/public/functions/filters.ts index 61fa67dc63316c..fdb5d69d35515b 100644 --- a/x-pack/plugins/canvas/public/functions/filters.ts +++ b/x-pack/plugins/canvas/public/functions/filters.ts @@ -81,7 +81,7 @@ export function filtersFunctionFactory(initialize: InitializeArguments): () => F const filterAST = fromExpression(filterExpression); return interpretAst(filterAST, getWorkpadVariablesAsObject(getState())); } else { - const filterType = initialize.typesRegistry.get('filter'); + const filterType = initialize.types.filter; return filterType?.from(null, {}); } }, diff --git a/x-pack/plugins/canvas/public/functions/index.ts b/x-pack/plugins/canvas/public/functions/index.ts index 5e098d8f175c51..a7893162be8f8b 100644 --- a/x-pack/plugins/canvas/public/functions/index.ts +++ b/x-pack/plugins/canvas/public/functions/index.ts @@ -12,7 +12,7 @@ import { CanvasSetupDeps, CoreSetup } from '../plugin'; export interface InitializeArguments { prependBasePath: CoreSetup['http']['basePath']['prepend']; - typesRegistry: CanvasSetupDeps['expressions']['__LEGACY']['types']; + types: ReturnType; timefilter: CanvasSetupDeps['data']['query']['timefilter']['timefilter']; } diff --git a/x-pack/plugins/canvas/public/functions/to.ts b/x-pack/plugins/canvas/public/functions/to.ts index 032873dfa6cf2f..36b2d3f9f04c66 100644 --- a/x-pack/plugins/canvas/public/functions/to.ts +++ b/x-pack/plugins/canvas/public/functions/to.ts @@ -38,7 +38,7 @@ export function toFunctionFactory(initialize: InitializeArguments): () => ToFunc throw errors.missingType(); } - return castProvider(initialize.typesRegistry.toJS())(input, args.type); + return castProvider(initialize.types)(input, args.type); }, }; }; diff --git a/x-pack/plugins/canvas/public/plugin.tsx b/x-pack/plugins/canvas/public/plugin.tsx index 4829a94bb0db84..0269774a446c1f 100644 --- a/x-pack/plugins/canvas/public/plugin.tsx +++ b/x-pack/plugins/canvas/public/plugin.tsx @@ -24,6 +24,7 @@ import { UiActionsStart } from '../../../../src/plugins/ui_actions/public'; import { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; import { Start as InspectorStart } from '../../../../src/plugins/inspector/public'; +import { BfetchPublicSetup } from '../../../../src/plugins/bfetch/public'; // @ts-expect-error untyped local import { argTypeSpecs } from './expression_types/arg_types'; import { transitions } from './transitions'; @@ -41,6 +42,7 @@ export interface CanvasSetupDeps { expressions: ExpressionsSetup; home: HomePublicPluginSetup; usageCollection?: UsageCollectionSetup; + bfetch: BfetchPublicSetup; } export interface CanvasStartDeps { diff --git a/x-pack/plugins/canvas/public/services/expressions.ts b/x-pack/plugins/canvas/public/services/expressions.ts index 1376aab0ca8b99..87a657641acfdd 100644 --- a/x-pack/plugins/canvas/public/services/expressions.ts +++ b/x-pack/plugins/canvas/public/services/expressions.ts @@ -5,7 +5,11 @@ */ import { CanvasServiceFactory } from '.'; -import { ExpressionsService } from '../../../../../src/plugins/expressions/common'; +import { + ExpressionsService, + serializeProvider, +} from '../../../../../src/plugins/expressions/common'; +import { API_ROUTE_FUNCTIONS } from '../../common/lib/constants'; export const expressionsServiceFactory: CanvasServiceFactory = async ( coreSetup, @@ -13,6 +17,37 @@ export const expressionsServiceFactory: CanvasServiceFactory setupPlugins, startPlugins ) => { - await setupPlugins.expressions.__LEGACY.loadLegacyServerFunctionWrappers(); + const { expressions, bfetch } = setupPlugins; + + let cached: Promise | null = null; + const loadServerFunctionWrappers = async () => { + if (!cached) { + cached = (async () => { + const serverFunctionList = await coreSetup.http.get(API_ROUTE_FUNCTIONS); + const batchedFunction = bfetch.batchedFunction({ url: API_ROUTE_FUNCTIONS }); + const { serialize } = serializeProvider(expressions.getTypes()); + + // For every sever-side function, register a client-side + // function that matches its definition, but which simply + // calls the server-side function endpoint. + Object.keys(serverFunctionList).forEach((functionName) => { + if (expressions.getFunction(functionName)) { + return; + } + const fn = () => ({ + ...serverFunctionList[functionName], + fn: (input: any, args: any) => { + return batchedFunction({ functionName, args, context: serialize(input) }); + }, + }); + expressions.registerFunction(fn); + }); + })(); + } + return cached; + }; + + await loadServerFunctionWrappers(); + return setupPlugins.expressions.fork(); }; diff --git a/x-pack/plugins/canvas/public/store.ts b/x-pack/plugins/canvas/public/store.ts index ef93a34296da2e..b1c10f8d46d279 100644 --- a/x-pack/plugins/canvas/public/store.ts +++ b/x-pack/plugins/canvas/public/store.ts @@ -15,6 +15,7 @@ import { import { getInitialState } from './state/initial_state'; import { CoreSetup } from '../../../../src/core/public'; +import { API_ROUTE_FUNCTIONS } from '../common/lib/constants'; import { CanvasSetupDeps } from './plugin'; export async function createStore(core: CoreSetup, plugins: CanvasSetupDeps) { @@ -31,7 +32,7 @@ export async function createFreshStore(core: CoreSetup, plugins: CanvasSetupDeps const basePath = core.http.basePath.get(); // Retrieve server functions - const serverFunctionsResponse = await core.http.get(`/api/interpreter/fns`); + const serverFunctionsResponse = await core.http.get(API_ROUTE_FUNCTIONS); const serverFunctions = Object.values(serverFunctionsResponse); initialState.app = { diff --git a/x-pack/plugins/canvas/server/plugin.ts b/x-pack/plugins/canvas/server/plugin.ts index 4fa7e2d934647d..c822ed86cb01c6 100644 --- a/x-pack/plugins/canvas/server/plugin.ts +++ b/x-pack/plugins/canvas/server/plugin.ts @@ -7,6 +7,7 @@ import { first } from 'rxjs/operators'; import { CoreSetup, PluginInitializerContext, Plugin, Logger, CoreStart } from 'src/core/server'; import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; +import { BfetchServerSetup } from 'src/plugins/bfetch/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { HomeServerPluginSetup } from 'src/plugins/home/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; @@ -21,6 +22,7 @@ interface PluginsSetup { expressions: ExpressionsServerSetup; features: FeaturesPluginSetup; home: HomeServerPluginSetup; + bfetch: BfetchServerSetup; usageCollection?: UsageCollectionSetup; } @@ -67,7 +69,13 @@ export class CanvasPlugin implements Plugin { const canvasRouter = coreSetup.http.createRouter(); - initRoutes({ router: canvasRouter, logger: this.logger }); + initRoutes({ + router: canvasRouter, + expressions: plugins.expressions, + bfetch: plugins.bfetch, + elasticsearch: coreSetup.elasticsearch, + logger: this.logger, + }); loadSampleData( plugins.home.sampleData.addSavedObjectsToSampleDataset, diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/create.test.ts b/x-pack/plugins/canvas/server/routes/custom_elements/create.test.ts index 290175d9062ea8..4f6215b811505f 100644 --- a/x-pack/plugins/canvas/server/routes/custom_elements/create.test.ts +++ b/x-pack/plugins/canvas/server/routes/custom_elements/create.test.ts @@ -5,15 +5,11 @@ */ import sinon from 'sinon'; -import { - savedObjectsClientMock, - httpServiceMock, - httpServerMock, - loggingSystemMock, -} from 'src/core/server/mocks'; +import { savedObjectsClientMock, httpServerMock } from 'src/core/server/mocks'; import { CUSTOM_ELEMENT_TYPE } from '../../../common/lib/constants'; import { initializeCreateCustomElementRoute } from './create'; import { kibanaResponseFactory, RequestHandlerContext, RequestHandler } from 'src/core/server'; +import { getMockedRouterDeps } from '../test_helpers'; const mockRouteContext = ({ core: { @@ -36,15 +32,10 @@ describe('POST custom element', () => { beforeEach(() => { clock = sinon.useFakeTimers(now); - const httpService = httpServiceMock.createSetupContract(); + const routerDeps = getMockedRouterDeps(); + initializeCreateCustomElementRoute(routerDeps); - const router = httpService.createRouter(); - initializeCreateCustomElementRoute({ - router, - logger: loggingSystemMock.create().get(), - }); - - routeHandler = router.post.mock.calls[0][1]; + routeHandler = routerDeps.router.post.mock.calls[0][1]; }); afterEach(() => { diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/delete.test.ts b/x-pack/plugins/canvas/server/routes/custom_elements/delete.test.ts index 62ce4b9c3593ce..1a72917a6bce93 100644 --- a/x-pack/plugins/canvas/server/routes/custom_elements/delete.test.ts +++ b/x-pack/plugins/canvas/server/routes/custom_elements/delete.test.ts @@ -7,12 +7,8 @@ import { CUSTOM_ELEMENT_TYPE } from '../../../common/lib/constants'; import { initializeDeleteCustomElementRoute } from './delete'; import { kibanaResponseFactory, RequestHandlerContext, RequestHandler } from 'src/core/server'; -import { - savedObjectsClientMock, - httpServiceMock, - httpServerMock, - loggingSystemMock, -} from 'src/core/server/mocks'; +import { savedObjectsClientMock, httpServerMock } from 'src/core/server/mocks'; +import { getMockedRouterDeps } from '../test_helpers'; const mockRouteContext = ({ core: { @@ -26,14 +22,10 @@ describe('DELETE custom element', () => { let routeHandler: RequestHandler; beforeEach(() => { - const httpService = httpServiceMock.createSetupContract(); - const router = httpService.createRouter(); - initializeDeleteCustomElementRoute({ - router, - logger: loggingSystemMock.create().get(), - }); + const routerDeps = getMockedRouterDeps(); + initializeDeleteCustomElementRoute(routerDeps); - routeHandler = router.delete.mock.calls[0][1]; + routeHandler = routerDeps.router.delete.mock.calls[0][1]; }); it(`returns 200 ok when the custom element is deleted`, async () => { diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/find.test.ts b/x-pack/plugins/canvas/server/routes/custom_elements/find.test.ts index d42c97b62e0f39..10aaa633be53c7 100644 --- a/x-pack/plugins/canvas/server/routes/custom_elements/find.test.ts +++ b/x-pack/plugins/canvas/server/routes/custom_elements/find.test.ts @@ -6,12 +6,8 @@ import { initializeFindCustomElementsRoute } from './find'; import { kibanaResponseFactory, RequestHandlerContext, RequestHandler } from 'src/core/server'; -import { - savedObjectsClientMock, - httpServiceMock, - httpServerMock, - loggingSystemMock, -} from 'src/core/server/mocks'; +import { savedObjectsClientMock, httpServerMock } from 'src/core/server/mocks'; +import { getMockedRouterDeps } from '../test_helpers'; const mockRouteContext = ({ core: { @@ -25,14 +21,10 @@ describe('Find custom element', () => { let routeHandler: RequestHandler; beforeEach(() => { - const httpService = httpServiceMock.createSetupContract(); - const router = httpService.createRouter(); - initializeFindCustomElementsRoute({ - router, - logger: loggingSystemMock.create().get(), - }); + const routerDeps = getMockedRouterDeps(); + initializeFindCustomElementsRoute(routerDeps); - routeHandler = router.get.mock.calls[0][1]; + routeHandler = routerDeps.router.get.mock.calls[0][1]; }); it(`returns 200 with the found custom elements`, async () => { diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/get.test.ts b/x-pack/plugins/canvas/server/routes/custom_elements/get.test.ts index 7b4d0eba374199..a6c3d60ee9096c 100644 --- a/x-pack/plugins/canvas/server/routes/custom_elements/get.test.ts +++ b/x-pack/plugins/canvas/server/routes/custom_elements/get.test.ts @@ -7,12 +7,8 @@ import { CUSTOM_ELEMENT_TYPE } from '../../../common/lib/constants'; import { initializeGetCustomElementRoute } from './get'; import { kibanaResponseFactory, RequestHandlerContext, RequestHandler } from 'src/core/server'; -import { - savedObjectsClientMock, - httpServiceMock, - httpServerMock, - loggingSystemMock, -} from 'src/core/server/mocks'; +import { savedObjectsClientMock, httpServerMock } from 'src/core/server/mocks'; +import { getMockedRouterDeps } from '../test_helpers'; const mockRouteContext = ({ core: { @@ -26,14 +22,10 @@ describe('GET custom element', () => { let routeHandler: RequestHandler; beforeEach(() => { - const httpService = httpServiceMock.createSetupContract(); - const router = httpService.createRouter(); - initializeGetCustomElementRoute({ - router, - logger: loggingSystemMock.create().get(), - }); + const routerDeps = getMockedRouterDeps(); + initializeGetCustomElementRoute(routerDeps); - routeHandler = router.get.mock.calls[0][1]; + routeHandler = routerDeps.router.get.mock.calls[0][1]; }); it(`returns 200 when the custom element is found`, async () => { diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/update.test.ts b/x-pack/plugins/canvas/server/routes/custom_elements/update.test.ts index 0f954904355ae5..6b81cf9e3faa15 100644 --- a/x-pack/plugins/canvas/server/routes/custom_elements/update.test.ts +++ b/x-pack/plugins/canvas/server/routes/custom_elements/update.test.ts @@ -9,13 +9,9 @@ import { CustomElement } from '../../../types'; import { CUSTOM_ELEMENT_TYPE } from '../../../common/lib/constants'; import { initializeUpdateCustomElementRoute } from './update'; import { kibanaResponseFactory, RequestHandlerContext, RequestHandler } from 'src/core/server'; -import { - savedObjectsClientMock, - httpServiceMock, - httpServerMock, - loggingSystemMock, -} from 'src/core/server/mocks'; +import { savedObjectsClientMock, httpServerMock } from 'src/core/server/mocks'; import { okResponse } from '../ok_response'; +import { getMockedRouterDeps } from '../test_helpers'; const mockRouteContext = ({ core: { @@ -51,14 +47,10 @@ describe('PUT custom element', () => { beforeEach(() => { clock = sinon.useFakeTimers(now); - const httpService = httpServiceMock.createSetupContract(); - const router = httpService.createRouter(); - initializeUpdateCustomElementRoute({ - router, - logger: loggingSystemMock.create().get(), - }); + const routerDeps = getMockedRouterDeps(); + initializeUpdateCustomElementRoute(routerDeps); - routeHandler = router.put.mock.calls[0][1]; + routeHandler = routerDeps.router.put.mock.calls[0][1]; }); afterEach(() => { diff --git a/x-pack/plugins/canvas/server/routes/es_fields/es_fields.test.ts b/x-pack/plugins/canvas/server/routes/es_fields/es_fields.test.ts index c2cff83f85f0df..a3aff029868d75 100644 --- a/x-pack/plugins/canvas/server/routes/es_fields/es_fields.test.ts +++ b/x-pack/plugins/canvas/server/routes/es_fields/es_fields.test.ts @@ -6,12 +6,8 @@ import { initializeESFieldsRoute } from './es_fields'; import { kibanaResponseFactory, RequestHandlerContext, RequestHandler } from 'src/core/server'; -import { - httpServiceMock, - httpServerMock, - loggingSystemMock, - elasticsearchServiceMock, -} from 'src/core/server/mocks'; +import { httpServerMock, elasticsearchServiceMock } from 'src/core/server/mocks'; +import { getMockedRouterDeps } from '../test_helpers'; const mockRouteContext = ({ core: { @@ -27,14 +23,10 @@ describe('Retrieve ES Fields', () => { let routeHandler: RequestHandler; beforeEach(() => { - const httpService = httpServiceMock.createSetupContract(); - const router = httpService.createRouter(); - initializeESFieldsRoute({ - router, - logger: loggingSystemMock.create().get(), - }); + const routerDeps = getMockedRouterDeps(); + initializeESFieldsRoute(routerDeps); - routeHandler = router.get.mock.calls[0][1]; + routeHandler = routerDeps.router.get.mock.calls[0][1]; }); it(`returns 200 with fields from existing index/index pattern`, async () => { diff --git a/x-pack/plugins/canvas/server/routes/functions/functions.test.ts b/x-pack/plugins/canvas/server/routes/functions/functions.test.ts new file mode 100644 index 00000000000000..755af3eb4ef7ef --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/functions/functions.test.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { kibanaResponseFactory, RequestHandlerContext, RequestHandler } from 'src/core/server'; +import { httpServerMock } from 'src/core/server/mocks'; +import { ExpressionFunction } from 'src/plugins/expressions/common/expression_functions'; +import { initializeGetFunctionsRoute } from './functions'; +import { getMockedRouterDeps } from '../test_helpers'; +import { API_ROUTE_FUNCTIONS } from '../../../common/lib'; +import { functions } from '../../../canvas_plugin_src/functions/server'; + +const mockRouteContext = {} as RequestHandlerContext; +const routePath = API_ROUTE_FUNCTIONS; + +describe('Get list of serverside expression functions', () => { + let routeHandler: RequestHandler; + let mockFuncs: Record; + + beforeEach(() => { + mockFuncs = { + demodata: new ExpressionFunction(functions[0]()), + }; + + const routerDeps = getMockedRouterDeps(); + + routerDeps.expressions.getFunctions.mockReturnValueOnce(mockFuncs); + + initializeGetFunctionsRoute(routerDeps); + + routeHandler = routerDeps.router.get.mock.calls[0][1]; + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + it(`returns 200 with list of functions`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: routePath, + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toBe(JSON.stringify(mockFuncs)); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/functions/functions.ts b/x-pack/plugins/canvas/server/routes/functions/functions.ts new file mode 100644 index 00000000000000..dba3d315c5f8be --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/functions/functions.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LegacyAPICaller } from 'src/core/server'; +import { serializeProvider } from '../../../../../../src/plugins/expressions/common'; +import { RouteInitializerDeps } from '../'; +import { API_ROUTE_FUNCTIONS } from '../../../common/lib/constants'; + +interface FunctionCall { + functionName: string; + args: Record; + context: Record; +} + +export function initializeGetFunctionsRoute(deps: RouteInitializerDeps) { + const { router, expressions } = deps; + router.get( + { + path: API_ROUTE_FUNCTIONS, + validate: false, + }, + async (context, request, response) => { + const functions = expressions.getFunctions(); + const body = JSON.stringify(functions); + return response.ok({ + body, + }); + } + ); +} + +export function initializeBatchFunctionsRoute(deps: RouteInitializerDeps) { + const { bfetch, elasticsearch, expressions } = deps; + + async function runFunction( + handlers: { environment: string; elasticsearchClient: LegacyAPICaller }, + fnCall: FunctionCall + ) { + const { functionName, args, context } = fnCall; + const { deserialize } = serializeProvider(expressions.getTypes()); + + const fnDef = expressions.getFunctions()[functionName]; + if (!fnDef) throw new Error(`Function "${functionName}" could not be found.`); + + const deserialized = deserialize(context); + const result = fnDef.fn(deserialized, args, handlers); + + return result; + } + + /** + * Register an endpoint that executes a batch of functions, and streams the + * results back using ND-JSON. + */ + bfetch.addBatchProcessingRoute(API_ROUTE_FUNCTIONS, (request) => { + return { + onBatchItem: async (fnCall: FunctionCall) => { + const handlers = { + environment: 'server', + elasticsearchClient: elasticsearch.legacy.client.asScoped(request).callAsCurrentUser, + }; + const result = await runFunction(handlers, fnCall); + if (typeof result === 'undefined') { + throw new Error(`Function ${fnCall.functionName} did not return anything.`); + } + return result; + }, + }; + }); +} diff --git a/x-pack/plugins/canvas/server/routes/functions/index.ts b/x-pack/plugins/canvas/server/routes/functions/index.ts new file mode 100644 index 00000000000000..768320fdf499a4 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/functions/index.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { initializeGetFunctionsRoute, initializeBatchFunctionsRoute } from './functions'; +import { RouteInitializerDeps } from '..'; + +export function initFunctionsRoutes(deps: RouteInitializerDeps) { + initializeGetFunctionsRoute(deps); + initializeBatchFunctionsRoute(deps); +} diff --git a/x-pack/plugins/canvas/server/routes/index.ts b/x-pack/plugins/canvas/server/routes/index.ts index 56874151530aa4..2999f4dc5cfe6f 100644 --- a/x-pack/plugins/canvas/server/routes/index.ts +++ b/x-pack/plugins/canvas/server/routes/index.ts @@ -4,16 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter, Logger } from 'src/core/server'; +import { IRouter, Logger, ElasticsearchServiceSetup } from 'src/core/server'; +import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; +import { BfetchServerSetup } from 'src/plugins/bfetch/server'; import { initCustomElementsRoutes } from './custom_elements'; import { initESFieldsRoutes } from './es_fields'; import { initShareablesRoutes } from './shareables'; import { initWorkpadRoutes } from './workpad'; import { initTemplateRoutes } from './templates'; +import { initFunctionsRoutes } from './functions'; export interface RouteInitializerDeps { router: IRouter; logger: Logger; + expressions: ExpressionsServerSetup; + bfetch: BfetchServerSetup; + elasticsearch: ElasticsearchServiceSetup; } export function initRoutes(deps: RouteInitializerDeps) { @@ -22,4 +28,5 @@ export function initRoutes(deps: RouteInitializerDeps) { initShareablesRoutes(deps); initWorkpadRoutes(deps); initTemplateRoutes(deps); + initFunctionsRoutes(deps); } diff --git a/x-pack/plugins/canvas/server/routes/shareables/download.test.ts b/x-pack/plugins/canvas/server/routes/shareables/download.test.ts index 0267a695ae9fe3..1edf9f52e164a4 100644 --- a/x-pack/plugins/canvas/server/routes/shareables/download.test.ts +++ b/x-pack/plugins/canvas/server/routes/shareables/download.test.ts @@ -8,8 +8,9 @@ jest.mock('fs'); import fs from 'fs'; import { kibanaResponseFactory, RequestHandlerContext, RequestHandler } from 'src/core/server'; -import { httpServiceMock, httpServerMock, loggingSystemMock } from 'src/core/server/mocks'; +import { httpServerMock } from 'src/core/server/mocks'; import { initializeDownloadShareableWorkpadRoute } from './download'; +import { getMockedRouterDeps } from '../test_helpers'; const mockRouteContext = {} as RequestHandlerContext; const path = `api/canvas/workpad/find`; @@ -19,14 +20,10 @@ describe('Download Canvas shareables runtime', () => { let routeHandler: RequestHandler; beforeEach(() => { - const httpService = httpServiceMock.createSetupContract(); - const router = httpService.createRouter(); - initializeDownloadShareableWorkpadRoute({ - router, - logger: loggingSystemMock.create().get(), - }); + const routerDeps = getMockedRouterDeps(); + initializeDownloadShareableWorkpadRoute(routerDeps); - routeHandler = router.get.mock.calls[0][1]; + routeHandler = routerDeps.router.get.mock.calls[0][1]; }); afterAll(() => { diff --git a/x-pack/plugins/canvas/server/routes/shareables/zip.test.ts b/x-pack/plugins/canvas/server/routes/shareables/zip.test.ts index 0c19886f07e5c4..b1649c7524c7eb 100644 --- a/x-pack/plugins/canvas/server/routes/shareables/zip.test.ts +++ b/x-pack/plugins/canvas/server/routes/shareables/zip.test.ts @@ -9,8 +9,9 @@ jest.mock('archiver'); // eslint-disable-next-line @typescript-eslint/no-var-requires const archiver = require('archiver') as jest.Mock; import { kibanaResponseFactory, RequestHandlerContext, RequestHandler } from 'src/core/server'; -import { httpServiceMock, httpServerMock, loggingSystemMock } from 'src/core/server/mocks'; +import { httpServerMock } from 'src/core/server/mocks'; import { initializeZipShareableWorkpadRoute } from './zip'; +import { getMockedRouterDeps } from '../test_helpers'; import { API_ROUTE_SHAREABLE_ZIP } from '../../../common/lib'; import { SHAREABLE_RUNTIME_FILE, @@ -26,14 +27,9 @@ describe('Zips Canvas shareables runtime together with workpad', () => { let routeHandler: RequestHandler; beforeEach(() => { - const httpService = httpServiceMock.createSetupContract(); - const router = httpService.createRouter(); - initializeZipShareableWorkpadRoute({ - router, - logger: loggingSystemMock.create().get(), - }); - - routeHandler = router.post.mock.calls[0][1]; + const routerDeps = getMockedRouterDeps(); + initializeZipShareableWorkpadRoute(routerDeps); + routeHandler = routerDeps.router.post.mock.calls[0][1]; }); afterAll(() => { diff --git a/x-pack/plugins/canvas/server/routes/templates/list.test.ts b/x-pack/plugins/canvas/server/routes/templates/list.test.ts index 95658e6a7b5113..9ceab3b5c9705f 100644 --- a/x-pack/plugins/canvas/server/routes/templates/list.test.ts +++ b/x-pack/plugins/canvas/server/routes/templates/list.test.ts @@ -6,18 +6,9 @@ import { badRequest } from 'boom'; import { initializeListTemplates } from './list'; -import { - IRouter, - kibanaResponseFactory, - RequestHandlerContext, - RequestHandler, -} from 'src/core/server'; -import { - savedObjectsClientMock, - httpServiceMock, - httpServerMock, - loggingSystemMock, -} from 'src/core/server/mocks'; +import { kibanaResponseFactory, RequestHandlerContext, RequestHandler } from 'src/core/server'; +import { savedObjectsClientMock, httpServerMock } from 'src/core/server/mocks'; +import { getMockedRouterDeps } from '../test_helpers'; const mockRouteContext = ({ core: { @@ -31,14 +22,10 @@ describe('Find workpad', () => { let routeHandler: RequestHandler; beforeEach(() => { - const httpService = httpServiceMock.createSetupContract(); - const router = httpService.createRouter() as jest.Mocked; - initializeListTemplates({ - router, - logger: loggingSystemMock.create().get(), - }); + const routerDeps = getMockedRouterDeps(); + initializeListTemplates(routerDeps); - routeHandler = router.get.mock.calls[0][1]; + routeHandler = routerDeps.router.get.mock.calls[0][1]; }); it(`returns 200 with the found templates`, async () => { diff --git a/x-pack/plugins/canvas/server/routes/test_helpers.ts b/x-pack/plugins/canvas/server/routes/test_helpers.ts new file mode 100644 index 00000000000000..695cc79e513dca --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/test_helpers.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + httpServiceMock, + loggingSystemMock, + elasticsearchServiceMock, +} from 'src/core/server/mocks'; +import { bfetchPluginMock } from '../../../../../src/plugins/bfetch/server/mocks'; +import { expressionsPluginMock } from '../../../../../src/plugins/expressions/server/mocks'; + +export function getMockedRouterDeps() { + const httpService = httpServiceMock.createSetupContract(); + const elasticsearch = elasticsearchServiceMock.createSetup(); + const bfetch = bfetchPluginMock.createSetupContract(); + const expressions = expressionsPluginMock.createSetupContract(); + const router = httpService.createRouter(); + + return { + router, + expressions, + elasticsearch, + bfetch, + logger: loggingSystemMock.create().get(), + }; +} 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 4756349a8a5ff0..4e927751a026f2 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/create.test.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/create.test.ts @@ -5,15 +5,11 @@ */ import sinon from 'sinon'; -import { - savedObjectsClientMock, - httpServiceMock, - httpServerMock, - loggingSystemMock, -} from 'src/core/server/mocks'; +import { savedObjectsClientMock, httpServerMock } from 'src/core/server/mocks'; import { CANVAS_TYPE } from '../../../common/lib/constants'; import { initializeCreateWorkpadRoute } from './create'; import { kibanaResponseFactory, RequestHandlerContext, RequestHandler } from 'src/core/server'; +import { getMockedRouterDeps } from '../test_helpers'; let mockRouteContext = ({ core: { @@ -44,15 +40,10 @@ describe('POST workpad', () => { clock = sinon.useFakeTimers(now); - const httpService = httpServiceMock.createSetupContract(); + const routerDeps = getMockedRouterDeps(); + initializeCreateWorkpadRoute(routerDeps); - const router = httpService.createRouter(); - initializeCreateWorkpadRoute({ - router, - logger: loggingSystemMock.create().get(), - }); - - routeHandler = router.post.mock.calls[0][1]; + routeHandler = routerDeps.router.post.mock.calls[0][1]; }); afterEach(() => { diff --git a/x-pack/plugins/canvas/server/routes/workpad/delete.test.ts b/x-pack/plugins/canvas/server/routes/workpad/delete.test.ts index 32ce30325b60ad..e66628ffb4d4e4 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/delete.test.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/delete.test.ts @@ -7,12 +7,8 @@ import { CANVAS_TYPE } from '../../../common/lib/constants'; import { initializeDeleteWorkpadRoute } from './delete'; import { kibanaResponseFactory, RequestHandlerContext, RequestHandler } from 'src/core/server'; -import { - savedObjectsClientMock, - httpServiceMock, - httpServerMock, - loggingSystemMock, -} from 'src/core/server/mocks'; +import { savedObjectsClientMock, httpServerMock } from 'src/core/server/mocks'; +import { getMockedRouterDeps } from '../test_helpers'; const mockRouteContext = ({ core: { @@ -26,14 +22,10 @@ describe('DELETE workpad', () => { let routeHandler: RequestHandler; beforeEach(() => { - const httpService = httpServiceMock.createSetupContract(); - const router = httpService.createRouter(); - initializeDeleteWorkpadRoute({ - router, - logger: loggingSystemMock.create().get(), - }); + const routerDeps = getMockedRouterDeps(); + initializeDeleteWorkpadRoute(routerDeps); - routeHandler = router.delete.mock.calls[0][1]; + routeHandler = routerDeps.router.delete.mock.calls[0][1]; }); it(`returns 200 ok when the workpad is deleted`, async () => { diff --git a/x-pack/plugins/canvas/server/routes/workpad/find.test.ts b/x-pack/plugins/canvas/server/routes/workpad/find.test.ts index a87cf7be57d811..593e4062686f91 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/find.test.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/find.test.ts @@ -6,12 +6,8 @@ import { initializeFindWorkpadsRoute } from './find'; import { kibanaResponseFactory, RequestHandlerContext, RequestHandler } from 'src/core/server'; -import { - savedObjectsClientMock, - httpServiceMock, - httpServerMock, - loggingSystemMock, -} from 'src/core/server/mocks'; +import { savedObjectsClientMock, httpServerMock } from 'src/core/server/mocks'; +import { getMockedRouterDeps } from '../test_helpers'; const mockRouteContext = ({ core: { @@ -25,14 +21,10 @@ describe('Find workpad', () => { let routeHandler: RequestHandler; beforeEach(() => { - const httpService = httpServiceMock.createSetupContract(); - const router = httpService.createRouter(); - initializeFindWorkpadsRoute({ - router, - logger: loggingSystemMock.create().get(), - }); + const routerDeps = getMockedRouterDeps(); + initializeFindWorkpadsRoute(routerDeps); - routeHandler = router.get.mock.calls[0][1]; + routeHandler = routerDeps.router.get.mock.calls[0][1]; }); it(`returns 200 with the found workpads`, async () => { 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 8cc190dc6231cc..a51cbefd4031ef 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/get.test.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/get.test.ts @@ -7,14 +7,10 @@ import { CANVAS_TYPE } from '../../../common/lib/constants'; import { initializeGetWorkpadRoute } from './get'; import { kibanaResponseFactory, RequestHandlerContext, RequestHandler } from 'src/core/server'; -import { - savedObjectsClientMock, - httpServiceMock, - httpServerMock, - loggingSystemMock, -} from 'src/core/server/mocks'; +import { savedObjectsClientMock, httpServerMock } from 'src/core/server/mocks'; import { workpadWithGroupAsElement } from '../../../__tests__/fixtures/workpads'; import { CanvasWorkpad } from '../../../types'; +import { getMockedRouterDeps } from '../test_helpers'; const mockRouteContext = ({ core: { @@ -28,14 +24,10 @@ describe('GET workpad', () => { let routeHandler: RequestHandler; beforeEach(() => { - const httpService = httpServiceMock.createSetupContract(); - const router = httpService.createRouter(); - initializeGetWorkpadRoute({ - router, - logger: loggingSystemMock.create().get(), - }); + const routerDeps = getMockedRouterDeps(); + initializeGetWorkpadRoute(routerDeps); - routeHandler = router.get.mock.calls[0][1]; + routeHandler = routerDeps.router.get.mock.calls[0][1]; }); it(`returns 200 when the workpad is found`, async () => { 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 6d7ea06852a5e5..0d97145c902986 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/update.test.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/update.test.ts @@ -8,14 +8,10 @@ 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 { - savedObjectsClientMock, - httpServiceMock, - httpServerMock, - loggingSystemMock, -} from 'src/core/server/mocks'; +import { savedObjectsClientMock, httpServerMock } from 'src/core/server/mocks'; import { workpads } from '../../../__tests__/fixtures/workpads'; import { okResponse } from '../ok_response'; +import { getMockedRouterDeps } from '../test_helpers'; const mockRouteContext = ({ core: { @@ -38,14 +34,10 @@ describe('PUT workpad', () => { beforeEach(() => { clock = sinon.useFakeTimers(now); - const httpService = httpServiceMock.createSetupContract(); - const router = httpService.createRouter(); - initializeUpdateWorkpadRoute({ - router, - logger: loggingSystemMock.create().get(), - }); + const routerDeps = getMockedRouterDeps(); + initializeUpdateWorkpadRoute(routerDeps); - routeHandler = router.put.mock.calls[0][1]; + routeHandler = routerDeps.router.put.mock.calls[0][1]; }); afterEach(() => { @@ -152,14 +144,11 @@ describe('update assets', () => { beforeEach(() => { clock = sinon.useFakeTimers(now); - const httpService = httpServiceMock.createSetupContract(); - const router = httpService.createRouter(); - initializeUpdateWorkpadAssetsRoute({ - router, - logger: loggingSystemMock.create().get(), - }); - routeHandler = router.put.mock.calls[0][1]; + const routerDeps = getMockedRouterDeps(); + initializeUpdateWorkpadAssetsRoute(routerDeps); + + routeHandler = routerDeps.router.put.mock.calls[0][1]; }); afterEach(() => { diff --git a/x-pack/plugins/canvas/server/setup_interpreter.ts b/x-pack/plugins/canvas/server/setup_interpreter.ts index 79bc2f3c1996b7..8dc431345e5f59 100644 --- a/x-pack/plugins/canvas/server/setup_interpreter.ts +++ b/x-pack/plugins/canvas/server/setup_interpreter.ts @@ -8,5 +8,5 @@ import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { functions } from '../canvas_plugin_src/functions/server'; export function setupInterpreter(expressions: ExpressionsServerSetup) { - expressions.__LEGACY.register({ types: [], serverFunctions: functions }); + functions.forEach((f) => expressions.registerFunction(f)); }