[7.x] Expressions refactor (#54342) (#57366)

* Expressions refactor (#54342)

* feat: 🎸 add UiComponent interface

* feat: 🎸 add adapter for react => ui components and back

* refactor: 💡 move registries to shared /common folder

* feat: 🎸 create expressions service state contaienr

* chore: 🤖 export some symbols

* feat: 🎸 add Executor class

* test: 💍 add simple integration test

* feat: 🎸 move registries into executor

* feat: 🎸 add initial implementation of Execution

* feat: 🎸 move Executor's state container into a signle file

* refactor: 💡 move createError() to /common folder

* feat: 🎸 use Executor in plugin definition

* refactor: 💡 rename handlers to FunctionHandlers

* feat: 🎸 improve function typings

* feat: 🎸 move types and func in sep folder, improve Execution

* refactor: 💡 cleanup expression_types folder

* refactor: 💡 improve typing names of expression types

* refactor: 💡 remove lodash from ExpressionType and improve types

* test: 💍 add ExpressionType tests

* refactor: 💡 remove function wrappers around types

* refactor: 💡 move functions to /common

* test: 💍 improve expression function tests

* feat: 🎸 create /parser folder

* refactor: 💡 move function types into /expression_functions dir

* refactor: 💡 improve parser setup

* refactor: 💡 fix export structure and move args into expr_func

* test: 💍 add ExpressionFunctionParameter tests

* fix: 🐛 fix executor types and imports

* refactor: 💡 move getByAlias to plugin, fix Execution types

* feat: 🎸 add function for argument parsing

* test: 💍 add Executor type tests

* test: 💍 add executor function and context tests

* test: 💍 check that Executor returns Execution

* test: 💍 add basic tests for Execution

* test: 💍 add basic test for execution of a chain of functions

* test: 💍 add "mult" function tot tests

* feat: 🎸 create separate expression_renderer folder

* feat: 🎸 use new executor in public plugin

* feat: 🎸 remove renderers from executor, add result to execution

* fix: 🐛 fix Kibana TypeScript errors

* test: 💍 add file to write integration tests for expr plugin

* refactor: 💡 move state_containers to /common

* refactor: 💡 move /parser to /ast and inline format() function

* refactor: 💡 remove remaining @kbn/interpreter imports

* feat: 🎸 better handling and typing for Executor context

* feat: 🎸 use Executor.run function in plugin

* fix: 🐛 fix TypeScript type errors

* test: 💍 move integration tests into one file

* feat: 🎸 create ExpressionsService

* chore: 🤖 clean up legacy code

* feat: 🎸 use ExpressionsService in /public

* refactor: 💡 move inspector adapters to /common

* feat: 🎸 improve execution

* feat: 🎸 add state to execution state and don't clone AST

* test: 💍 add tests for Execution object

* test: 💍 improve expression test helpers

* test: 💍 add Execution tests

* refactor: 💡 improve required argument checking

* fix: 🐛 fix Kibana TypeScript errors

* test: 💍 add ExpressionsService unit tests

* fix: 🐛 fix Expression plugin TypeScript types

* refactor: 💡 prefix React component with React*

* fix: 🐛 fix X-Pack TypeScript errors

* fix: 🐛 fix test TypeScript errors

* fix: 🐛 fix issues preventing loading

* feat: 🎸 remove getInitialContext() handler

* fix: 🐛 fix TypeScript errors

* chore: 🤖 remove uicomponent interface

* chore: 🤖 remove missing import

* fix: 🐛 correctly handle .query in "kibana" expression function

* refactor: 💡 call first arg in expression functions "input"

* fix: 🐛 do not free Execution state container

* test: 💍 fix tests after refactor

* test: 💍 fix more tests after refactor

* fix: 🐛 remove redundant export

* test: 💍 update intepreter_functional test shapshots

* fix: 🐛 relax "kibana" function throwing on missin gsearch ctx

* refactor: 💡 don't use ExpressionAST interface in Canvas

* docs: ✏️ improve ExpressionRenderer JSDocs

* refactor: 💡 rename context.types to inputTypes in internal fn

* refactor: 💡 replace context.types by unknown in ExprFuncDef

* refactor: 💡 improve expression function definitions in OSS

* fix: 🐛 correctly set name on metric_vis_fn

* refactor: 💡 improve Lens definitions of expression functions

* refactor: 💡 improve Canvas expression function definitions

* test: 💍 add createMockExecutionContext() helper

* refactor: 💡 add some type to events$ observable in expr handler

* feat: 🎸 add types to observables in data handler

* refactor: 💡 use inputTypes in canvas

* fix: 🐛 fix interpreter grammer generation script

* feat: 🎸 allow array in getByAlias

* test: 💍 simplify test function specs

* test: 💍 fix autocomplete tests

* fix: 🐛 use correct expression types and NP getFunctions() API

* refactor: 💡 use NP expressions to get renderer

* fix: 🐛 use context.types on server-side Canvas function defs

* refactor: 💡 use NP API to register Canvas renderers

* feat: 🎸 use NP API to get types

* style: 💄 minor formatting changes

* feat: 🎸 use NP API to get expression functions

* fix: 🐛 fix Canvas workpads

* test: 💍 add missing mock functions

* refactor: 💡 improve Lens func definition argument types

* fix: 🐛 fix Lens type error

* feat: 🎸 make lens datatable work again

* feat: 🎸 bootstrap ExpressionsService on server-side

* feat: 🎸 expose more registry related functions in contract

* feat: 🎸 add environment: server to server-side expressions

* docs: ✏️ add documentation

* test: 💍 add missing Jest mocks

* fix: 🐛 correct TypeScript type

* docs: ✏️ improve documentation

* fix: 🐛 make FunctionHelpDict type contain only Canvas functions

* fix: 🐛 fix merge conflict

* test: 💍 fix expression mocks

* fix: fix TypeScript disabled help type

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Vadim Dalecky 2020-02-12 09:26:48 +01:00 committed by GitHub
parent c34027098c
commit b9f4eec008
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
304 changed files with 5275 additions and 3033 deletions

View file

@ -3,7 +3,7 @@
"version": "1.0.0",
"license": "Apache-2.0",
"scripts": {
"interpreter:peg": "pegjs common/lib/grammar.peg",
"interpreter:peg": "pegjs src/common/lib/grammar.peg",
"build": "node scripts/build",
"kbn:bootstrap": "node scripts/build --dev",
"kbn:watch": "node scripts/build --dev --watch"

View file

@ -31,9 +31,7 @@ export class Registry {
}
register(fn) {
if (typeof fn !== 'function') throw new Error(`Register requires an function`);
const obj = fn();
const obj = typeof fn === 'function' ? fn() : fn;
if (typeof obj !== 'object' || !obj[this._prop]) {
throw new Error(`Registered functions must return an object with a ${this._prop} property`);

View file

@ -24,3 +24,4 @@ type B = UnwrapPromise<A>; // string
- `ShallowPromise<T>` &mdash; Same as `Promise` type, but it flat maps the wrapped type.
- `UnwrapObservable<T>` &mdash; Returns wrapped type of an observable.
- `UnwrapPromise<T>` &mdash; Returns wrapped type of a promise.
- `UnwrapPromiseOrReturn<T>` &mdash; Returns wrapped type of a promise or the type itself, if it isn't a promise.

View file

@ -35,6 +35,11 @@ export type ShallowPromise<T> = T extends Promise<infer U> ? Promise<U> : Promis
*/
export type UnwrapPromise<T extends Promise<any>> = PromiseType<T>;
/**
* Returns wrapped type of a promise, or returns type as is, if it is not a promise.
*/
export type UnwrapPromiseOrReturn<T> = T extends Promise<infer U> ? U : T;
/**
* Minimal interface for an object resembling an `Observable`.
*/

View file

@ -24,7 +24,7 @@ import { createFormat } from 'ui/visualize/loader/pipeline_helpers/utilities';
import {
KibanaContext,
KibanaDatatable,
ExpressionFunction,
ExpressionFunctionDefinition,
KibanaDatatableColumn,
} from 'src/plugins/expressions/public';
import {
@ -66,7 +66,8 @@ export interface RequestHandlerParams {
const name = 'esaggs';
type Context = KibanaContext | null;
type Input = KibanaContext | null;
type Output = Promise<KibanaDatatable>;
interface Arguments {
index: string;
@ -76,8 +77,6 @@ interface Arguments {
aggConfigs: string;
}
type Return = Promise<KibanaDatatable>;
const handleCourierRequest = async ({
searchSource,
aggs,
@ -221,12 +220,10 @@ const handleCourierRequest = async ({
return (searchSource as any).tabifiedResponse;
};
export const esaggs = (): ExpressionFunction<typeof name, Context, Arguments, Return> => ({
export const esaggs = (): ExpressionFunctionDefinition<typeof name, Input, Arguments, Output> => ({
name,
type: 'kibana_datatable',
context: {
types: ['kibana_context', 'null'],
},
inputTypes: ['kibana_context', 'null'],
help: i18n.translate('data.functions.esaggs.help', {
defaultMessage: 'Run AggConfig aggregation',
}),
@ -256,7 +253,7 @@ export const esaggs = (): ExpressionFunction<typeof name, Context, Arguments, Re
help: '',
},
},
async fn(context, args, { inspectorAdapters, abortSignal }) {
async fn(input, args, { inspectorAdapters, abortSignal }) {
const indexPatterns = getIndexPatterns();
const { filterManager } = getQueryService();
@ -272,13 +269,13 @@ export const esaggs = (): ExpressionFunction<typeof name, Context, Arguments, Re
const response = await handleCourierRequest({
searchSource,
aggs,
timeRange: get(context, 'timeRange', undefined),
query: get(context, 'query', undefined),
filters: get(context, 'filters', undefined),
timeRange: get(input, 'timeRange', undefined),
query: get(input, 'query', undefined),
filters: get(input, 'filters', undefined),
forceFetch: true,
metricsAtAllLevels: args.metricsAtAllLevels,
partialRows: args.partialRows,
inspectorAdapters,
inspectorAdapters: inspectorAdapters as Adapters,
filterManager,
abortSignal: (abortSignal as unknown) as AbortSignal,
});

View file

@ -20,12 +20,12 @@
import { createInputControlVisFn } from './input_control_fn';
// eslint-disable-next-line
import { functionWrapper } from '../../../../plugins/expressions/public/functions/tests/utils';
import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils';
jest.mock('./legacy_imports.ts');
describe('interpreter/functions#input_control_vis', () => {
const fn = functionWrapper(createInputControlVisFn);
const fn = functionWrapper(createInputControlVisFn());
const visConfig = {
controls: [
{

View file

@ -20,15 +20,11 @@
import { i18n } from '@kbn/i18n';
import {
ExpressionFunction,
ExpressionFunctionDefinition,
KibanaDatatable,
Render,
} from '../../../../plugins/expressions/public';
const name = 'input_control_vis';
type Context = KibanaDatatable;
interface Arguments {
visConfig: string;
}
@ -40,19 +36,15 @@ interface RenderValue {
visConfig: VisParams;
}
type Return = Promise<Render<RenderValue>>;
export const createInputControlVisFn = (): ExpressionFunction<
typeof name,
Context,
export const createInputControlVisFn = (): ExpressionFunctionDefinition<
'input_control_vis',
KibanaDatatable,
Arguments,
Return
Render<RenderValue>
> => ({
name: 'input_control_vis',
type: 'render',
context: {
types: [],
},
inputTypes: [],
help: i18n.translate('inputControl.function.help', {
defaultMessage: 'Input control visualization',
}),
@ -63,7 +55,7 @@ export const createInputControlVisFn = (): ExpressionFunction<
help: '',
},
},
async fn(context, args) {
fn(input, args) {
const params = JSON.parse(args.visConfig);
return {
type: 'render',

View file

@ -1,22 +1,2 @@
Interpreter legacy plugin has been migrated to the New Platform. Use
`expressions` New Platform plugin instead.
In the New Platform:
```ts
class MyPlugin {
setup(core, { expressions }) {
expressions.registerFunction(myFunction);
}
start(core, { expressions }) {
}
}
```
In the Legacy Platform:
```ts
import { npSetup, npStart } from 'ui/new_platform';
npSetup.plugins.expressions.registerFunction(myFunction);
```

View file

@ -22,10 +22,7 @@ import 'uiExports/interpreter';
import { register, registryFactory } from '@kbn/interpreter/common';
import { npSetup } from 'ui/new_platform';
import { registries } from './registries';
import {
ExpressionInterpretWithHandlers,
ExpressionExecutor,
} from '../../../../plugins/expressions/public';
import { Executor, ExpressionExecutor } from '../../../../plugins/expressions/public';
// Expose kbnInterpreter.register(specs) and kbnInterpreter.registries() globally so that plugins
// can register without a transpile step.
@ -46,7 +43,7 @@ export const getInterpreter = async () => {
};
// TODO: This function will be left behind in the legacy platform.
export const interpretAst: ExpressionInterpretWithHandlers = async (ast, context, handlers) => {
export const interpretAst: Executor['run'] = async (ast, context, handlers) => {
const { interpreter } = await getInterpreter();
return await interpreter.interpretAst(ast, context, handlers);
};

View file

@ -18,13 +18,13 @@
*/
// eslint-disable-next-line
import { functionWrapper } from '../../../../plugins/expressions/public/functions/tests/utils';
import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils';
import { createRegionMapFn } from './region_map_fn';
jest.mock('ui/new_platform');
describe('interpreter/functions#regionmap', () => {
const fn = functionWrapper(createRegionMapFn);
const fn = functionWrapper(createRegionMapFn());
const context = {
type: 'kibana_datatable',
rows: [{ 'col-0-1': 0 }],

View file

@ -18,7 +18,7 @@
*/
// eslint-disable-next-line
import { functionWrapper } from '../../../../plugins/expressions/public/functions/tests/utils';
import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils';
import { createTileMapFn } from './tile_map_fn';
jest.mock('ui/new_platform');
@ -41,7 +41,7 @@ jest.mock('ui/vis/map/convert_to_geojson', () => ({
import { convertToGeoJson } from 'ui/vis/map/convert_to_geojson';
describe('interpreter/functions#tilemap', () => {
const fn = functionWrapper(createTileMapFn);
const fn = functionWrapper(createTileMapFn());
const context = {
type: 'kibana_datatable',
rows: [{ 'col-0-1': 0 }],

View file

@ -18,11 +18,11 @@
*/
// eslint-disable-next-line
import { functionWrapper } from '../../../../plugins/expressions/public/functions/tests/utils';
import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils';
import { createMarkdownVisFn } from './markdown_fn';
describe('interpreter/functions#markdown', () => {
const fn = functionWrapper(createMarkdownVisFn);
const fn = functionWrapper(createMarkdownVisFn());
const args = {
font: { spec: { fontSize: 12 } },
openLinksInNewTab: true,

View file

@ -18,31 +18,23 @@
*/
import { i18n } from '@kbn/i18n';
import { ExpressionFunction, Render } from '../../../../plugins/expressions/public';
import { ExpressionFunctionDefinition, Render } from '../../../../plugins/expressions/public';
import { Arguments, MarkdownVisParams } from './types';
const name = 'markdownVis';
type Context = undefined;
interface RenderValue {
visType: 'markdown';
visConfig: MarkdownVisParams;
}
type Return = Promise<Render<RenderValue>>;
export const createMarkdownVisFn = (): ExpressionFunction<
typeof name,
Context,
export const createMarkdownVisFn = (): ExpressionFunctionDefinition<
'markdownVis',
unknown,
Arguments,
Return
Render<RenderValue>
> => ({
name,
name: 'markdownVis',
type: 'render',
context: {
types: [],
},
inputTypes: [],
help: i18n.translate('visTypeMarkdown.function.help', {
defaultMessage: 'Markdown visualization',
}),
@ -70,7 +62,7 @@ export const createMarkdownVisFn = (): ExpressionFunction<
}),
},
},
async fn(context, args) {
fn(input, args) {
return {
type: 'render',
as: 'visualization',

View file

@ -19,13 +19,11 @@
import { last, findIndex, isNaN } from 'lodash';
import React, { Component } from 'react';
import { isColorDark } from '@elastic/eui';
import { getFormat } from '../legacy_imports';
import { MetricVisValue } from './metric_vis_value';
import { Input } from '../metric_vis_fn';
import { FieldFormatsContentType, IFieldFormat } from '../../../../../plugins/data/public';
import { Context } from '../metric_vis_fn';
import { KibanaDatatable } from '../../../../../plugins/expressions/public';
import { getHeatmapColors } from '../../../../../plugins/charts/public';
import { VisParams, MetricVisMetric } from '../types';
@ -33,7 +31,7 @@ import { SchemaConfig, Vis } from '../../../visualizations/public';
export interface MetricVisComponentProps {
visParams: VisParams;
visData: Context;
visData: Input;
vis: Vis;
renderComplete: () => void;
}

View file

@ -19,12 +19,12 @@
import { createMetricVisFn } from './metric_vis_fn';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { functionWrapper } from '../../../../plugins/expressions/public/functions/tests/utils';
import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils';
jest.mock('ui/new_platform');
describe('interpreter/functions#metric', () => {
const fn = functionWrapper(createMetricVisFn);
const fn = functionWrapper(createMetricVisFn());
const context = {
type: 'kibana_datatable',
rows: [{ 'col-0-1': 0 }],

View file

@ -20,7 +20,7 @@
import { i18n } from '@kbn/i18n';
import {
ExpressionFunction,
ExpressionFunctionDefinition,
KibanaDatatable,
Range,
Render,
@ -30,9 +30,7 @@ import { ColorModes } from '../../vis_type_vislib/public';
import { visType, DimensionsVisParam, VisParams } from './types';
import { ColorSchemas, vislibColorMaps } from '../../../../plugins/charts/public';
export type Context = KibanaDatatable;
const name = 'metricVis';
export type Input = KibanaDatatable;
interface Arguments {
percentageMode: boolean;
@ -51,24 +49,20 @@ interface Arguments {
interface RenderValue {
visType: typeof visType;
visData: Context;
visData: Input;
visConfig: Pick<VisParams, 'metric' | 'dimensions'>;
params: any;
}
type Return = Render<RenderValue>;
export const createMetricVisFn = (): ExpressionFunction<
typeof name,
Context,
export const createMetricVisFn = (): ExpressionFunctionDefinition<
'metricVis',
Input,
Arguments,
Return
Render<RenderValue>
> => ({
name,
name: 'metricVis',
type: 'render',
context: {
types: ['kibana_datatable'],
},
inputTypes: ['kibana_datatable'],
help: i18n.translate('visTypeMetric.function.help', {
defaultMessage: 'Metric visualization',
}),
@ -165,7 +159,7 @@ export const createMetricVisFn = (): ExpressionFunction<
}),
},
},
fn(context: Context, args: Arguments) {
fn(input, args) {
const dimensions: DimensionsVisParam = {
metrics: args.metric,
};
@ -184,7 +178,7 @@ export const createMetricVisFn = (): ExpressionFunction<
type: 'render',
as: 'visualization',
value: {
visData: context,
visData: input,
visType,
visConfig: {
metric: {

View file

@ -21,7 +21,7 @@ import { createTableVisFn } from './table_vis_fn';
import { tableVisResponseHandler } from './table_vis_response_handler';
// eslint-disable-next-line
import { functionWrapper } from '../../../../plugins/expressions/public/functions/tests/utils';
import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils';
jest.mock('./table_vis_response_handler', () => ({
tableVisResponseHandler: jest.fn().mockReturnValue({
@ -30,7 +30,7 @@ jest.mock('./table_vis_response_handler', () => ({
}));
describe('interpreter/functions#table', () => {
const fn = functionWrapper(createTableVisFn);
const fn = functionWrapper(createTableVisFn());
const context = {
type: 'kibana_datatable',
rows: [{ 'col-0-1': 0 }],

View file

@ -19,16 +19,13 @@
import { i18n } from '@kbn/i18n';
import { tableVisResponseHandler, TableContext } from './table_vis_response_handler';
import {
ExpressionFunction,
ExpressionFunctionDefinition,
KibanaDatatable,
Render,
} from '../../../../plugins/expressions/public';
const name = 'kibana_table';
export type Context = KibanaDatatable;
export type Input = KibanaDatatable;
interface Arguments {
visConfig: string | null;
@ -45,19 +42,15 @@ interface RenderValue {
};
}
type Return = Render<RenderValue>;
export const createTableVisFn = (): ExpressionFunction<
typeof name,
Context,
export const createTableVisFn = (): ExpressionFunctionDefinition<
'kibana_table',
Input,
Arguments,
Return
Render<RenderValue>
> => ({
name,
name: 'kibana_table',
type: 'render',
context: {
types: ['kibana_datatable'],
},
inputTypes: ['kibana_datatable'],
help: i18n.translate('visTypeTable.function.help', {
defaultMessage: 'Table visualization',
}),
@ -68,9 +61,9 @@ export const createTableVisFn = (): ExpressionFunction<
help: '',
},
},
fn(context, args) {
fn(input, args) {
const visConfig = args.visConfig && JSON.parse(args.visConfig);
const convertedData = tableVisResponseHandler(context, visConfig.dimensions);
const convertedData = tableVisResponseHandler(input, visConfig.dimensions);
return {
type: 'render',

View file

@ -20,7 +20,7 @@
import { Required } from '@kbn/utility-types';
import { getFormat } from './legacy_imports';
import { Context } from './table_vis_fn';
import { Input } from './table_vis_fn';
export interface TableContext {
tables: Array<TableGroup | Table>;
@ -29,7 +29,7 @@ export interface TableContext {
export interface TableGroup {
$parent: TableContext;
table: Context;
table: Input;
tables: Table[];
title: string;
name: string;
@ -40,11 +40,11 @@ export interface TableGroup {
export interface Table {
$parent?: TableGroup;
columns: Context['columns'];
rows: Context['rows'];
columns: Input['columns'];
rows: Input['rows'];
}
export function tableVisResponseHandler(table: Context, dimensions: any): TableContext {
export function tableVisResponseHandler(table: Input, dimensions: any): TableContext {
const converted: TableContext = {
tables: [],
};
@ -63,8 +63,7 @@ export function tableVisResponseHandler(table: Context, dimensions: any): TableC
const splitValue: any = row[splitColumn.id];
if (!splitMap.hasOwnProperty(splitValue as any)) {
// @ts-ignore
splitMap[splitValue] = splitIndex++;
(splitMap as any)[splitValue] = splitIndex++;
const tableGroup: Required<TableGroup, 'tables'> = {
$parent: converted,
title: `${splitColumnFormatter.convert(splitValue)}: ${splitColumn.name}`,
@ -85,10 +84,8 @@ export function tableVisResponseHandler(table: Context, dimensions: any): TableC
converted.tables.push(tableGroup);
}
// @ts-ignore
const tableIndex = splitMap[splitValue];
// @ts-ignore
converted.tables[tableIndex].tables[0].rows.push(row);
const tableIndex = (splitMap as any)[splitValue];
(converted.tables[tableIndex] as any).tables[0].rows.push(row);
});
} else {
converted.tables.push({

View file

@ -20,10 +20,10 @@
import { createTagCloudFn } from './tag_cloud_fn';
// eslint-disable-next-line
import { functionWrapper } from '../../../../plugins/expressions/public/functions/tests/utils';
import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils';
describe('interpreter/functions#tagcloud', () => {
const fn = functionWrapper(createTagCloudFn);
const fn = functionWrapper(createTagCloudFn());
const context = {
type: 'kibana_datatable',
rows: [{ 'col-0-1': 0 }],

View file

@ -20,7 +20,7 @@
import { i18n } from '@kbn/i18n';
import {
ExpressionFunction,
ExpressionFunctionDefinition,
KibanaDatatable,
Render,
} from '../../../../plugins/expressions/public';
@ -28,8 +28,6 @@ import { TagCloudVisParams } from './types';
const name = 'tagcloud';
type Context = KibanaDatatable;
interface Arguments extends TagCloudVisParams {
metric: any; // these aren't typed yet
bucket: any; // these aren't typed yet
@ -37,24 +35,20 @@ interface Arguments extends TagCloudVisParams {
interface RenderValue {
visType: typeof name;
visData: Context;
visData: KibanaDatatable;
visConfig: Arguments;
params: any;
}
type Return = Render<RenderValue>;
export const createTagCloudFn = (): ExpressionFunction<
export const createTagCloudFn = (): ExpressionFunctionDefinition<
typeof name,
Context,
KibanaDatatable,
Arguments,
Return
Render<RenderValue>
> => ({
name,
type: 'render',
context: {
types: ['kibana_datatable'],
},
inputTypes: ['kibana_datatable'],
help: i18n.translate('visTypeTagCloud.function.help', {
defaultMessage: 'Tagcloud visualization',
}),
@ -104,7 +98,7 @@ export const createTagCloudFn = (): ExpressionFunction<
}),
},
},
fn(context, args) {
fn(input, args) {
const visConfig = {
scale: args.scale,
orientation: args.orientation,
@ -122,7 +116,7 @@ export const createTagCloudFn = (): ExpressionFunction<
type: 'render',
as: 'visualization',
value: {
visData: context,
visData: input,
visType: name,
visConfig,
params: {

View file

@ -19,36 +19,36 @@
import { get } from 'lodash';
import { i18n } from '@kbn/i18n';
import { ExpressionFunction, KibanaContext, Render } from 'src/plugins/expressions/public';
import {
ExpressionFunctionDefinition,
KibanaContext,
Render,
} from 'src/plugins/expressions/public';
import { getTimelionRequestHandler } from './helpers/timelion_request_handler';
import { TIMELION_VIS_NAME } from './timelion_vis_type';
import { TimelionVisDependencies } from './plugin';
const name = 'timelion_vis';
type Input = KibanaContext | null;
type Output = Promise<Render<RenderValue>>;
interface Arguments {
expression: string;
interval: string;
}
interface RenderValue {
visData: Context;
visData: Input;
visType: 'timelion';
visParams: VisParams;
}
type Context = KibanaContext | null;
export type VisParams = Arguments;
type Return = Promise<Render<RenderValue>>;
export const getTimelionVisualizationConfig = (
dependencies: TimelionVisDependencies
): ExpressionFunction<typeof name, Context, Arguments, Return> => ({
name,
): ExpressionFunctionDefinition<'timelion_vis', Input, Arguments, Output> => ({
name: 'timelion_vis',
type: 'render',
context: {
types: ['kibana_context', 'null'],
},
inputTypes: ['kibana_context', 'null'],
help: i18n.translate('timelion.function.help', {
defaultMessage: 'Timelion visualization',
}),
@ -65,15 +65,15 @@ export const getTimelionVisualizationConfig = (
help: '',
},
},
async fn(context, args) {
async fn(input, args) {
const timelionRequestHandler = getTimelionRequestHandler(dependencies);
const visParams = { expression: args.expression, interval: args.interval };
const response = await timelionRequestHandler({
timeRange: get(context, 'timeRange'),
query: get(context, 'query'),
filters: get(context, 'filters'),
timeRange: get(input, 'timeRange'),
query: get(input, 'query'),
filters: get(input, 'filters'),
visParams,
forceFetch: true,
});

View file

@ -19,14 +19,18 @@
import { get } from 'lodash';
import { i18n } from '@kbn/i18n';
import { ExpressionFunction, KibanaContext, Render } from '../../../../plugins/expressions/public';
import {
ExpressionFunctionDefinition,
KibanaContext,
Render,
} from '../../../../plugins/expressions/public';
// @ts-ignore
import { metricsRequestHandler } from './request_handler';
import { PersistedState } from './legacy_imports';
const name = 'tsvb';
type Context = KibanaContext | null;
type Input = KibanaContext | null;
type Output = Promise<Render<RenderValue>>;
interface Arguments {
params: string;
@ -38,19 +42,20 @@ type VisParams = Required<Arguments>;
interface RenderValue {
visType: 'metrics';
visData: Context;
visData: Input;
visConfig: VisParams;
uiState: any;
}
type Return = Promise<Render<RenderValue>>;
export const createMetricsFn = (): ExpressionFunction<typeof name, Context, Arguments, Return> => ({
name,
export const createMetricsFn = (): ExpressionFunctionDefinition<
'tsvb',
Input,
Arguments,
Output
> => ({
name: 'tsvb',
type: 'render',
context: {
types: ['kibana_context', 'null'],
},
inputTypes: ['kibana_context', 'null'],
help: i18n.translate('visTypeTimeseries.function.help', {
defaultMessage: 'TSVB visualization',
}),
@ -71,16 +76,16 @@ export const createMetricsFn = (): ExpressionFunction<typeof name, Context, Argu
help: '',
},
},
async fn(context: Context, args: Arguments) {
async fn(input, args) {
const params = JSON.parse(args.params);
const uiStateParams = JSON.parse(args.uiState);
const savedObjectId = args.savedObjectId;
const uiState = new PersistedState(uiStateParams);
const response = await metricsRequestHandler({
timeRange: get(context, 'timeRange', null),
query: get(context, 'query', null),
filters: get(context, 'filters', null),
timeRange: get(input, 'timeRange', null),
query: get(input, 'query', null),
filters: get(input, 'filters', null),
visParams: params,
uiState,
savedObjectId,

View file

@ -19,13 +19,16 @@
import { get } from 'lodash';
import { i18n } from '@kbn/i18n';
import { ExpressionFunction, KibanaContext, Render } from '../../../../plugins/expressions/public';
import {
ExpressionFunctionDefinition,
KibanaContext,
Render,
} from '../../../../plugins/expressions/public';
import { VegaVisualizationDependencies } from './plugin';
import { createVegaRequestHandler } from './vega_request_handler';
const name = 'vega';
type Context = KibanaContext | null;
type Input = KibanaContext | null;
type Output = Promise<Render<RenderValue>>;
interface Arguments {
spec: string;
@ -34,21 +37,17 @@ interface Arguments {
export type VisParams = Required<Arguments>;
interface RenderValue {
visData: Context;
visType: typeof name;
visData: Input;
visType: 'vega';
visConfig: VisParams;
}
type Return = Promise<Render<RenderValue>>;
export const createVegaFn = (
dependencies: VegaVisualizationDependencies
): ExpressionFunction<typeof name, Context, Arguments, Return> => ({
name,
): ExpressionFunctionDefinition<'vega', Input, Arguments, Output> => ({
name: 'vega',
type: 'render',
context: {
types: ['kibana_context', 'null'],
},
inputTypes: ['kibana_context', 'null'],
help: i18n.translate('visTypeVega.function.help', {
defaultMessage: 'Vega visualization',
}),
@ -59,13 +58,13 @@ export const createVegaFn = (
help: '',
},
},
async fn(context, args) {
async fn(input, args) {
const vegaRequestHandler = createVegaRequestHandler(dependencies);
const response = await vegaRequestHandler({
timeRange: get(context, 'timeRange'),
query: get(context, 'query'),
filters: get(context, 'filters'),
timeRange: get(input, 'timeRange'),
query: get(input, 'query'),
filters: get(input, 'filters'),
visParams: { spec: args.spec },
});

View file

@ -18,7 +18,7 @@
*/
// eslint-disable-next-line
import { functionWrapper } from '../../../../plugins/expressions/public/functions/tests/utils';
import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils';
import { createPieVisFn } from './pie_fn';
// @ts-ignore
import { vislibSlicesResponseHandler } from './vislib/response_handler';
@ -42,7 +42,7 @@ jest.mock('./vislib/response_handler', () => ({
}));
describe('interpreter/functions#pie', () => {
const fn = functionWrapper(createPieVisFn);
const fn = functionWrapper(createPieVisFn());
const context = {
type: 'kibana_datatable',
rows: [{ 'col-0-1': 0 }],

View file

@ -18,19 +18,14 @@
*/
import { i18n } from '@kbn/i18n';
import {
ExpressionFunction,
ExpressionFunctionDefinition,
KibanaDatatable,
Render,
} from '../../../../plugins/expressions/public';
// @ts-ignore
import { vislibSlicesResponseHandler } from './vislib/response_handler';
const name = 'kibana_pie';
type Context = KibanaDatatable;
interface Arguments {
visConfig: string;
}
@ -41,14 +36,15 @@ interface RenderValue {
visConfig: VisParams;
}
type Return = Render<RenderValue>;
export const createPieVisFn = (): ExpressionFunction<typeof name, Context, Arguments, Return> => ({
export const createPieVisFn = (): ExpressionFunctionDefinition<
'kibana_pie',
KibanaDatatable,
Arguments,
Render<RenderValue>
> => ({
name: 'kibana_pie',
type: 'render',
context: {
types: ['kibana_datatable'],
},
inputTypes: ['kibana_datatable'],
help: i18n.translate('visTypeVislib.functions.pie.help', {
defaultMessage: 'Pie visualization',
}),
@ -59,9 +55,9 @@ export const createPieVisFn = (): ExpressionFunction<typeof name, Context, Argum
help: '',
},
},
fn(context, args) {
fn(input, args) {
const visConfig = JSON.parse(args.visConfig);
const convertedData = vislibSlicesResponseHandler(context, visConfig.dimensions);
const convertedData = vislibSlicesResponseHandler(input, visConfig.dimensions);
return {
type: 'render',

View file

@ -83,7 +83,7 @@ export class VisTypeVislibPlugin implements Plugin<Promise<void>, void> {
createGaugeVisTypeDefinition,
createGoalVisTypeDefinition,
];
const vislibFns = [createVisTypeVislibVisFn, createPieVisFn];
const vislibFns = [createVisTypeVislibVisFn(), createPieVisFn()];
const visTypeXy = core.injectedMetadata.getInjectedVar('visTypeXy') as
| VisTypeXyConfigSchema['visTypeXy']

View file

@ -18,19 +18,14 @@
*/
import { i18n } from '@kbn/i18n';
import {
ExpressionFunction,
ExpressionFunctionDefinition,
KibanaDatatable,
Render,
} from '../../../../plugins/expressions/public';
// @ts-ignore
import { vislibSeriesResponseHandler } from './vislib/response_handler';
const name = 'vislib';
type Context = KibanaDatatable;
interface Arguments {
type: string;
visConfig: string;
@ -43,19 +38,15 @@ interface RenderValue {
visConfig: VisParams;
}
type Return = Render<RenderValue>;
export const createVisTypeVislibVisFn = (): ExpressionFunction<
typeof name,
Context,
export const createVisTypeVislibVisFn = (): ExpressionFunctionDefinition<
'vislib',
KibanaDatatable,
Arguments,
Return
Render<RenderValue>
> => ({
name: 'vislib',
type: 'render',
context: {
types: ['kibana_datatable'],
},
inputTypes: ['kibana_datatable'],
help: i18n.translate('visTypeVislib.functions.vislib.help', {
defaultMessage: 'Vislib visualization',
}),

View file

@ -350,7 +350,6 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
private async updateHandler() {
const expressionParams: IExpressionLoaderParams = {
searchContext: {
type: 'kibana_context',
timeRange: this.timeRange,
query: this.input.query,
filters: this.input.filters,

View file

@ -20,7 +20,7 @@
import { get } from 'lodash';
import { i18n } from '@kbn/i18n';
import { VisResponseValue } from 'src/plugins/visualizations/public';
import { ExpressionFunction, Render } from 'src/plugins/expressions/public';
import { ExpressionFunctionDefinition, Render } from 'src/plugins/expressions/public';
import { PersistedState } from '../../../legacy_imports';
import { getTypes, getIndexPatterns, getFilterManager } from '../services';
@ -34,7 +34,7 @@ interface Arguments {
uiState?: string;
}
export type ExpressionFunctionVisualization = ExpressionFunction<
export type ExpressionFunctionVisualization = ExpressionFunctionDefinition<
'visualization',
any,
Arguments,
@ -86,7 +86,7 @@ export const visualization = (): ExpressionFunctionVisualization => ({
help: 'User interface state',
},
},
async fn(context, args, handlers) {
async fn(input, args, { inspectorAdapters }) {
const visConfigParams = args.visConfig ? JSON.parse(args.visConfig) : {};
const schemas = args.schemas ? JSON.parse(args.schemas) : {};
const visType = getTypes().get(args.type || 'histogram') as any;
@ -96,25 +96,25 @@ export const visualization = (): ExpressionFunctionVisualization => ({
const uiState = new PersistedState(uiStateParams);
if (typeof visType.requestHandler === 'function') {
context = await visType.requestHandler({
input = await visType.requestHandler({
partialRows: args.partialRows,
metricsAtAllLevels: args.metricsAtAllLevels,
index: indexPattern,
visParams: visConfigParams,
timeRange: get(context, 'timeRange', null),
query: get(context, 'query', null),
filters: get(context, 'filters', null),
timeRange: get(input, 'timeRange', null),
query: get(input, 'query', null),
filters: get(input, 'filters', null),
uiState,
inspectorAdapters: handlers.inspectorAdapters,
inspectorAdapters,
queryFilter: getFilterManager(),
forceFetch: true,
});
}
if (typeof visType.responseHandler === 'function') {
if (context.columns) {
if (input.columns) {
// assign schemas to aggConfigs
context.columns.forEach((column: any) => {
input.columns.forEach((column: any) => {
if (column.aggConfig) {
column.aggConfig.aggConfigs.schemas = visType.schemas.all;
}
@ -122,21 +122,21 @@ export const visualization = (): ExpressionFunctionVisualization => ({
Object.keys(schemas).forEach(key => {
schemas[key].forEach((i: any) => {
if (context.columns[i] && context.columns[i].aggConfig) {
context.columns[i].aggConfig.schema = key;
if (input.columns[i] && input.columns[i].aggConfig) {
input.columns[i].aggConfig.schema = key;
}
});
});
}
context = await visType.responseHandler(context, visConfigParams.dimensions);
input = await visType.responseHandler(input, visConfigParams.dimensions);
}
return {
type: 'render',
as: 'visualization',
value: {
visData: context,
visData: input,
visType: args.type || '',
visConfig: visConfigParams,
},

View file

@ -0,0 +1,35 @@
# `expressions` plugin
This plugin provides methods which will parse & execute an *expression pipeline*
string for you, as well as a series of registries for advanced users who might
want to incorporate their own functions, types, and renderers into the service
for use in their own application.
Expression pipeline is a chain of functions that *pipe* its output to the
input of the next function. Functions can be configured using arguments provided
by the user. The final output of the expression pipeline can be rendered using
one of the *renderers* registered in `expressions` plugin.
Expressions power visualizations in Dashboard and Lens, as well as, every
*element* in Canvas is backed by an expression.
Below is an example of one Canvas element that fetches data using `essql` function,
pipes it further to `math` and `metric` functions, and final `render` function
renders the result.
```
filters
| essql
query="SELECT COUNT(timestamp) as total_errors
FROM kibana_sample_data_logs
WHERE tags LIKE '%warning%' OR tags LIKE '%error%'"
| math "total_errors"
| metric "TOTAL ISSUES"
metricFont={font family="'Open Sans', Helvetica, Arial, sans-serif" size=48 align="left" color="#FFFFFF" weight="normal" underline=false italic=false}
labelFont={font family="'Open Sans', Helvetica, Arial, sans-serif" size=30 align="left" color="#FFFFFF" weight="lighter" underline=false italic=false}
| render
```
![image](https://user-images.githubusercontent.com/9773803/74162514-3250a880-4c21-11ea-9e68-86f66862a183.png)
[See Canvas documentation about expressions](https://www.elastic.co/guide/en/kibana/current/canvas-function-arguments.html).

View file

@ -0,0 +1,39 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { formatExpression } from './format';
describe('formatExpression()', () => {
test('converts expression AST to string', () => {
const str = formatExpression({
type: 'expression',
chain: [
{
type: 'function',
arguments: {
bar: ['baz'],
},
function: 'foo',
},
],
});
expect(str).toMatchInlineSnapshot(`"foo bar=\\"baz\\""`);
});
});

View file

@ -0,0 +1,34 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ExpressionAstExpression, ExpressionAstArgument } from './types';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { toExpression } = require('@kbn/interpreter/common');
export function format(
ast: ExpressionAstExpression | ExpressionAstArgument,
type: 'expression' | 'argument'
): string {
return toExpression(ast, type);
}
export function formatExpression(ast: ExpressionAstExpression): string {
return format(ast, 'expression');
}

View file

@ -0,0 +1,23 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export * from './types';
export * from './parse';
export * from './parse_expression';
export * from './format';

View file

@ -0,0 +1,44 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { parse } from './parse';
describe('parse()', () => {
test('parses an expression', () => {
const ast = parse('foo bar="baz"', 'expression');
expect(ast).toMatchObject({
type: 'expression',
chain: [
{
type: 'function',
arguments: {
bar: ['baz'],
},
function: 'foo',
},
],
});
});
test('parses an argument', () => {
const arg = parse('foo', 'argument');
expect(arg).toBe('foo');
});
});

View file

@ -0,0 +1,34 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ExpressionAstExpression, ExpressionAstArgument } from './types';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { parse: parseRaw } = require('@kbn/interpreter/common');
export function parse(
expression: string,
startRule: 'expression' | 'argument'
): ExpressionAstExpression | ExpressionAstArgument {
try {
return parseRaw(String(expression), { startRule });
} catch (e) {
throw new Error(`Unable to parse expression: ${e.message}`);
}
}

View file

@ -0,0 +1,68 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { parseExpression } from './parse_expression';
describe('parseExpression()', () => {
test('parses an expression', () => {
const ast = parseExpression('foo bar="baz"');
expect(ast).toMatchObject({
type: 'expression',
chain: [
{
type: 'function',
arguments: {
bar: ['baz'],
},
function: 'foo',
},
],
});
});
test('parses an expression with sub-expression', () => {
const ast = parseExpression('foo bar="baz" quux={quix}');
expect(ast).toMatchObject({
type: 'expression',
chain: [
{
type: 'function',
arguments: {
bar: ['baz'],
quux: [
{
type: 'expression',
chain: [
{
type: 'function',
function: 'quix',
arguments: {},
},
],
},
],
},
function: 'foo',
},
],
});
});
});

View file

@ -0,0 +1,30 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ExpressionAstExpression } from './types';
import { parse } from './parse';
/**
* Given expression pipeline string, returns parsed AST.
*
* @param expression Expression pipeline string.
*/
export function parseExpression(expression: string): ExpressionAstExpression {
return parse(expression, 'expression') as ExpressionAstExpression;
}

View file

@ -0,0 +1,36 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export type ExpressionAstNode =
| ExpressionAstExpression
| ExpressionAstFunction
| ExpressionAstArgument;
export interface ExpressionAstExpression {
type: 'expression';
chain: ExpressionAstFunction[];
}
export interface ExpressionAstFunction {
type: 'function';
function: string;
arguments: Record<string, ExpressionAstArgument[]>;
}
export type ExpressionAstArgument = string | boolean | number | ExpressionAstExpression;

View file

@ -0,0 +1,108 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
StateContainer,
createStateContainer,
} from '../../../kibana_utils/common/state_containers';
import { ExecutorState, defaultState as executorDefaultState } from '../executor';
import { ExpressionAstExpression } from '../ast';
import { ExpressionValue } from '../expression_types';
export interface ExecutionState<Output = ExpressionValue> extends ExecutorState {
ast: ExpressionAstExpression;
/**
* Tracks state of execution.
*
* - `not-started` - before .start() method was called.
* - `pending` - immediately after .start() method is called.
* - `result` - when expression execution completed.
* - `error` - when execution failed with error.
*/
state: 'not-started' | 'pending' | 'result' | 'error';
/**
* Result of the expression execution.
*/
result?: Output;
/**
* Error happened during the execution.
*/
error?: Error;
}
const executionDefaultState: ExecutionState = {
...executorDefaultState,
state: 'not-started',
ast: {
type: 'expression',
chain: [],
},
};
// eslint-disable-next-line
export interface ExecutionPureTransitions<Output = ExpressionValue> {
start: (state: ExecutionState<Output>) => () => ExecutionState<Output>;
setResult: (state: ExecutionState<Output>) => (result: Output) => ExecutionState<Output>;
setError: (state: ExecutionState<Output>) => (error: Error) => ExecutionState<Output>;
}
export const executionPureTransitions: ExecutionPureTransitions = {
start: state => () => ({
...state,
state: 'pending',
}),
setResult: state => result => ({
...state,
state: 'result',
result,
}),
setError: state => error => ({
...state,
state: 'error',
error,
}),
};
export type ExecutionContainer<Output = ExpressionValue> = StateContainer<
ExecutionState<Output>,
ExecutionPureTransitions<Output>
>;
const freeze = <T>(state: T): T => state;
export const createExecutionContainer = <Output = ExpressionValue>(
state: ExecutionState<Output> = executionDefaultState
): ExecutionContainer<Output> => {
const container = createStateContainer<
ExecutionState<Output>,
ExecutionPureTransitions<Output>,
object
>(
state,
executionPureTransitions,
{},
{
freeze,
}
);
return container;
};

View file

@ -0,0 +1,372 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Execution } from './execution';
import { parseExpression } from '../ast';
import { createUnitTestExecutor } from '../test_helpers';
import { ExpressionFunctionDefinition } from '../../public';
const createExecution = (
expression: string = 'foo bar=123',
context: Record<string, unknown> = {}
) => {
const executor = createUnitTestExecutor();
const execution = new Execution({
executor,
ast: parseExpression(expression),
context,
});
return execution;
};
const run = async (
expression: string = 'foo bar=123',
context?: Record<string, unknown>,
input: any = null
) => {
const execution = createExecution(expression, context);
execution.start(input);
return await execution.result;
};
describe('Execution', () => {
test('can instantiate', () => {
const execution = createExecution('foo bar=123');
expect(execution.params.ast.chain[0].arguments.bar).toEqual([123]);
});
test('initial input is null at creation', () => {
const execution = createExecution();
expect(execution.input).toBe(null);
});
test('creates default ExecutionContext', () => {
const execution = createExecution();
expect(execution.context).toMatchObject({
getInitialInput: expect.any(Function),
variables: expect.any(Object),
types: expect.any(Object),
});
});
test('executes a single clog function in expression pipeline', async () => {
const execution = createExecution('clog');
/* eslint-disable no-console */
const console$log = console.log;
const spy = (console.log = jest.fn());
/* eslint-enable no-console */
execution.start(123);
const result = await execution.result;
expect(result).toBe(123);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith(123);
/* eslint-disable no-console */
console.log = console$log;
/* eslint-enable no-console */
});
test('executes a chain of multiple "add" functions', async () => {
const execution = createExecution('add val=1 | add val=2 | add val=3');
execution.start({
type: 'num',
value: -1,
});
const result = await execution.result;
expect(result).toEqual({
type: 'num',
value: 5,
});
});
test('executes a chain of "add" and "mult" functions', async () => {
const execution = createExecution('add val=5 | mult val=-1 | add val=-10 | mult val=2');
execution.start({
type: 'num',
value: 0,
});
const result = await execution.result;
expect(result).toEqual({
type: 'num',
value: -30,
});
});
test('casts input to correct type', async () => {
const execution = createExecution('add val=1');
// Below 1 is cast to { type: 'num', value: 1 }.
execution.start(1);
const result = await execution.result;
expect(result).toEqual({
type: 'num',
value: 2,
});
});
describe('execution context', () => {
test('context.variables is an object', async () => {
const { result } = (await run('introspectContext key="variables"')) as any;
expect(typeof result).toBe('object');
});
test('context.types is an object', async () => {
const { result } = (await run('introspectContext key="types"')) as any;
expect(typeof result).toBe('object');
});
test('context.abortSignal is an object', async () => {
const { result } = (await run('introspectContext key="abortSignal"')) as any;
expect(typeof result).toBe('object');
});
test('context.inspectorAdapters is an object', async () => {
const { result } = (await run('introspectContext key="inspectorAdapters"')) as any;
expect(typeof result).toBe('object');
});
test('unknown context key is undefined', async () => {
const { result } = (await run('introspectContext key="foo"')) as any;
expect(typeof result).toBe('undefined');
});
test('can set context variables', async () => {
const variables = { foo: 'bar' };
const result = await run('var name="foo"', { variables });
expect(result).toBe('bar');
});
});
describe('inspector adapters', () => {
test('by default, "data" and "requests" inspector adapters are available', async () => {
const { result } = (await run('introspectContext key="inspectorAdapters"')) as any;
expect(result).toMatchObject({
data: expect.any(Object),
requests: expect.any(Object),
});
});
test('can set custom inspector adapters', async () => {
const inspectorAdapters = {};
const { result } = (await run('introspectContext key="inspectorAdapters"', {
inspectorAdapters,
})) as any;
expect(result).toBe(inspectorAdapters);
});
test('can access custom inspector adapters on Execution object', async () => {
const inspectorAdapters = {};
const execution = createExecution('introspectContext key="inspectorAdapters"', {
inspectorAdapters,
});
expect(execution.inspectorAdapters).toBe(inspectorAdapters);
});
});
describe('expression abortion', () => {
test('context has abortSignal object', async () => {
const { result } = (await run('introspectContext key="abortSignal"')) as any;
expect(typeof result).toBe('object');
expect((result as AbortSignal).aborted).toBe(false);
});
});
describe('expression execution', () => {
test('supports default argument alias _', async () => {
const execution = createExecution('add val=1 | add 2');
execution.start({
type: 'num',
value: 0,
});
const result = await execution.result;
expect(result).toEqual({
type: 'num',
value: 3,
});
});
test('can execute async functions', async () => {
const res = await run('sleep 10 | sleep 10');
expect(res).toBe(null);
});
test('result is undefined until execution completes', async () => {
const execution = createExecution('sleep 10');
expect(execution.state.get().result).toBe(undefined);
execution.start(null);
expect(execution.state.get().result).toBe(undefined);
await new Promise(r => setTimeout(r, 1));
expect(execution.state.get().result).toBe(undefined);
await new Promise(r => setTimeout(r, 11));
expect(execution.state.get().result).toBe(null);
});
});
describe('when function throws', () => {
test('error is reported in output object', async () => {
const result = await run('error "foobar"');
expect(result).toMatchObject({
type: 'error',
});
});
test('error message is prefixed with function name', async () => {
const result = await run('error "foobar"');
expect(result).toMatchObject({
error: {
message: `[error] > foobar`,
},
});
});
test('returns error of the first function that throws', async () => {
const result = await run('error "foo" | error "bar"');
expect(result).toMatchObject({
error: {
message: `[error] > foo`,
},
});
});
test('when function throws, execution still succeeds', async () => {
const execution = await createExecution('error "foo"');
execution.start(null);
const result = await execution.result;
expect(result).toMatchObject({
type: 'error',
});
expect(execution.state.get().state).toBe('result');
expect(execution.state.get().result).toMatchObject({
type: 'error',
});
});
test('does not execute remaining functions in pipeline', async () => {
const spy: ExpressionFunctionDefinition<'spy', any, {}, any> = {
name: 'spy',
args: {},
help: '',
fn: jest.fn(),
};
const executor = createUnitTestExecutor();
executor.registerFunction(spy);
await executor.run('error "..." | spy', null);
expect(spy.fn).toHaveBeenCalledTimes(0);
});
});
describe('state', () => {
test('execution state is "not-started" before .start() is called', async () => {
const execution = createExecution('var foo');
expect(execution.state.get().state).toBe('not-started');
});
test('execution state is "pending" after .start() was called', async () => {
const execution = createExecution('var foo');
execution.start(null);
expect(execution.state.get().state).toBe('pending');
});
test('execution state is "pending" while execution is in progress', async () => {
const execution = createExecution('sleep 20');
execution.start(null);
await new Promise(r => setTimeout(r, 5));
expect(execution.state.get().state).toBe('pending');
});
test('execution state is "result" when execution successfully completes', async () => {
const execution = createExecution('sleep 1');
execution.start(null);
await new Promise(r => setTimeout(r, 30));
expect(execution.state.get().state).toBe('result');
});
test('execution state is "result" when execution successfully completes - 2', async () => {
const execution = createExecution('var foo');
execution.start(null);
await execution.result;
expect(execution.state.get().state).toBe('result');
});
});
describe('sub-expressions', () => {
test('executes sub-expressions', async () => {
const result = await run('add val={add 5 | access "value"}', {}, null);
expect(result).toMatchObject({
type: 'num',
value: 5,
});
});
});
describe('when arguments are missing', () => {
test('when required argument is missing and has not alias, returns error', async () => {
const requiredArg: ExpressionFunctionDefinition<'requiredArg', any, { arg: any }, any> = {
name: 'requiredArg',
args: {
arg: {
help: '',
required: true,
},
},
help: '',
fn: jest.fn(),
};
const executor = createUnitTestExecutor();
executor.registerFunction(requiredArg);
const result = await executor.run('requiredArg', null, {});
expect(result).toMatchObject({
type: 'error',
error: {
message: '[requiredArg] > requiredArg requires an argument',
},
});
});
test('when required argument is missing and has alias, returns error', async () => {
const result = await run('var_set', {});
expect(result).toMatchObject({
type: 'error',
error: {
message: '[var_set] > var_set requires an "name" argument',
},
});
});
});
});

View file

@ -0,0 +1,330 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { keys, last, mapValues, reduce, zipObject } from 'lodash';
import { Executor } from '../executor';
import { createExecutionContainer, ExecutionContainer } from './container';
import { createError } from '../util';
import { Defer } from '../../../kibana_utils/common';
import { RequestAdapter, DataAdapter } from '../../../inspector/common';
import { isExpressionValueError } from '../expression_types/specs/error';
import { ExpressionAstExpression, ExpressionAstFunction, parse } from '../ast';
import { ExecutionContext, DefaultInspectorAdapters } from './types';
import { getType } from '../expression_types';
import { ArgumentType, ExpressionFunction } from '../expression_functions';
import { getByAlias } from '../util/get_by_alias';
export interface ExecutionParams<
ExtraContext extends Record<string, unknown> = Record<string, unknown>
> {
executor: Executor<any>;
ast: ExpressionAstExpression;
context?: ExtraContext;
}
const createDefaultInspectorAdapters = (): DefaultInspectorAdapters => ({
requests: new RequestAdapter(),
data: new DataAdapter(),
});
export class Execution<
ExtraContext extends Record<string, unknown> = Record<string, unknown>,
Input = unknown,
Output = unknown,
InspectorAdapters = ExtraContext['inspectorAdapters'] extends object
? ExtraContext['inspectorAdapters']
: DefaultInspectorAdapters
> {
/**
* Dynamic state of the execution.
*/
public readonly state: ExecutionContainer<Output>;
/**
* Initial input of the execution.
*
* N.B. It is initialized to `null` rather than `undefined` for legacy reasons,
* because in legacy interpreter it was set to `null` by default.
*/
public input: Input = null as any;
/**
* Execution context - object that allows to do side-effects. Context is passed
* to every function.
*/
public readonly context: ExecutionContext<Input, InspectorAdapters> & ExtraContext;
/**
* AbortController to cancel this Execution.
*/
private readonly abortController = new AbortController();
/**
* Whether .start() method has been called.
*/
private hasStarted: boolean = false;
/**
* Future that tracks result or error of this execution.
*/
private readonly firstResultFuture = new Defer<Output>();
public get result(): Promise<unknown> {
return this.firstResultFuture.promise;
}
public get inspectorAdapters(): InspectorAdapters {
return this.context.inspectorAdapters;
}
constructor(public readonly params: ExecutionParams<ExtraContext>) {
const { executor, ast } = params;
this.state = createExecutionContainer<Output>({
...executor.state.get(),
state: 'not-started',
ast,
});
this.context = {
getInitialInput: () => this.input,
variables: {},
types: executor.getTypes(),
abortSignal: this.abortController.signal,
...(params.context || ({} as ExtraContext)),
inspectorAdapters: (params.context && params.context.inspectorAdapters
? params.context.inspectorAdapters
: createDefaultInspectorAdapters()) as InspectorAdapters,
};
}
/**
* Stop execution of expression.
*/
cancel() {
this.abortController.abort();
}
/**
* Call this method to start execution.
*
* N.B. `input` is initialized to `null` rather than `undefined` for legacy reasons,
* because in legacy interpreter it was set to `null` by default.
*/
public start(input: Input = null as any) {
if (this.hasStarted) throw new Error('Execution already started.');
this.hasStarted = true;
this.input = input;
this.state.transitions.start();
const { resolve, reject } = this.firstResultFuture;
this.invokeChain(this.state.get().ast.chain, input).then(resolve, reject);
this.firstResultFuture.promise.then(
result => {
this.state.transitions.setResult(result);
},
error => {
this.state.transitions.setError(error);
}
);
}
async invokeChain(chainArr: ExpressionAstFunction[], input: unknown): Promise<any> {
if (!chainArr.length) return input;
for (const link of chainArr) {
// if execution was aborted return error
if (this.context.abortSignal && this.context.abortSignal.aborted) {
return createError({
message: 'The expression was aborted.',
name: 'AbortError',
});
}
const { function: fnName, arguments: fnArgs } = link;
const fnDef = getByAlias(this.state.get().functions, fnName);
if (!fnDef) {
return createError({ message: `Function ${fnName} could not be found.` });
}
try {
// Resolve arguments before passing to function
// resolveArgs returns an object because the arguments themselves might
// actually have a 'then' function which would be treated as a promise
const { resolvedArgs } = await this.resolveArgs(fnDef, input, fnArgs);
const output = await this.invokeFunction(fnDef, input, resolvedArgs);
if (getType(output) === 'error') return output;
input = output;
} catch (e) {
e.message = `[${fnName}] > ${e.message}`;
return createError(e);
}
}
return input;
}
async invokeFunction(
fn: ExpressionFunction,
input: unknown,
args: Record<string, unknown>
): Promise<any> {
const normalizedInput = this.cast(input, fn.inputTypes);
const output = await fn.fn(normalizedInput, args, this.context);
// Validate that the function returned the type it said it would.
// This isn't required, but it keeps function developers honest.
const returnType = getType(output);
const expectedType = fn.type;
if (expectedType && returnType !== expectedType) {
throw new Error(
`Function '${fn.name}' should return '${expectedType}',` +
` actually returned '${returnType}'`
);
}
// Validate the function output against the type definition's validate function.
const type = this.context.types[fn.type];
if (type && type.validate) {
try {
type.validate(output);
} catch (e) {
throw new Error(`Output of '${fn.name}' is not a valid type '${fn.type}': ${e}`);
}
}
return output;
}
public cast(value: any, toTypeNames?: string[]) {
// If you don't give us anything to cast to, you'll get your input back
if (!toTypeNames || toTypeNames.length === 0) return value;
// No need to cast if node is already one of the valid types
const fromTypeName = getType(value);
if (toTypeNames.includes(fromTypeName)) return value;
const { types } = this.state.get();
const fromTypeDef = types[fromTypeName];
for (const toTypeName of toTypeNames) {
// First check if the current type can cast to this type
if (fromTypeDef && fromTypeDef.castsTo(toTypeName)) {
return fromTypeDef.to(value, toTypeName, types);
}
// If that isn't possible, check if this type can cast from the current type
const toTypeDef = types[toTypeName];
if (toTypeDef && toTypeDef.castsFrom(fromTypeName)) return toTypeDef.from(value, types);
}
throw new Error(`Can not cast '${fromTypeName}' to any of '${toTypeNames.join(', ')}'`);
}
// Processes the multi-valued AST argument values into arguments that can be passed to the function
async resolveArgs(fnDef: ExpressionFunction, input: unknown, argAsts: any): Promise<any> {
const argDefs = fnDef.args;
// Use the non-alias name from the argument definition
const dealiasedArgAsts = reduce(
argAsts,
(acc, argAst, argName) => {
const argDef = getByAlias(argDefs, argName);
if (!argDef) {
throw new Error(`Unknown argument '${argName}' passed to function '${fnDef.name}'`);
}
acc[argDef.name] = (acc[argDef.name] || []).concat(argAst);
return acc;
},
{} as any
);
// Check for missing required arguments.
for (const argDef of Object.values(argDefs)) {
const { aliases, default: argDefault, name: argName, required } = argDef as ArgumentType<
any
> & { name: string };
if (
typeof argDefault !== 'undefined' ||
!required ||
typeof dealiasedArgAsts[argName] !== 'undefined'
)
continue;
if (!aliases || aliases.length === 0) {
throw new Error(`${fnDef.name} requires an argument`);
}
// use an alias if _ is the missing arg
const errorArg = argName === '_' ? aliases[0] : argName;
throw new Error(`${fnDef.name} requires an "${errorArg}" argument`);
}
// Fill in default values from argument definition
const argAstsWithDefaults = reduce(
argDefs,
(acc: any, argDef: any, argName: any) => {
if (typeof acc[argName] === 'undefined' && typeof argDef.default !== 'undefined') {
acc[argName] = [parse(argDef.default, 'argument')];
}
return acc;
},
dealiasedArgAsts
);
// Create the functions to resolve the argument ASTs into values
// These are what are passed to the actual functions if you opt out of resolving
const resolveArgFns = mapValues(argAstsWithDefaults, (asts, argName) => {
return asts.map((item: ExpressionAstExpression) => {
return async (subInput = input) => {
const output = await this.params.executor.interpret(item, subInput);
if (isExpressionValueError(output)) throw output.error;
const casted = this.cast(output, argDefs[argName as any].types);
return casted;
};
});
});
const argNames = keys(resolveArgFns);
// Actually resolve unless the argument definition says not to
const resolvedArgValues = await Promise.all(
argNames.map(argName => {
const interpretFns = resolveArgFns[argName];
if (!argDefs[argName].resolve) return interpretFns;
return Promise.all(interpretFns.map((fn: any) => fn()));
})
);
const resolvedMultiArgs = zipObject(argNames, resolvedArgValues);
// Just return the last unless the argument definition allows multiple
const resolvedArgs = mapValues(resolvedMultiArgs, (argValues, argName) => {
if (argDefs[argName as any].multi) return argValues;
return last(argValues as any);
});
// Return an object here because the arguments themselves might actually have a 'then'
// function which would be treated as a promise
return { resolvedArgs };
}
}

View file

@ -17,6 +17,6 @@
* under the License.
*/
export * from './type_registry';
export * from './function_registry';
export * from './render_registry';
export * from './types';
export * from './container';
export * from './execution';

View file

@ -0,0 +1,72 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ExpressionType } from '../expression_types';
import { DataAdapter, RequestAdapter } from '../../../inspector/common';
import { TimeRange, Query, esFilters } from '../../../data/common';
/**
* `ExecutionContext` is an object available to all functions during a single execution;
* it provides various methods to perform side-effects.
*/
export interface ExecutionContext<Input = unknown, InspectorAdapters = DefaultInspectorAdapters> {
/**
* Get initial input with which execution started.
*/
getInitialInput: () => Input;
/**
* Context variables that can be consumed using `var` and `var_set` functions.
*/
variables: Record<string, unknown>;
/**
* A map of available expression types.
*/
types: Record<string, ExpressionType>;
/**
* Adds ability to abort current execution.
*/
abortSignal: AbortSignal;
/**
* Adapters for `inspector` plugin.
*/
inspectorAdapters: InspectorAdapters;
/**
* Search context in which expression should operate.
*/
search?: ExecutionContextSearch;
}
/**
* Default inspector adapters created if inspector adapters are not set explicitly.
*/
export interface DefaultInspectorAdapters {
requests: RequestAdapter;
data: DataAdapter;
}
export interface ExecutionContextSearch {
filters?: esFilters.Filter[];
query?: Query | Query[];
timeRange?: TimeRange;
}

View file

@ -0,0 +1,81 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
StateContainer,
createStateContainer,
} from '../../../kibana_utils/common/state_containers';
import { ExpressionFunction } from '../expression_functions';
import { ExpressionType } from '../expression_types';
export interface ExecutorState<Context extends Record<string, unknown> = Record<string, unknown>> {
functions: Record<string, ExpressionFunction>;
types: Record<string, ExpressionType>;
context: Context;
}
export const defaultState: ExecutorState<any> = {
functions: {},
types: {},
context: {},
};
export interface ExecutorPureTransitions {
addFunction: (state: ExecutorState) => (fn: ExpressionFunction) => ExecutorState;
addType: (state: ExecutorState) => (type: ExpressionType) => ExecutorState;
extendContext: (state: ExecutorState) => (extraContext: Record<string, unknown>) => ExecutorState;
}
export const pureTransitions: ExecutorPureTransitions = {
addFunction: state => fn => ({ ...state, functions: { ...state.functions, [fn.name]: fn } }),
addType: state => type => ({ ...state, types: { ...state.types, [type.name]: type } }),
extendContext: state => extraContext => ({
...state,
context: { ...state.context, ...extraContext },
}),
};
export interface ExecutorPureSelectors {
getFunction: (state: ExecutorState) => (id: string) => ExpressionFunction | null;
getType: (state: ExecutorState) => (id: string) => ExpressionType | null;
getContext: (state: ExecutorState) => () => ExecutorState['context'];
}
export const pureSelectors: ExecutorPureSelectors = {
getFunction: state => id => state.functions[id] || null,
getType: state => id => state.types[id] || null,
getContext: ({ context }) => () => context,
};
export type ExecutorContainer<
Context extends Record<string, unknown> = Record<string, unknown>
> = StateContainer<ExecutorState<Context>, ExecutorPureTransitions, ExecutorPureSelectors>;
export const createExecutorContainer = <
Context extends Record<string, unknown> = Record<string, unknown>
>(
state: ExecutorState<Context> = defaultState
): ExecutorContainer<Context> => {
const container = createStateContainer<
ExecutorState<Context>,
ExecutorPureTransitions,
ExecutorPureSelectors
>(state, pureTransitions, pureSelectors);
return container;
};

View file

@ -0,0 +1,155 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Executor } from './executor';
import * as expressionTypes from '../expression_types';
import * as expressionFunctions from '../expression_functions';
import { Execution } from '../execution';
import { parseExpression } from '../ast';
describe('Executor', () => {
test('can instantiate', () => {
new Executor();
});
describe('type registry', () => {
test('can register a type', () => {
const executor = new Executor();
executor.registerType(expressionTypes.datatable);
});
test('can register all types', () => {
const executor = new Executor();
for (const type of expressionTypes.typeSpecs) executor.registerType(type);
});
test('can retrieve all types', () => {
const executor = new Executor();
executor.registerType(expressionTypes.datatable);
const types = executor.getTypes();
expect(Object.keys(types)).toEqual(['datatable']);
});
test('can retrieve all types - 2', () => {
const executor = new Executor();
for (const type of expressionTypes.typeSpecs) executor.registerType(type);
const types = executor.getTypes();
expect(Object.keys(types).sort()).toEqual(
expressionTypes.typeSpecs.map(spec => spec.name).sort()
);
});
});
describe('function registry', () => {
test('can register a function', () => {
const executor = new Executor();
executor.registerFunction(expressionFunctions.clog);
});
test('can register all functions', () => {
const executor = new Executor();
for (const functionDefinition of expressionFunctions.functionSpecs)
executor.registerFunction(functionDefinition);
});
test('can retrieve all functions', () => {
const executor = new Executor();
executor.registerFunction(expressionFunctions.clog);
const functions = executor.getFunctions();
expect(Object.keys(functions)).toEqual(['clog']);
});
test('can retrieve all functions - 2', () => {
const executor = new Executor();
for (const functionDefinition of expressionFunctions.functionSpecs)
executor.registerFunction(functionDefinition);
const functions = executor.getFunctions();
expect(Object.keys(functions).sort()).toEqual(
expressionFunctions.functionSpecs.map(spec => spec.name).sort()
);
});
});
describe('context', () => {
test('context is empty by default', () => {
const executor = new Executor();
expect(executor.context).toEqual({});
});
test('can extend context', () => {
const executor = new Executor();
executor.extendContext({
foo: 'bar',
});
expect(executor.context).toEqual({
foo: 'bar',
});
});
test('can extend context multiple times with multiple keys', () => {
const executor = new Executor();
const abortSignal = {};
const env = {};
executor.extendContext({
foo: 'bar',
});
executor.extendContext({
abortSignal,
env,
});
expect(executor.context).toEqual({
foo: 'bar',
abortSignal,
env,
});
});
});
describe('execution', () => {
describe('createExecution()', () => {
test('returns Execution object from string', () => {
const executor = new Executor();
const execution = executor.createExecution('foo bar="baz"');
expect(execution).toBeInstanceOf(Execution);
expect(execution.params.ast.chain[0].function).toBe('foo');
});
test('returns Execution object from AST', () => {
const executor = new Executor();
const ast = parseExpression('foo bar="baz"');
const execution = executor.createExecution(ast);
expect(execution).toBeInstanceOf(Execution);
expect(execution.params.ast.chain[0].function).toBe('foo');
});
test('Execution inherits context from Executor', () => {
const executor = new Executor();
const foo = {};
executor.extendContext({ foo });
const execution = executor.createExecution('foo bar="baz"');
expect((execution.context as any).foo).toBe(foo);
});
});
});
});

View file

@ -0,0 +1,204 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/* eslint-disable max-classes-per-file */
import { ExecutorState, ExecutorContainer } from './container';
import { createExecutorContainer } from './container';
import { AnyExpressionFunctionDefinition, ExpressionFunction } from '../expression_functions';
import { Execution } from '../execution/execution';
import { IRegistry } from '../types';
import { ExpressionType } from '../expression_types/expression_type';
import { AnyExpressionTypeDefinition } from '../expression_types/types';
import { getType } from '../expression_types';
import { ExpressionAstExpression, ExpressionAstNode, parseExpression } from '../ast';
import { typeSpecs } from '../expression_types/specs';
import { functionSpecs } from '../expression_functions/specs';
export class TypesRegistry implements IRegistry<ExpressionType> {
constructor(private readonly executor: Executor<any>) {}
public register(
typeDefinition: AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition)
) {
this.executor.registerType(typeDefinition);
}
public get(id: string): ExpressionType | null {
return this.executor.state.selectors.getType(id);
}
public toJS(): Record<string, ExpressionType> {
return this.executor.getTypes();
}
public toArray(): ExpressionType[] {
return Object.values(this.toJS());
}
}
export class FunctionsRegistry implements IRegistry<ExpressionFunction> {
constructor(private readonly executor: Executor<any>) {}
public register(
functionDefinition: AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition)
) {
this.executor.registerFunction(functionDefinition);
}
public get(id: string): ExpressionFunction | null {
return this.executor.state.selectors.getFunction(id);
}
public toJS(): Record<string, ExpressionFunction> {
return this.executor.getFunctions();
}
public toArray(): ExpressionFunction[] {
return Object.values(this.toJS());
}
}
export class Executor<Context extends Record<string, unknown> = Record<string, unknown>> {
static createWithDefaults<Ctx extends Record<string, unknown> = Record<string, unknown>>(
state?: ExecutorState<Ctx>
): Executor<Ctx> {
const executor = new Executor<Ctx>(state);
for (const type of typeSpecs) executor.registerType(type);
for (const func of functionSpecs) executor.registerFunction(func);
return executor;
}
public readonly state: ExecutorContainer<Context>;
/**
* @deprecated
*/
public readonly functions: FunctionsRegistry;
/**
* @deprecated
*/
public readonly types: TypesRegistry;
constructor(state?: ExecutorState<Context>) {
this.state = createExecutorContainer<Context>(state);
this.functions = new FunctionsRegistry(this);
this.types = new TypesRegistry(this);
}
public registerFunction(
functionDefinition: AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition)
) {
const fn = new ExpressionFunction(
typeof functionDefinition === 'object' ? functionDefinition : functionDefinition()
);
this.state.transitions.addFunction(fn);
}
public getFunction(name: string): ExpressionFunction | undefined {
return this.state.get().functions[name];
}
public getFunctions(): Record<string, ExpressionFunction> {
return { ...this.state.get().functions };
}
public registerType(
typeDefinition: AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition)
) {
const type = new ExpressionType(
typeof typeDefinition === 'object' ? typeDefinition : typeDefinition()
);
this.state.transitions.addType(type);
}
public getType(name: string): ExpressionType | undefined {
return this.state.get().types[name];
}
public getTypes(): Record<string, ExpressionType> {
return { ...this.state.get().types };
}
public extendContext(extraContext: Record<string, unknown>) {
this.state.transitions.extendContext(extraContext);
}
public get context(): Record<string, unknown> {
return this.state.selectors.getContext();
}
public async interpret<T>(ast: ExpressionAstNode, input: T): Promise<unknown> {
switch (getType(ast)) {
case 'expression':
return await this.interpretExpression(ast as ExpressionAstExpression, input);
case 'string':
case 'number':
case 'null':
case 'boolean':
return ast;
default:
throw new Error(`Unknown AST object: ${JSON.stringify(ast)}`);
}
}
public async interpretExpression<T>(
ast: string | ExpressionAstExpression,
input: T
): Promise<unknown> {
const execution = this.createExecution(ast);
execution.start(input);
return await execution.result;
}
/**
* Execute expression and return result.
*
* @param ast Expression AST or a string representing expression.
* @param input Initial input to the first expression function.
* @param context Extra global context object that will be merged into the
* expression global context object that is provided to each function to allow side-effects.
*/
public async run<
Input,
Output,
ExtraContext extends Record<string, unknown> = Record<string, unknown>
>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext) {
const execution = this.createExecution(ast, context);
execution.start(input);
return (await execution.result) as Output;
}
public createExecution<ExtraContext extends Record<string, unknown> = Record<string, unknown>>(
ast: string | ExpressionAstExpression,
context: ExtraContext = {} as ExtraContext
): Execution<Context & ExtraContext> {
if (typeof ast === 'string') ast = parseExpression(ast);
const execution = new Execution<Context & ExtraContext>({
ast,
executor: this,
context: {
...this.context,
...context,
} as Context & ExtraContext,
});
return execution;
}
}

View file

@ -17,8 +17,5 @@
* under the License.
*/
export function createHandlers() {
return {
environment: 'client',
};
}
export * from './container';
export * from './executor';

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { KnownTypeToString, TypeString, UnmappedTypeStrings } from './common';
import { KnownTypeToString, TypeString, UnmappedTypeStrings } from '../types/common';
/**
* This type represents all of the possible combinations of properties of an

View file

@ -0,0 +1,84 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { AnyExpressionFunctionDefinition } from './types';
import { ExpressionFunctionParameter } from './expression_function_parameter';
import { ExpressionValue } from '../expression_types/types';
import { ExecutionContext } from '../execution';
export class ExpressionFunction {
/**
* Name of function
*/
name: string;
/**
* Aliases that can be used instead of `name`.
*/
aliases: string[];
/**
* Return type of function. This SHOULD be supplied. We use it for UI
* and autocomplete hinting. We may also use it for optimizations in
* the future.
*/
type: string;
/**
* Function to run function (context, args)
*/
fn: (input: ExpressionValue, params: Record<string, any>, handlers: object) => ExpressionValue;
/**
* A short help text.
*/
help: string;
/**
* Specification of expression function parameters.
*/
args: Record<string, ExpressionFunctionParameter> = {};
/**
* Type of inputs that this function supports.
*/
inputTypes: string[] | undefined;
constructor(functionDefinition: AnyExpressionFunctionDefinition) {
const { name, type, aliases, fn, help, args, inputTypes, context } = functionDefinition;
this.name = name;
this.type = type;
this.aliases = aliases || [];
this.fn = (input, params, handlers) =>
Promise.resolve(fn(input, params, handlers as ExecutionContext));
this.help = help || '';
this.inputTypes = inputTypes || context?.types;
for (const [key, arg] of Object.entries(args || {})) {
this.args[key] = new ExpressionFunctionParameter(key, arg);
}
}
accepts = (type: string): boolean => {
// If you don't tell us input types, we'll assume you don't care what you get.
if (!this.inputTypes) return true;
return this.inputTypes.indexOf(type) > -1;
};
}

View file

@ -0,0 +1,55 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ArgumentType } from './arguments';
export class ExpressionFunctionParameter {
name: string;
required: boolean;
help: string;
types: string[];
default: any;
aliases: string[];
multi: boolean;
resolve: boolean;
options: any[];
constructor(name: string, arg: ArgumentType<any>) {
const { required, help, types, aliases, multi, resolve, options } = arg;
if (name === '_') {
throw Error('Arg names must not be _. Use it in aliases instead.');
}
this.name = name;
this.required = !!required;
this.help = help || '';
this.types = types || [];
this.default = arg.default;
this.aliases = aliases || [];
this.multi = !!multi;
this.resolve = resolve == null ? true : resolve;
this.options = options || [];
}
accepts(type: string) {
if (!this.types.length) return true;
return this.types.indexOf(type) > -1;
}
}

View file

@ -0,0 +1,51 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ExpressionFunctionParameter } from './expression_function_parameter';
describe('ExpressionFunctionParameter', () => {
test('can instantiate', () => {
const param = new ExpressionFunctionParameter('foo', {
help: 'bar',
});
expect(param.name).toBe('foo');
});
test('checks supported types', () => {
const param = new ExpressionFunctionParameter('foo', {
help: 'bar',
types: ['baz', 'quux'],
});
expect(param.accepts('baz')).toBe(true);
expect(param.accepts('quux')).toBe(true);
expect(param.accepts('quix')).toBe(false);
});
test('if no types are provided, then accepts any type', () => {
const param = new ExpressionFunctionParameter('foo', {
help: 'bar',
});
expect(param.accepts('baz')).toBe(true);
expect(param.accepts('quux')).toBe(true);
expect(param.accepts('quix')).toBe(true);
});
});

View file

@ -0,0 +1,24 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export * from './types';
export * from './arguments';
export * from './expression_function_parameter';
export * from './expression_function';
export * from './specs';

View file

@ -17,19 +17,15 @@
* under the License.
*/
import { ExpressionFunction } from '../../common/types';
import { ExpressionFunctionDefinition } from '../types';
const name = 'clog';
type Context = any;
type ClogExpressionFunction = ExpressionFunction<typeof name, Context, {}, Context>;
export const clog = (): ClogExpressionFunction => ({
name,
export const clog: ExpressionFunctionDefinition<'clog', unknown, {}, unknown> = {
name: 'clog',
args: {},
help: 'Outputs the context to the console',
fn: context => {
console.log(context); // eslint-disable-line no-console
return context;
fn: (input: unknown) => {
// eslint-disable-next-line no-console
console.log(input);
return input;
},
});
};

View file

@ -0,0 +1,182 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { i18n } from '@kbn/i18n';
import { ExpressionFunctionDefinition } from '../types';
import { openSans, FontLabel as FontFamily } from '../../fonts';
import { CSSStyle, FontStyle, FontWeight, Style, TextAlignment, TextDecoration } from '../../types';
const dashify = (str: string) => {
return str
.trim()
.replace(/([a-z])([A-Z])/g, '$1-$2')
.replace(/\W/g, m => (/[À-ž]/.test(m) ? m : '-'))
.replace(/^-+|-+$/g, '')
.toLowerCase();
};
const inlineStyle = (obj: Record<string, string | number>) => {
if (!obj) return '';
const styles = Object.keys(obj).map(key => {
const prop = dashify(key);
const line = prop.concat(':').concat(String(obj[key]));
return line;
});
return styles.join(';');
};
interface Arguments {
align?: TextAlignment;
color?: string;
family?: FontFamily;
italic?: boolean;
lHeight?: number | null;
size?: number;
underline?: boolean;
weight?: FontWeight;
}
export const font: ExpressionFunctionDefinition<'font', null, Arguments, Style> = {
name: 'font',
aliases: [],
type: 'style',
help: i18n.translate('expressions.functions.fontHelpText', {
defaultMessage: 'Create a font style.',
}),
inputTypes: ['null'],
args: {
align: {
default: 'left',
help: i18n.translate('expressions.functions.font.args.alignHelpText', {
defaultMessage: 'The horizontal text alignment.',
}),
options: Object.values(TextAlignment),
types: ['string'],
},
color: {
help: i18n.translate('expressions.functions.font.args.colorHelpText', {
defaultMessage: 'The text color.',
}),
types: ['string'],
},
family: {
default: `"${openSans.value}"`,
help: i18n.translate('expressions.functions.font.args.familyHelpText', {
defaultMessage: 'An acceptable {css} web font string',
values: {
css: 'CSS',
},
}),
types: ['string'],
},
italic: {
default: false,
help: i18n.translate('expressions.functions.font.args.italicHelpText', {
defaultMessage: 'Italicize the text?',
}),
options: [true, false],
types: ['boolean'],
},
lHeight: {
default: null,
aliases: ['lineHeight'],
help: i18n.translate('expressions.functions.font.args.lHeightHelpText', {
defaultMessage: 'The line height in pixels',
}),
types: ['number', 'null'],
},
size: {
default: 14,
help: i18n.translate('expressions.functions.font.args.sizeHelpText', {
defaultMessage: 'The font size in pixels',
}),
types: ['number'],
},
underline: {
default: false,
help: i18n.translate('expressions.functions.font.args.underlineHelpText', {
defaultMessage: 'Underline the text?',
}),
options: [true, false],
types: ['boolean'],
},
weight: {
default: 'normal',
help: i18n.translate('expressions.functions.font.args.weightHelpText', {
defaultMessage: 'The font weight. For example, {list}, or {end}.',
values: {
list: Object.values(FontWeight)
.slice(0, -1)
.map(weight => `\`"${weight}"\``)
.join(', '),
end: `\`"${Object.values(FontWeight).slice(-1)[0]}"\``,
},
}),
options: Object.values(FontWeight),
types: ['string'],
},
},
fn: (input, args) => {
if (!Object.values(FontWeight).includes(args.weight!)) {
throw new Error(
i18n.translate('expressions.functions.font.invalidFontWeightErrorMessage', {
defaultMessage: "Invalid font weight: '{weight}'",
values: {
weight: args.weight,
},
})
);
}
if (!Object.values(TextAlignment).includes(args.align!)) {
throw new Error(
i18n.translate('expressions.functions.font.invalidTextAlignmentErrorMessage', {
defaultMessage: "Invalid text alignment: '{align}'",
values: {
align: args.align,
},
})
);
}
// the line height shouldn't ever be lower than the size, and apply as a
// pixel setting
const lineHeight = args.lHeight != null ? `${args.lHeight}px` : '1';
const spec: CSSStyle = {
fontFamily: args.family,
fontWeight: args.weight,
fontStyle: args.italic ? FontStyle.ITALIC : FontStyle.NORMAL,
textDecoration: args.underline ? TextDecoration.UNDERLINE : TextDecoration.NONE,
textAlign: args.align,
fontSize: `${args.size}px`, // apply font size as a pixel setting
lineHeight, // apply line height as a pixel setting
};
// conditionally apply styles based on input
if (args.color) {
spec.color = args.color;
}
return {
type: 'style',
spec,
css: inlineStyle(spec as Record<string, string | number>),
};
},
};

View file

@ -0,0 +1,39 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { clog } from './clog';
import { font } from './font';
import { kibana } from './kibana';
import { variableSet } from './var_set';
import { variable } from './var';
import { AnyExpressionFunctionDefinition } from '../types';
export const functionSpecs: AnyExpressionFunctionDefinition[] = [
clog,
font,
kibana,
variableSet,
variable,
];
export * from './clog';
export * from './font';
export * from './kibana';
export * from './var_set';
export * from './var';

View file

@ -18,47 +18,43 @@
*/
import { i18n } from '@kbn/i18n';
import { ExpressionFunction } from '../../common/types';
import { KibanaContext } from '../../common/expression_types';
import { ExpressionFunctionDefinition } from '../types';
import { ExpressionValueSearchContext } from '../../expression_types';
export type ExpressionFunctionKibana = ExpressionFunction<
const toArray = <T>(query: undefined | T | T[]): T[] =>
!query ? [] : Array.isArray(query) ? query : [query];
export type ExpressionFunctionKibana = ExpressionFunctionDefinition<
'kibana',
KibanaContext | null,
// TODO: Get rid of the `null` type below.
ExpressionValueSearchContext | null,
object,
KibanaContext
ExpressionValueSearchContext
>;
export const kibana = (): ExpressionFunctionKibana => ({
export const kibana: ExpressionFunctionKibana = {
name: 'kibana',
type: 'kibana_context',
context: {
types: ['kibana_context', 'null'],
},
inputTypes: ['kibana_context', 'null'],
help: i18n.translate('expressions.functions.kibana.help', {
defaultMessage: 'Gets kibana global context',
}),
args: {},
fn(context, args, handlers) {
const initialContext = handlers.getInitialContext ? handlers.getInitialContext() : {};
if (context && context.query) {
initialContext.query = initialContext.query.concat(context.query);
}
if (context && context.filters) {
initialContext.filters = initialContext.filters.concat(context.filters);
}
const timeRange = initialContext.timeRange || (context ? context.timeRange : undefined);
return {
...context,
fn(input, _, { search = {} }) {
const output: ExpressionValueSearchContext = {
// TODO: This spread is left here for legacy reasons, possibly Lens uses it.
// TODO: But it shouldn't be need.
...input,
type: 'kibana_context',
query: initialContext.query,
filters: initialContext.filters,
timeRange,
query: [...toArray(search.query), ...toArray((input || {}).query)],
filters: [...(search.filters || []), ...((input || {}).filters || [])],
timeRange: search.timeRange || (input ? input.timeRange : undefined),
};
return output;
},
});
};

View file

@ -14,10 +14,12 @@ Object {
},
},
],
"query": Object {
"language": "lucene",
"query": "geo.src:US",
},
"query": Array [
Object {
"language": "lucene",
"query": "geo.src:US",
},
],
"timeRange": Object {
"from": "2",
"to": "3",

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { openSans } from '../../../common/fonts';
import { openSans } from '../../../fonts';
import { font } from '../font';
import { functionWrapper } from './utils';

View file

@ -19,18 +19,18 @@
import { functionWrapper } from './utils';
import { kibana } from '../kibana';
import { FunctionHandlers } from '../../../common/types';
import { KibanaContext } from '../../../common/expression_types/kibana_context';
import { ExecutionContext } from '../../../execution/types';
import { KibanaContext, ExpressionValueSearchContext } from '../../../expression_types';
describe('interpreter/functions#kibana', () => {
const fn = functionWrapper(kibana);
let context: Partial<KibanaContext>;
let initialContext: KibanaContext;
let handlers: FunctionHandlers;
let input: Partial<KibanaContext>;
let search: ExpressionValueSearchContext;
let context: ExecutionContext;
beforeEach(() => {
context = { timeRange: { from: '0', to: '1' } };
initialContext = {
input = { timeRange: { from: '0', to: '1' } };
search = {
type: 'kibana_context',
query: { language: 'lucene', query: 'geo.src:US' },
filters: [
@ -45,31 +45,29 @@ describe('interpreter/functions#kibana', () => {
],
timeRange: { from: '2', to: '3' },
};
handlers = {
getInitialContext: () => initialContext,
context = {
search,
getInitialInput: () => input,
types: {},
variables: {},
abortSignal: {} as any,
inspectorAdapters: {} as any,
};
});
it('returns an object with the correct structure', () => {
const actual = fn(context, {}, handlers);
const actual = fn(input, {}, context);
expect(actual).toMatchSnapshot();
});
it('uses timeRange from context if not provided in initialContext', () => {
initialContext.timeRange = undefined;
const actual = fn(context, {}, handlers);
it('uses timeRange from input if not provided in search context', () => {
search.timeRange = undefined;
const actual = fn(input, {}, context);
expect(actual.timeRange).toEqual({ from: '0', to: '1' });
});
it.skip('combines query from context with initialContext', () => {
context.query = { language: 'kuery', query: 'geo.dest:CN' };
// TODO: currently this fails & likely requires a fix in run_pipeline
const actual = fn(context, {}, handlers);
expect(actual.query).toEqual('TBD');
});
it('combines filters from context with initialContext', () => {
context.filters = [
it('combines filters from input with search context', () => {
input.filters = [
{
meta: {
disabled: true,
@ -79,7 +77,7 @@ describe('interpreter/functions#kibana', () => {
query: { match: {} },
},
];
const actual = fn(context, {}, handlers);
const actual = fn(input, {}, context);
expect(actual.filters).toEqual([
{
meta: {

View file

@ -18,16 +18,18 @@
*/
import { mapValues } from 'lodash';
import { AnyExpressionFunction, FunctionHandlers } from '../../../common/types';
import { AnyExpressionFunctionDefinition } from '../../types';
import { ExecutionContext } from '../../../execution/types';
// Takes a function spec and passes in default args,
// overriding with any provided args.
export const functionWrapper = <T extends AnyExpressionFunction>(fnSpec: () => T) => {
const spec = fnSpec();
/**
* Takes a function spec and passes in default args,
* overriding with any provided args.
*/
export const functionWrapper = (spec: AnyExpressionFunctionDefinition) => {
const defaultArgs = mapValues(spec.args, argSpec => argSpec.default);
return (
context: object | null,
args: Record<string, any> = {},
handlers: FunctionHandlers = {}
handlers: ExecutionContext = {} as ExecutionContext
) => spec.fn(context, { ...defaultArgs, ...args }, handlers);
};

View file

@ -0,0 +1,52 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { functionWrapper } from './utils';
import { variable } from '../var';
import { ExecutionContext } from '../../../execution/types';
import { KibanaContext } from '../../../expression_types';
describe('expression_functions', () => {
describe('var', () => {
const fn = functionWrapper(variable);
let input: Partial<KibanaContext>;
let context: ExecutionContext;
beforeEach(() => {
input = { timeRange: { from: '0', to: '1' } };
context = {
getInitialInput: () => input,
types: {},
variables: { test: 1 },
abortSignal: {} as any,
inspectorAdapters: {} as any,
};
});
it('returns the selected variable', () => {
const actual = fn(input, { name: 'test' }, context);
expect(actual).toEqual(1);
});
it('returns undefined if variable does not exist', () => {
const actual = fn(input, { name: 'unknown' }, context);
expect(actual).toEqual(undefined);
});
});
});

View file

@ -0,0 +1,63 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { functionWrapper } from './utils';
import { variableSet } from '../var_set';
import { ExecutionContext } from '../../../execution/types';
import { KibanaContext } from '../../../expression_types';
describe('expression_functions', () => {
describe('var_set', () => {
const fn = functionWrapper(variableSet);
let input: Partial<KibanaContext>;
let context: ExecutionContext;
let variables: Record<string, any>;
beforeEach(() => {
input = { timeRange: { from: '0', to: '1' } };
context = {
getInitialInput: () => input,
types: {},
variables: { test: 1 },
abortSignal: {} as any,
inspectorAdapters: {} as any,
};
variables = context.variables;
});
it('updates a variable', () => {
const actual = fn(input, { name: 'test', value: 2 }, context);
expect(variables.test).toEqual(2);
expect(actual).toEqual(input);
});
it('sets a new variable', () => {
const actual = fn(input, { name: 'new', value: 3 }, context);
expect(variables.new).toEqual(3);
expect(actual).toEqual(input);
});
it('stores context if value is not set', () => {
const actual = fn(input, { name: 'test' }, context);
expect(variables.test).toEqual(input);
expect(actual).toEqual(input);
});
});
});

View file

@ -18,16 +18,15 @@
*/
import { i18n } from '@kbn/i18n';
import { ExpressionFunction } from '../../common/types';
import { ExpressionFunctionDefinition } from '../types';
interface Arguments {
name: string;
}
type Context = any;
type ExpressionFunctionVar = ExpressionFunction<'var', Context, Arguments, any>;
type ExpressionFunctionVar = ExpressionFunctionDefinition<'var', unknown, Arguments, unknown>;
export const variable = (): ExpressionFunctionVar => ({
export const variable: ExpressionFunctionVar = {
name: 'var',
help: i18n.translate('expressions.functions.var.help', {
defaultMessage: 'Updates kibana global context',
@ -42,8 +41,8 @@ export const variable = (): ExpressionFunctionVar => ({
}),
},
},
fn(context, args, handlers) {
const variables: Record<string, any> = handlers.variables;
fn(input, args, context) {
const variables: Record<string, any> = context.variables;
return variables[args.name];
},
});
};

View file

@ -18,17 +18,14 @@
*/
import { i18n } from '@kbn/i18n';
import { ExpressionFunction } from '../../common/types';
import { ExpressionFunctionDefinition } from '../types';
interface Arguments {
name: string;
value?: any;
}
type Context = any;
type ExpressionFunctionVarSet = ExpressionFunction<'var_set', Context, Arguments, Context>;
export const variableSet = (): ExpressionFunctionVarSet => ({
export const variableSet: ExpressionFunctionDefinition<'var_set', unknown, Arguments, unknown> = {
name: 'var_set',
help: i18n.translate('expressions.functions.varset.help', {
defaultMessage: 'Updates kibana global context',
@ -50,9 +47,9 @@ export const variableSet = (): ExpressionFunctionVarSet => ({
}),
},
},
fn(context, args, handlers) {
const variables: Record<string, any> = handlers.variables;
variables[args.name] = args.value === undefined ? context : args.value;
return context;
fn(input, args, context) {
const variables: Record<string, any> = context.variables;
variables[args.name] = args.value === undefined ? input : args.value;
return input;
},
});
};

View file

@ -0,0 +1,96 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { UnwrapPromiseOrReturn } from '@kbn/utility-types';
import { ArgumentType } from './arguments';
import { TypeToString } from '../types/common';
import { ExecutionContext } from '../execution/types';
/**
* `ExpressionFunctionDefinition` is the interface plugins have to implement to
* register a function in `expressions` plugin.
*/
export interface ExpressionFunctionDefinition<
Name extends string,
Input,
Arguments,
Output,
Context extends ExecutionContext = ExecutionContext
> {
/**
* The name of the function, as will be used in expression.
*/
name: Name;
/**
* Name of type of value this function outputs.
*/
type?: TypeToString<UnwrapPromiseOrReturn<Output>>;
/**
* List of allowed type names for input value of this function. If this
* property is set the input of function will be cast to the first possible
* type in this list. If this property is missing the input will be provided
* to the function as-is.
*/
inputTypes?: Array<TypeToString<Input>>;
/**
* Specification of arguments that function supports. This list will also be
* used for autocomplete functionality when your function is being edited.
*/
args: { [key in keyof Arguments]: ArgumentType<Arguments[key]> };
/**
* @todo What is this?
*/
aliases?: string[];
/**
* Help text displayed in the Expression editor. This text should be
* internationalized.
*/
help: string;
/**
* The actual implementation of the function.
*
* @param input Output of the previous function, or initial input.
* @param args Parameters set for this function in expression.
* @param context Object with functions to perform side effects. This object
* is created for the duration of the execution of expression and is the
* same for all functions in expression chain.
*/
fn(input: Input, args: Arguments, context: Context): Output;
/**
* @deprecated Use `inputTypes` instead.
*/
context?: {
/**
* @deprecated This is alias for `inputTypes`, use `inputTypes` instead.
*/
types: AnyExpressionFunctionDefinition['inputTypes'];
};
}
/**
* Type to capture every possible expression function definition.
*/
export type AnyExpressionFunctionDefinition = ExpressionFunctionDefinition<any, any, any, any>;

View file

@ -0,0 +1,40 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ExpressionRenderDefinition } from './types';
export class ExpressionRenderer<Config = unknown> {
public readonly name: string;
public readonly displayName: string;
public readonly help: string;
public readonly validate: () => void | Error;
public readonly reuseDomNode: boolean;
public readonly render: ExpressionRenderDefinition<Config>['render'];
constructor(config: ExpressionRenderDefinition<Config>) {
const { name, displayName, help, validate, reuseDomNode, render } = config;
this.name = name;
this.displayName = displayName || name;
this.help = help || '';
this.validate = validate || (() => {});
this.reuseDomNode = Boolean(reuseDomNode);
this.render = render;
}
}

View file

@ -0,0 +1,53 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { IRegistry } from '../types';
import { ExpressionRenderer } from './expression_renderer';
import { AnyExpressionRenderDefinition } from './types';
export class ExpressionRendererRegistry implements IRegistry<ExpressionRenderer> {
private readonly renderers: Map<string, ExpressionRenderer> = new Map<
string,
ExpressionRenderer
>();
register(definition: AnyExpressionRenderDefinition | (() => AnyExpressionRenderDefinition)) {
if (typeof definition === 'function') definition = definition();
const renderer = new ExpressionRenderer(definition);
this.renderers.set(renderer.name, renderer);
}
public get(id: string): ExpressionRenderer | null {
return this.renderers.get(id) || null;
}
public toJS(): Record<string, ExpressionRenderer> {
return this.toArray().reduce(
(acc, renderer) => ({
...acc,
[renderer.name]: renderer,
}),
{} as Record<string, ExpressionRenderer>
);
}
public toArray(): ExpressionRenderer[] {
return [...this.renderers.values()];
}
}

View file

@ -0,0 +1,22 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export * from './types';
export * from './expression_renderer';
export * from './expression_renderer_registry';

View file

@ -0,0 +1,71 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export interface ExpressionRenderDefinition<Config = unknown> {
/**
* Technical name of the renderer, used as ID to identify renderer in
* expression renderer registry. This must match the name of the expression
* function that is used to create the `type: render` object.
*/
name: string;
/**
* A user friendly name of the renderer as will be displayed to user in UI.
*/
displayName: string;
/**
* Help text as will be displayed to user. A sentence or few about what this
* element does.
*/
help?: string;
/**
* Used to validate the data before calling the render function.
*/
validate?: () => undefined | Error;
/**
* Tell the renderer if the dom node should be reused, it's recreated each
* time by default.
*/
reuseDomNode: boolean;
/**
* The function called to render the output data of an expression.
*/
render: (
domNode: HTMLElement,
config: Config,
handlers: IInterpreterRenderHandlers
) => void | Promise<void>;
}
export type AnyExpressionRenderDefinition = ExpressionRenderDefinition<any>;
export interface IInterpreterRenderHandlers {
/**
* Done increments the number of rendering successes
*/
done: () => void;
onDestroy: (fn: () => void) => void;
reload: () => void;
update: (params: any) => void;
event: (event: any) => void;
}

View file

@ -0,0 +1,145 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ExpressionType } from './expression_type';
import { ExpressionTypeDefinition } from './types';
import { ExpressionValueRender } from './specs';
export const boolean: ExpressionTypeDefinition<'boolean', boolean> = {
name: 'boolean',
from: {
null: () => false,
number: n => Boolean(n),
string: s => Boolean(s),
},
to: {
render: (value): ExpressionValueRender<{ text: string }> => {
const text = `${value}`;
return {
type: 'render',
as: 'text',
value: { text },
};
},
},
};
export const render: ExpressionTypeDefinition<'render', ExpressionValueRender<unknown>> = {
name: 'render',
from: {
'*': <T>(v: T): ExpressionValueRender<T> => ({
type: name,
as: 'debug',
value: v,
}),
},
};
const emptyDatatableValue = {
type: 'datatable',
columns: [],
rows: [],
};
describe('ExpressionType', () => {
test('can create a boolean type', () => {
new ExpressionType(boolean);
});
describe('castsFrom()', () => {
describe('when "from" definition specifies "*" as one of its from types', () => {
test('returns true for any value', () => {
const type = new ExpressionType(render);
expect(type.castsFrom(123)).toBe(true);
expect(type.castsFrom('foo')).toBe(true);
expect(type.castsFrom(true)).toBe(true);
expect(
type.castsFrom({
type: 'datatable',
columns: [],
rows: [],
})
).toBe(true);
});
});
});
describe('castsTo()', () => {
describe('when "to" definition is not specified', () => {
test('returns false for any value', () => {
const type = new ExpressionType(render);
expect(type.castsTo(123)).toBe(false);
expect(type.castsTo('foo')).toBe(false);
expect(type.castsTo(true)).toBe(false);
expect(type.castsTo(emptyDatatableValue)).toBe(false);
});
});
});
describe('from()', () => {
test('can cast from any type specified in definition', () => {
const type = new ExpressionType(boolean);
expect(type.from(1, {})).toBe(true);
expect(type.from(0, {})).toBe(false);
expect(type.from('foo', {})).toBe(true);
expect(type.from('', {})).toBe(false);
expect(type.from(null, {})).toBe(false);
// undefined is used like null in legacy interpreter
expect(type.from(undefined, {})).toBe(false);
});
test('throws when casting from type that is not supported', async () => {
const type = new ExpressionType(boolean);
expect(() => type.from(emptyDatatableValue, {})).toThrowError();
expect(() => type.from(emptyDatatableValue, {})).toThrowErrorMatchingInlineSnapshot(
`"Can not cast 'boolean' from datatable"`
);
});
});
describe('to()', () => {
test('can cast to type specified in definition', () => {
const type = new ExpressionType(boolean);
expect(type.to(true, 'render', {})).toMatchObject({
as: 'text',
type: 'render',
value: {
text: 'true',
},
});
expect(type.to(false, 'render', {})).toMatchObject({
as: 'text',
type: 'render',
value: {
text: 'false',
},
});
});
test('throws when casting to type that is not supported', async () => {
const type = new ExpressionType(boolean);
expect(() => type.to(emptyDatatableValue, 'number', {})).toThrowError();
expect(() => type.to(emptyDatatableValue, 'number', {})).toThrowErrorMatchingInlineSnapshot(
`"Can not cast object of type 'datatable' using 'boolean'"`
);
});
});
});

View file

@ -17,35 +17,10 @@
* under the License.
*/
import { get, identity } from 'lodash';
import { AnyExpressionType, ExpressionValue } from './types';
import { AnyExpressionTypeDefinition, ExpressionValue, ExpressionValueConverter } from './types';
import { getType } from './get_type';
export function getType(node: any) {
if (node == null) return 'null';
if (typeof node === 'object') {
if (!node.type) throw new Error('Objects must have a type property');
return node.type;
}
return typeof node;
}
export function serializeProvider(types: any) {
function provider(key: any) {
return (context: any) => {
const type = getType(context);
const typeDef = types[type];
const fn: any = get(typeDef, key) || identity;
return fn(context);
};
}
return {
serialize: provider('serialize'),
deserialize: provider('deserialize'),
};
}
export class Type {
export class ExpressionType {
name: string;
/**
@ -66,41 +41,53 @@ export class Type {
serialize?: (value: ExpressionValue) => any;
deserialize?: (serialized: any) => ExpressionValue;
constructor(private readonly config: AnyExpressionType) {
const { name, help, deserialize, serialize, validate } = config;
constructor(private readonly definition: AnyExpressionTypeDefinition) {
const { name, help, deserialize, serialize, validate } = definition;
this.name = name;
this.help = help || '';
this.validate = validate || (() => {});
// Optional
this.create = (config as any).create;
this.create = (definition as any).create;
this.serialize = serialize;
this.deserialize = deserialize;
}
getToFn = (value: any) => get(this.config, ['to', value]) || get(this.config, ['to', '*']);
getFromFn = (value: any) => get(this.config, ['from', value]) || get(this.config, ['from', '*']);
getToFn = (
typeName: string
): undefined | ExpressionValueConverter<ExpressionValue, ExpressionValue> =>
!this.definition.to ? undefined : this.definition.to[typeName] || this.definition.to['*'];
castsTo = (value: any) => typeof this.getToFn(value) === 'function';
castsFrom = (value: any) => typeof this.getFromFn(value) === 'function';
getFromFn = (
typeName: string
): undefined | ExpressionValueConverter<ExpressionValue, ExpressionValue> =>
!this.definition.from ? undefined : this.definition.from[typeName] || this.definition.from['*'];
castsTo = (value: ExpressionValue) => typeof this.getToFn(value) === 'function';
castsFrom = (value: ExpressionValue) => typeof this.getFromFn(value) === 'function';
to = (value: ExpressionValue, toTypeName: string, types: Record<string, ExpressionType>) => {
const typeName = getType(value);
to = (node: any, toTypeName: any, types: any) => {
const typeName = getType(node);
if (typeName !== this.name) {
throw new Error(`Can not cast object of type '${typeName}' using '${this.name}'`);
} else if (!this.castsTo(toTypeName)) {
throw new Error(`Can not cast '${typeName}' to '${toTypeName}'`);
}
return (this.getToFn(toTypeName) as any)(node, types);
return this.getToFn(toTypeName)!(value, types);
};
from = (node: any, types: any) => {
const typeName = getType(node);
if (!this.castsFrom(typeName)) throw new Error(`Can not cast '${this.name}' from ${typeName}`);
from = (value: ExpressionValue, types: Record<string, ExpressionType>) => {
const typeName = getType(value);
return (this.getFromFn(typeName) as any)(node, types);
if (!this.castsFrom(typeName)) {
throw new Error(`Can not cast '${this.name}' from ${typeName}`);
}
return this.getFromFn(typeName)!(value, types);
};
}

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { getType } from './type';
import { getType } from './get_type';
describe('getType()', () => {
test('returns "null" string for null or undefined', () => {

View file

@ -17,13 +17,11 @@
* under the License.
*/
import { Registry } from './registry';
import { Type } from '../../common/type';
import { AnyExpressionType } from '../../common/types';
export class TypesRegistry extends Registry<Type> {
register(typeDefinition: AnyExpressionType | (() => AnyExpressionType)) {
const type = new Type(typeof typeDefinition === 'object' ? typeDefinition : typeDefinition());
this.set(type.name, type);
export function getType(node: any) {
if (node == null) return 'null';
if (typeof node === 'object') {
if (!node.type) throw new Error('Objects must have a type property');
return node.type;
}
return typeof node;
}

View file

@ -17,52 +17,8 @@
* under the License.
*/
import { boolean } from './boolean';
import { datatable } from './datatable';
import { error } from './error';
import { filter } from './filter';
import { image } from './image';
import { kibanaContext } from './kibana_context';
import { kibanaDatatable } from './kibana_datatable';
import { nullType } from './null';
import { number } from './number';
import { pointseries } from './pointseries';
import { range } from './range';
import { render } from './render';
import { shape } from './shape';
import { string } from './string';
import { style } from './style';
export const typeSpecs = [
boolean,
datatable,
error,
filter,
image,
kibanaContext,
kibanaDatatable,
nullType,
number,
pointseries,
range,
render,
shape,
string,
style,
];
export * from './boolean';
export * from './datatable';
export * from './error';
export * from './filter';
export * from './image';
export * from './kibana_context';
export * from './kibana_datatable';
export * from './null';
export * from './number';
export * from './pointseries';
export * from './range';
export * from './render';
export * from './shape';
export * from './string';
export * from './style';
export * from './types';
export * from './get_type';
export * from './serialize_provider';
export * from './expression_type';
export * from './specs';

View file

@ -0,0 +1,29 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ExpressionType } from './expression_type';
import { ExpressionValue } from './types';
import { getType } from './get_type';
const identity = <T>(x: T) => x;
export const serializeProvider = (types: Record<string, ExpressionType>) => ({
serialize: (value: ExpressionValue) => (types[getType(value)].serialize || identity)(value),
deserialize: (value: ExpressionValue) => (types[getType(value)].deserialize || identity)(value),
});

View file

@ -17,13 +17,13 @@
* under the License.
*/
import { ExpressionType } from '../types';
import { ExpressionTypeDefinition } from '../types';
import { Datatable } from './datatable';
import { Render } from './render';
import { ExpressionValueRender } from './render';
const name = 'boolean';
export const boolean = (): ExpressionType<'boolean', boolean> => ({
export const boolean: ExpressionTypeDefinition<'boolean', boolean> = {
name,
from: {
null: () => false,
@ -31,7 +31,7 @@ export const boolean = (): ExpressionType<'boolean', boolean> => ({
string: s => Boolean(s),
},
to: {
render: (value): Render<{ text: string }> => {
render: (value): ExpressionValueRender<{ text: string }> => {
const text = `${value}`;
return {
type: 'render',
@ -45,4 +45,4 @@ export const boolean = (): ExpressionType<'boolean', boolean> => ({
rows: [{ value }],
}),
},
});
};

View file

@ -19,9 +19,9 @@
import { map, pick, zipObject } from 'lodash';
import { ExpressionType } from '../types';
import { ExpressionTypeDefinition } from '../types';
import { PointSeries } from './pointseries';
import { Render } from './render';
import { ExpressionValueRender } from './render';
const name = 'datatable';
@ -70,7 +70,7 @@ interface RenderedDatatable {
showHeader: boolean;
}
export const datatable = (): ExpressionType<typeof name, Datatable, SerializedDatatable> => ({
export const datatable: ExpressionTypeDefinition<typeof name, Datatable, SerializedDatatable> = {
name,
validate: table => {
// TODO: Check columns types. Only string, boolean, number, date, allowed for now.
@ -115,7 +115,7 @@ export const datatable = (): ExpressionType<typeof name, Datatable, SerializedDa
}),
},
to: {
render: (table): Render<RenderedDatatable> => ({
render: (table): ExpressionValueRender<RenderedDatatable> => ({
type: 'render',
as: 'table',
value: {
@ -143,4 +143,4 @@ export const datatable = (): ExpressionType<typeof name, Datatable, SerializedDa
};
},
},
});
};

View file

@ -17,20 +17,27 @@
* under the License.
*/
import { ExpressionType } from '../types';
import { Render } from './render';
import { ExpressionValueBoxed } from '../types/types';
import { ExpressionTypeDefinition, ExpressionValueBoxed } from '../types';
import { ExpressionValueRender } from './render';
import { getType } from '../get_type';
const name = 'error';
export type ExpressionValueError = ExpressionValueBoxed<
'error',
{
error: unknown;
error: {
message: string;
name?: string;
stack?: string;
};
info: unknown;
}
>;
export const isExpressionValueError = (value: any): value is ExpressionValueError =>
getType(value) === 'error';
/**
* @deprecated
*
@ -38,10 +45,10 @@ export type ExpressionValueError = ExpressionValueBoxed<
*/
export type InterpreterErrorType = ExpressionValueError;
export const error = (): ExpressionType<'error', ExpressionValueError> => ({
export const error: ExpressionTypeDefinition<'error', ExpressionValueError> = {
name,
to: {
render: (input): Render<Pick<InterpreterErrorType, 'error' | 'info'>> => {
render: (input): ExpressionValueRender<Pick<InterpreterErrorType, 'error' | 'info'>> => {
return {
type: 'render',
as: name,
@ -52,4 +59,4 @@ export const error = (): ExpressionType<'error', ExpressionValueError> => ({
};
},
},
});
};

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { ExpressionType } from '../types';
import { ExpressionTypeDefinition } from '../types';
const name = 'filter';
@ -34,7 +34,7 @@ export interface Filter {
query?: string | null;
}
export const filter = (): ExpressionType<typeof name, Filter> => ({
export const filter: ExpressionTypeDefinition<typeof name, Filter> = {
name,
from: {
null: () => {
@ -47,4 +47,4 @@ export const filter = (): ExpressionType<typeof name, Filter> => ({
};
},
},
});
};

View file

@ -17,8 +17,8 @@
* under the License.
*/
import { ExpressionType } from '../types';
import { Render } from './render';
import { ExpressionTypeDefinition } from '../types';
import { ExpressionValueRender } from './render';
const name = 'image';
@ -28,10 +28,10 @@ export interface ExpressionImage {
dataurl: string;
}
export const image = (): ExpressionType<typeof name, ExpressionImage> => ({
export const image: ExpressionTypeDefinition<typeof name, ExpressionImage> = {
name,
to: {
render: (input): Render<Pick<ExpressionImage, 'mode' | 'dataurl'>> => {
render: (input): ExpressionValueRender<Pick<ExpressionImage, 'mode' | 'dataurl'>> => {
return {
type: 'render',
as: 'image',
@ -39,4 +39,4 @@ export const image = (): ExpressionType<typeof name, ExpressionImage> => ({
};
},
},
});
};

View file

@ -0,0 +1,72 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { boolean } from './boolean';
import { datatable } from './datatable';
import { error } from './error';
import { filter } from './filter';
import { image } from './image';
import { kibanaContext } from './kibana_context';
import { kibanaDatatable } from './kibana_datatable';
import { nullType } from './null';
import { num } from './num';
import { number } from './number';
import { pointseries } from './pointseries';
import { range } from './range';
import { render } from './render';
import { shape } from './shape';
import { string } from './string';
import { style } from './style';
import { AnyExpressionTypeDefinition } from '../types';
export const typeSpecs: AnyExpressionTypeDefinition[] = [
boolean,
datatable,
error,
filter,
image,
kibanaContext,
kibanaDatatable,
nullType,
num,
number,
pointseries,
range,
render,
shape,
string,
style,
];
export * from './boolean';
export * from './datatable';
export * from './error';
export * from './filter';
export * from './image';
export * from './kibana_context';
export * from './kibana_datatable';
export * from './null';
export * from './num';
export * from './number';
export * from './pointseries';
export * from './range';
export * from './render';
export * from './shape';
export * from './string';
export * from './style';

View file

@ -17,24 +17,24 @@
* under the License.
*/
import { TimeRange, Query, esFilters } from 'src/plugins/data/public';
import { ExpressionValueBoxed } from '../types';
import { ExecutionContextSearch } from '../../execution/types';
const name = 'kibana_context';
export type ExpressionValueSearchContext = ExpressionValueBoxed<
'kibana_context',
ExecutionContextSearch
>;
// TODO: These two are exported for legacy reasons - remove them eventually.
export type KIBANA_CONTEXT_NAME = 'kibana_context';
export type KibanaContext = ExpressionValueSearchContext;
export interface KibanaContext {
type: typeof name;
query?: Query | Query[];
filters?: esFilters.Filter[];
timeRange?: TimeRange;
}
export const kibanaContext = () => ({
name,
export const kibanaContext = {
name: 'kibana_context',
from: {
null: () => {
return {
type: name,
type: 'kibana_context',
};
},
},
@ -45,4 +45,4 @@ export const kibanaContext = () => ({
};
},
},
});
};

View file

@ -18,7 +18,7 @@
*/
import { map } from 'lodash';
import { SerializedFieldFormat } from '../types/common';
import { SerializedFieldFormat } from '../../types/common';
import { Datatable, PointSeries } from '.';
const name = 'kibana_datatable';
@ -46,7 +46,7 @@ export interface KibanaDatatable {
rows: KibanaDatatableRow[];
}
export const kibanaDatatable = () => ({
export const kibanaDatatable = {
name,
from: {
datatable: (context: Datatable) => {
@ -72,4 +72,4 @@ export const kibanaDatatable = () => ({
};
},
},
});
};

View file

@ -17,13 +17,13 @@
* under the License.
*/
import { ExpressionType } from '../types';
import { ExpressionTypeDefinition } from '../types';
const name = 'null';
export const nullType = (): ExpressionType<typeof name, null> => ({
export const nullType: ExpressionTypeDefinition<typeof name, null> = {
name,
from: {
'*': () => null,
},
});
};

View file

@ -0,0 +1,80 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { i18n } from '@kbn/i18n';
import { ExpressionTypeDefinition, ExpressionValueBoxed } from '../types';
import { Datatable } from './datatable';
import { ExpressionValueRender } from './render';
export type ExpressionValueNum = ExpressionValueBoxed<
'num',
{
value: number;
}
>;
export const num: ExpressionTypeDefinition<'num', ExpressionValueNum> = {
name: 'num',
from: {
null: () => ({
type: 'num',
value: 0,
}),
boolean: b => ({
type: 'num',
value: Number(b),
}),
string: n => {
const value = Number(n);
if (Number.isNaN(value)) {
throw new Error(
i18n.translate('expressions.types.number.fromStringConversionErrorMessage', {
defaultMessage: 'Can\'t typecast "{string}" string to number',
values: {
string: n,
},
})
);
}
return {
type: 'num',
value,
};
},
'*': value => ({
type: 'num',
value: Number(value),
}),
},
to: {
render: ({ value }): ExpressionValueRender<{ text: string }> => {
const text = `${value}`;
return {
type: 'render',
as: 'text',
value: { text },
};
},
datatable: ({ value }): Datatable => ({
type: 'datatable',
columns: [{ name: 'value', type: 'number' }],
rows: [{ value }],
}),
},
};

View file

@ -18,13 +18,13 @@
*/
import { i18n } from '@kbn/i18n';
import { ExpressionType } from '../../common/types';
import { ExpressionTypeDefinition } from '../types';
import { Datatable } from './datatable';
import { Render } from './render';
import { ExpressionValueRender } from './render';
const name = 'number';
export const number = (): ExpressionType<typeof name, number> => ({
export const number: ExpressionTypeDefinition<typeof name, number> = {
name,
from: {
null: () => 0,
@ -45,7 +45,7 @@ export const number = (): ExpressionType<typeof name, number> => ({
},
},
to: {
render: (value: number): Render<{ text: string }> => {
render: (value: number): ExpressionValueRender<{ text: string }> => {
const text = `${value}`;
return {
type: 'render',
@ -59,4 +59,4 @@ export const number = (): ExpressionType<typeof name, number> => ({
rows: [{ value }],
}),
},
});
};

View file

@ -17,10 +17,9 @@
* under the License.
*/
import { ExpressionType } from '../types';
import { ExpressionTypeDefinition, ExpressionValueBoxed } from '../types';
import { Datatable } from './datatable';
import { Render } from './render';
import { ExpressionValueBoxed } from '../types/types';
import { ExpressionValueRender } from './render';
const name = 'pointseries';
@ -56,7 +55,7 @@ export type PointSeries = ExpressionValueBoxed<
}
>;
export const pointseries = (): ExpressionType<'pointseries', PointSeries> => ({
export const pointseries: ExpressionTypeDefinition<'pointseries', PointSeries> = {
name,
from: {
null: () => {
@ -71,7 +70,7 @@ export const pointseries = (): ExpressionType<'pointseries', PointSeries> => ({
render: (
pseries: PointSeries,
types
): Render<{ datatable: Datatable; showHeader: boolean }> => {
): ExpressionValueRender<{ datatable: Datatable; showHeader: boolean }> => {
const datatable: Datatable = types.datatable.from(pseries, types);
return {
type: 'render',
@ -83,4 +82,4 @@ export const pointseries = (): ExpressionType<'pointseries', PointSeries> => ({
};
},
},
});
};

View file

@ -17,8 +17,8 @@
* under the License.
*/
import { ExpressionType } from '../types';
import { Render } from '.';
import { ExpressionTypeDefinition } from '../types';
import { ExpressionValueRender } from '.';
const name = 'range';
@ -28,7 +28,7 @@ export interface Range {
to: number;
}
export const range = (): ExpressionType<typeof name, Range> => ({
export const range: ExpressionTypeDefinition<typeof name, Range> = {
name,
from: {
null: (): Range => {
@ -40,7 +40,7 @@ export const range = (): ExpressionType<typeof name, Range> => ({
},
},
to: {
render: (value: Range): Render<{ text: string }> => {
render: (value: Range): ExpressionValueRender<{ text: string }> => {
const text = `from ${value.from} to ${value.to}`;
return {
type: 'render',
@ -49,4 +49,4 @@ export const range = (): ExpressionType<typeof name, Range> => ({
};
},
},
});
};

View file

@ -17,15 +17,14 @@
* under the License.
*/
import { ExpressionType } from '../types';
import { ExpressionValueBoxed } from '../types/types';
import { ExpressionTypeDefinition, ExpressionValueBoxed } from '../types';
const name = 'render';
/**
* Represents an object that is intended to be rendered.
*/
export type Render<T> = ExpressionValueBoxed<
export type ExpressionValueRender<T> = ExpressionValueBoxed<
typeof name,
{
as: string;
@ -33,13 +32,20 @@ export type Render<T> = ExpressionValueBoxed<
}
>;
export const render = (): ExpressionType<typeof name, Render<unknown>> => ({
/**
* @deprecated
*
* Use `ExpressionValueRender` instead.
*/
export type Render<T> = ExpressionValueRender<T>;
export const render: ExpressionTypeDefinition<typeof name, ExpressionValueRender<unknown>> = {
name,
from: {
'*': <T>(v: T): Render<T> => ({
'*': <T>(v: T): ExpressionValueRender<T> => ({
type: name,
as: 'debug',
value: v,
}),
},
});
};

View file

@ -17,12 +17,12 @@
* under the License.
*/
import { ExpressionType } from '../types';
import { Render } from './render';
import { ExpressionTypeDefinition } from '../types';
import { ExpressionValueRender } from './render';
const name = 'shape';
export const shape = (): ExpressionType<typeof name, Render<any>> => ({
export const shape: ExpressionTypeDefinition<typeof name, ExpressionValueRender<any>> = {
name: 'shape',
to: {
render: input => {
@ -33,4 +33,4 @@ export const shape = (): ExpressionType<typeof name, Render<any>> => ({
};
},
},
});
};

View file

@ -17,13 +17,13 @@
* under the License.
*/
import { ExpressionType } from '../types';
import { ExpressionTypeDefinition } from '../types';
import { Datatable } from './datatable';
import { Render } from './render';
import { ExpressionValueRender } from './render';
const name = 'string';
export const string = (): ExpressionType<typeof name, string> => ({
export const string: ExpressionTypeDefinition<typeof name, string> = {
name,
from: {
null: () => '',
@ -31,7 +31,7 @@ export const string = (): ExpressionType<typeof name, string> => ({
number: n => String(n),
},
to: {
render: <T>(text: T): Render<{ text: T }> => {
render: <T>(text: T): ExpressionValueRender<{ text: T }> => {
return {
type: 'render',
as: 'text',
@ -44,4 +44,4 @@ export const string = (): ExpressionType<typeof name, string> => ({
rows: [{ value }],
}),
},
});
};

View file

@ -17,11 +17,12 @@
* under the License.
*/
import { ExpressionType, ExpressionTypeStyle } from '../types';
import { ExpressionTypeDefinition } from '../types';
import { ExpressionTypeStyle } from '../../types/style';
const name = 'style';
export const style = (): ExpressionType<typeof name, ExpressionTypeStyle> => ({
export const style: ExpressionTypeDefinition<typeof name, ExpressionTypeStyle> = {
name,
from: {
null: () => {
@ -32,4 +33,4 @@ export const style = (): ExpressionType<typeof name, ExpressionTypeStyle> => ({
};
},
},
});
};

View file

@ -21,7 +21,7 @@ import { number } from '../number';
describe('number', () => {
it('should fail when typecasting not numeric string to number', () => {
expect(() => number().from!.string('123test', {})).toThrowErrorMatchingInlineSnapshot(
expect(() => number.from!.string('123test', {})).toThrowErrorMatchingInlineSnapshot(
`"Can't typecast \\"123test\\" string to number"`
);
});

View file

@ -34,7 +34,7 @@ export type ExpressionValueConverter<I extends ExpressionValue, O extends Expres
* A generic type which represents a custom Expression Type Definition that's
* registered to the Interpreter.
*/
export interface ExpressionType<
export interface ExpressionTypeDefinition<
Name extends string,
Value extends ExpressionValueUnboxed | ExpressionValueBoxed,
SerializedType = undefined
@ -54,4 +54,4 @@ export interface ExpressionType<
help?: string;
}
export type AnyExpressionType = ExpressionType<string, ExpressionValueBoxed>;
export type AnyExpressionTypeDefinition = ExpressionTypeDefinition<any, any, any>;

View file

@ -17,6 +17,13 @@
* under the License.
*/
export * from './type';
export * from './types';
export * from './ast';
export * from './fonts';
export * from './expression_types';
export * from './expression_functions';
export * from './expression_renderers';
export * from './executor';
export * from './execution';
export * from './service';
export * from './util';

View file

@ -0,0 +1,47 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ExecutionContext } from './execution/types';
export const createMockExecutionContext = <ExtraContext extends object = object>(
extraContext: ExtraContext = {} as ExtraContext
): ExecutionContext & ExtraContext => {
const executionContext: ExecutionContext = {
getInitialInput: jest.fn(),
variables: {},
types: {},
abortSignal: {
aborted: false,
addEventListener: jest.fn(),
dispatchEvent: jest.fn(),
onabort: jest.fn(),
removeEventListener: jest.fn(),
},
inspectorAdapters: {
requests: {} as any,
data: {} as any,
},
search: {},
};
return {
...executionContext,
...extraContext,
};
};

View file

@ -0,0 +1,54 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ExpressionsService } from './expressions_services';
describe('ExpressionsService', () => {
test('can instantiate', () => {
new ExpressionsService();
});
test('returns expected setup contract', () => {
const expressions = new ExpressionsService();
expect(expressions.setup()).toMatchObject({
getFunctions: expect.any(Function),
registerFunction: expect.any(Function),
registerType: expect.any(Function),
registerRenderer: expect.any(Function),
run: expect.any(Function),
});
});
test('returns expected start contract', () => {
const expressions = new ExpressionsService();
expressions.setup();
expect(expressions.start()).toMatchObject({
getFunctions: expect.any(Function),
run: expect.any(Function),
});
});
test('has pre-installed default functions', () => {
const expressions = new ExpressionsService();
expect(typeof expressions.setup().getFunctions().var_set).toBe('object');
});
});

View file

@ -0,0 +1,168 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Executor } from '../executor';
import { ExpressionRendererRegistry } from '../expression_renderers';
import { ExpressionAstExpression } from '../ast';
export type ExpressionsServiceSetup = ReturnType<ExpressionsService['setup']>;
export type ExpressionsServiceStart = ReturnType<ExpressionsService['start']>;
/**
* `ExpressionsService` class is used for multiple purposes:
*
* 1. It implements the same Expressions service that can be used on both:
* (1) server-side and (2) browser-side.
* 2. It implements the same Expressions service that users can fork/clone,
* thus have their own instance of the Expressions plugin.
* 3. `ExpressionsService` defines the public contracts of *setup* and *start*
* Kibana Platform life-cycles for ease-of-use on server-side and browser-side.
* 4. `ExpressionsService` creates a bound version of all exported contract functions.
* 5. Functions are bound the way there are:
*
* ```ts
* registerFunction = (...args: Parameters<Executor['registerFunction']>
* ): ReturnType<Executor['registerFunction']> => this.executor.registerFunction(...args);
* ```
*
* so that JSDoc appears in developers IDE when they use those `plugins.expressions.registerFunction(`.
*/
export class ExpressionsService {
public readonly executor = Executor.createWithDefaults();
public readonly renderers = new ExpressionRendererRegistry();
/**
* Register an expression function, which will be possible to execute as
* part of the expression pipeline.
*
* Below we register a function which simply sleeps for given number of
* milliseconds to delay the execution and outputs its input as-is.
*
* ```ts
* expressions.registerFunction({
* name: 'sleep',
* args: {
* time: {
* aliases: ['_'],
* help: 'Time in milliseconds for how long to sleep',
* types: ['number'],
* },
* },
* help: '',
* fn: async (input, args, context) => {
* await new Promise(r => setTimeout(r, args.time));
* return input;
* },
* }
* ```
*
* The actual function is defined in the `fn` key. The function can be *async*.
* It receives three arguments: (1) `input` is the output of the previous function
* or the initial input of the expression if the function is first in chain;
* (2) `args` are function arguments as defined in expression string, that can
* be edited by user (e.g in case of Canvas); (3) `context` is a shared object
* passed to all functions that can be used for side-effects.
*/
public readonly registerFunction = (
...args: Parameters<Executor['registerFunction']>
): ReturnType<Executor['registerFunction']> => this.executor.registerFunction(...args);
/**
* Executes expression string or a parsed expression AST and immediately
* returns the result.
*
* Below example will execute `sleep 100 | clog` expression with `123` initial
* input to the first function.
*
* ```ts
* expressions.run('sleep 100 | clog', 123);
* ```
*
* - `sleep 100` will delay execution by 100 milliseconds and pass the `123` input as
* its output.
* - `clog` will print to console `123` and pass it as its output.
* - The final result of the execution will be `123`.
*
* Optionally, you can pass an object as the third argument which will be used
* to extend the `ExecutionContext`&mdash;an object passed to each function
* as the third argument, that allows functions to perform side-effects.
*
* ```ts
* expressions.run('...', null, { elasticsearchClient });
* ```
*/
public readonly run = <
Input,
Output,
ExtraContext extends Record<string, unknown> = Record<string, unknown>
>(
ast: string | ExpressionAstExpression,
input: Input,
context?: ExtraContext
): Promise<Output> => this.executor.run<Input, Output, ExtraContext>(ast, input, context);
public setup() {
const { executor, renderers, registerFunction, run } = this;
const getFunction = executor.getFunction.bind(executor);
const getFunctions = executor.getFunctions.bind(executor);
const getRenderer = renderers.get.bind(renderers);
const getRenderers = renderers.toJS.bind(renderers);
const getType = executor.getType.bind(executor);
const getTypes = executor.getTypes.bind(executor);
const registerRenderer = renderers.register.bind(renderers);
const registerType = executor.registerType.bind(executor);
return {
getFunction,
getFunctions,
getRenderer,
getRenderers,
getType,
getTypes,
registerFunction,
registerRenderer,
registerType,
run,
};
}
public start() {
const { executor, renderers, run } = this;
const getFunction = executor.getFunction.bind(executor);
const getFunctions = executor.getFunctions.bind(executor);
const getRenderer = renderers.get.bind(renderers);
const getRenderers = renderers.toJS.bind(renderers);
const getType = executor.getType.bind(executor);
const getTypes = executor.getTypes.bind(executor);
return {
getFunction,
getFunctions,
getRenderer,
getRenderers,
getType,
getTypes,
run,
};
}
public stop() {}
}

View file

@ -0,0 +1,20 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export * from './expressions_services';

Some files were not shown because too many files have changed in this diff Show more