ExecutionContract (#57559) (#57806)

* feat: 🎸 add ExecutionContract class

* test: 💍 add Execution and Executor tests

* feat: 🎸 add .execute() method to ExpressionsService

* refactor: 💡 replace ExpressionDataHandler by ExecutionContract

* fix: 🐛 fix TypeScript typecheck errors

* docs: ✏️ add JSDocs to ExecutionContract

* refactor: 💡 make .ast and .expresions both optional

* test: 💍 fix test

* test: 💍 fix a test

* test: 💍 fix interpreter functional tests
This commit is contained in:
Vadim Dalecky 2020-02-17 16:41:19 +01:00 committed by GitHub
parent e34ee17574
commit c4a2067838
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 478 additions and 399 deletions

View file

@ -21,6 +21,7 @@ import { Execution } from './execution';
import { parseExpression } from '../ast';
import { createUnitTestExecutor } from '../test_helpers';
import { ExpressionFunctionDefinition } from '../../public';
import { ExecutionContract } from './execution_contract';
const createExecution = (
expression: string = 'foo bar=123',
@ -48,7 +49,7 @@ const run = async (
describe('Execution', () => {
test('can instantiate', () => {
const execution = createExecution('foo bar=123');
expect(execution.params.ast.chain[0].arguments.bar).toEqual([123]);
expect(execution.state.get().ast.chain[0].arguments.bar).toEqual([123]);
});
test('initial input is null at creation', () => {
@ -127,6 +128,40 @@ describe('Execution', () => {
});
});
describe('.expression', () => {
test('uses expression passed in to constructor', () => {
const expression = 'add val="1"';
const executor = createUnitTestExecutor();
const execution = new Execution({
executor,
expression,
});
expect(execution.expression).toBe(expression);
});
test('generates expression from AST if not passed to constructor', () => {
const expression = 'add val="1"';
const executor = createUnitTestExecutor();
const execution = new Execution({
ast: parseExpression(expression),
executor,
});
expect(execution.expression).toBe(expression);
});
});
describe('.contract', () => {
test('is instance of ExecutionContract', () => {
const execution = createExecution('add val=1');
expect(execution.contract).toBeInstanceOf(ExecutionContract);
});
test('execution returns the same expression string', () => {
const execution = createExecution('add val=1');
expect(execution.expression).toBe(execution.contract.getExpression());
});
});
describe('execution context', () => {
test('context.variables is an object', async () => {
const { result } = (await run('introspectContext key="variables"')) as any;

View file

@ -24,17 +24,25 @@ 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 {
ExpressionAstExpression,
ExpressionAstFunction,
parse,
formatExpression,
parseExpression,
} 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';
import { ExecutionContract } from './execution_contract';
export interface ExecutionParams<
ExtraContext extends Record<string, unknown> = Record<string, unknown>
> {
executor: Executor<any>;
ast: ExpressionAstExpression;
ast?: ExpressionAstExpression;
expression?: string;
context?: ExtraContext;
}
@ -85,6 +93,19 @@ export class Execution<
*/
private readonly firstResultFuture = new Defer<Output>();
/**
* Contract is a public representation of `Execution` instances. Contract we
* can return to other plugins for their consumption.
*/
public readonly contract: ExecutionContract<
ExtraContext,
Input,
Output,
InspectorAdapters
> = new ExecutionContract<ExtraContext, Input, Output, InspectorAdapters>(this);
public readonly expression: string;
public get result(): Promise<unknown> {
return this.firstResultFuture.promise;
}
@ -94,7 +115,17 @@ export class Execution<
}
constructor(public readonly params: ExecutionParams<ExtraContext>) {
const { executor, ast } = params;
const { executor } = params;
if (!params.ast && !params.expression) {
throw new TypeError('Execution params should contain at least .ast or .expression key.');
} else if (params.ast && params.expression) {
throw new TypeError('Execution params cannot contain both .ast and .expression key.');
}
this.expression = params.expression || formatExpression(params.ast!);
const ast = params.ast || parseExpression(this.expression);
this.state = createExecutionContainer<Output>({
...executor.state.get(),
state: 'not-started',

View file

@ -0,0 +1,140 @@
/*
* 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 { ExecutionContract } from './execution_contract';
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;
};
describe('ExecutionContract', () => {
test('can instantiate', () => {
const execution = createExecution('foo bar=123');
const contract = new ExecutionContract(execution);
expect(contract).toBeInstanceOf(ExecutionContract);
});
test('can get the AST of expression', () => {
const execution = createExecution('foo bar=123');
const contract = new ExecutionContract(execution);
expect(contract.getAst()).toMatchObject({
type: 'expression',
chain: expect.any(Array),
});
});
test('can get expression string', () => {
const execution = createExecution('foo bar=123');
const contract = new ExecutionContract(execution);
expect(contract.getExpression()).toBe('foo bar=123');
});
test('can cancel execution', () => {
const execution = createExecution('foo bar=123');
const spy = jest.spyOn(execution, 'cancel');
const contract = new ExecutionContract(execution);
expect(spy).toHaveBeenCalledTimes(0);
contract.cancel();
expect(spy).toHaveBeenCalledTimes(1);
});
test('can get inspector adapters', () => {
const execution = createExecution('foo bar=123');
const contract = new ExecutionContract(execution);
expect(contract.inspect()).toMatchObject({
data: expect.any(Object),
requests: expect.any(Object),
});
});
test('can get error result of the expression execution', async () => {
const execution = createExecution('foo bar=123');
const contract = new ExecutionContract(execution);
execution.start();
const result = await contract.getData();
expect(result).toMatchObject({
type: 'error',
});
});
test('can get result of the expression execution', async () => {
const execution = createExecution('var_set name="foo" value="bar" | var name="foo"');
const contract = new ExecutionContract(execution);
execution.start();
const result = await contract.getData();
expect(result).toBe('bar');
});
describe('isPending', () => {
test('is true if execution has not been started', async () => {
const execution = createExecution('var_set name="foo" value="bar" | var name="foo"');
const contract = new ExecutionContract(execution);
expect(contract.isPending).toBe(true);
});
test('is true when execution just started', async () => {
const execution = createExecution('var_set name="foo" value="bar" | var name="foo"');
const contract = new ExecutionContract(execution);
execution.start();
expect(contract.isPending).toBe(true);
});
test('is false when execution finished successfully', async () => {
const execution = createExecution('var_set name="foo" value="bar" | var name="foo"');
const contract = new ExecutionContract(execution);
execution.start();
await execution.result;
expect(contract.isPending).toBe(false);
expect(execution.state.get().state).toBe('result');
});
test('is false when execution finished with error', async () => {
const execution = createExecution('var_set name="foo" value="bar" | var name="foo"');
const contract = new ExecutionContract(execution);
execution.start();
await execution.result;
execution.state.get().state = 'error';
expect(contract.isPending).toBe(false);
expect(execution.state.get().state).toBe('error');
});
});
});

View file

@ -0,0 +1,90 @@
/*
* 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';
/**
* `ExecutionContract` is a wrapper around `Execution` class. It provides the
* same functionality but does not expose Expressions plugin internals.
*/
export class ExecutionContract<
ExtraContext extends Record<string, unknown> = Record<string, unknown>,
Input = unknown,
Output = unknown,
InspectorAdapters = unknown
> {
public get isPending(): boolean {
const state = this.execution.state.get().state;
const finished = state === 'error' || state === 'result';
return !finished;
}
constructor(
protected readonly execution: Execution<ExtraContext, Input, Output, InspectorAdapters>
) {}
/**
* Cancel the execution of the expression. This will set abort signal
* (available in execution context) to aborted state, letting expression
* functions to stop their execution.
*/
cancel = () => {
this.execution.cancel();
};
/**
* Returns the final output of expression, if any error happens still
* wraps that error into `ExpressionValueError` type and returns that.
* This function never throws.
*/
getData = async () => {
try {
return await this.execution.result;
} catch (e) {
return {
type: 'error',
error: {
type: e.type,
message: e.message,
stack: e.stack,
},
};
}
};
/**
* Get string representation of the expression. Returns the original string
* if execution was started from a string. If execution was started from an
* AST this method returns a string generated from AST.
*/
getExpression = () => {
return this.execution.expression;
};
/**
* Get AST used to execute the expression.
*/
getAst = () => this.execution.state.get().ast;
/**
* Get Inspector adapters provided to all functions of expression through
* execution context.
*/
inspect = () => this.execution.inspectorAdapters;
}

View file

@ -20,3 +20,4 @@
export * from './types';
export * from './container';
export * from './execution';
export * from './execution_contract';

View file

@ -0,0 +1,59 @@
/*
* 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 { parseExpression } from '../ast';
// eslint-disable-next-line
const { __getArgs } = require('../execution/execution');
jest.mock('../execution/execution', () => {
const mockedModule = {
args: undefined,
__getArgs: () => mockedModule.args,
Execution: function ExecutionMock(...args: any) {
mockedModule.args = args;
},
};
return mockedModule;
});
describe('Executor mocked execution tests', () => {
describe('createExecution()', () => {
describe('when execution is created from string', () => {
test('passes expression string to Execution', () => {
const executor = new Executor();
executor.createExecution('foo bar="baz"');
expect(__getArgs()[0].expression).toBe('foo bar="baz"');
});
});
describe('when execution is created from AST', () => {
test('does not pass in expression string', () => {
const executor = new Executor();
const ast = parseExpression('foo bar="baz"');
executor.createExecution(ast);
expect(__getArgs()[0].expression).toBe(undefined);
});
});
});
});

View file

@ -130,7 +130,7 @@ describe('Executor', () => {
const execution = executor.createExecution('foo bar="baz"');
expect(execution).toBeInstanceOf(Execution);
expect(execution.params.ast.chain[0].function).toBe('foo');
expect(execution.state.get().ast.chain[0].function).toBe('foo');
});
test('returns Execution object from AST', () => {
@ -139,7 +139,7 @@ describe('Executor', () => {
const execution = executor.createExecution(ast);
expect(execution).toBeInstanceOf(Execution);
expect(execution.params.ast.chain[0].function).toBe('foo');
expect(execution.state.get().ast.chain[0].function).toBe('foo');
});
test('Execution inherits context from Executor', () => {

View file

@ -22,12 +22,12 @@
import { ExecutorState, ExecutorContainer } from './container';
import { createExecutorContainer } from './container';
import { AnyExpressionFunctionDefinition, ExpressionFunction } from '../expression_functions';
import { Execution } from '../execution/execution';
import { Execution, ExecutionParams } 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 { ExpressionAstExpression, ExpressionAstNode } from '../ast';
import { typeSpecs } from '../expression_types/specs';
import { functionSpecs } from '../expression_functions/specs';
@ -186,19 +186,27 @@ export class Executor<Context extends Record<string, unknown> = Record<string, u
return (await execution.result) as Output;
}
public createExecution<ExtraContext extends Record<string, unknown> = Record<string, unknown>>(
public createExecution<
ExtraContext extends Record<string, unknown> = Record<string, unknown>,
Input = unknown,
Output = 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,
): Execution<Context & ExtraContext, Input, Output> {
const params: ExecutionParams<Context & ExtraContext> = {
executor: this,
context: {
...this.context,
...context,
} as Context & ExtraContext,
});
};
if (typeof ast === 'string') params.expression = ast;
else params.ast = ast;
const execution = new Execution<Context & ExtraContext, Input, Output>(params);
return execution;
}
}

View file

@ -20,6 +20,7 @@
import { Executor } from '../executor';
import { ExpressionRendererRegistry } from '../expression_renderers';
import { ExpressionAstExpression } from '../ast';
import { ExecutionContract } from '../execution/execution_contract';
export type ExpressionsServiceSetup = ReturnType<ExpressionsService['setup']>;
export type ExpressionsServiceStart = ReturnType<ExpressionsService['start']>;
@ -117,6 +118,26 @@ export class ExpressionsService {
context?: ExtraContext
): Promise<Output> => this.executor.run<Input, Output, ExtraContext>(ast, input, context);
/**
* Starts expression execution and immediately returns `ExecutionContract`
* instance that tracks the progress of the execution and can be used to
* interact with the execution.
*/
public readonly execute = <
Input = unknown,
Output = unknown,
ExtraContext extends Record<string, unknown> = Record<string, unknown>
>(
ast: string | ExpressionAstExpression,
// This any is for legacy reasons.
input: Input = { type: 'null' } as any,
context?: ExtraContext
): ExecutionContract<ExtraContext, Input, Output> => {
const execution = this.executor.createExecution<ExtraContext, Input, Output>(ast, context);
execution.start(input);
return execution.contract;
};
public setup() {
const { executor, renderers, registerFunction, run } = this;
@ -144,7 +165,7 @@ export class ExpressionsService {
}
public start() {
const { executor, renderers, run } = this;
const { execute, executor, renderers, run } = this;
const getFunction = executor.getFunction.bind(executor);
const getFunctions = executor.getFunctions.bind(executor);
@ -154,6 +175,7 @@ export class ExpressionsService {
const getTypes = executor.getTypes.bind(executor);
return {
execute,
getFunction,
getFunctions,
getRenderer,

View file

@ -1,100 +0,0 @@
/*
* 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 { execute, ExpressionDataHandler } from './execute';
import { ExpressionAstExpression, parseExpression } from '../common';
jest.mock('./services', () => ({
getInterpreter: () => {
return {
interpretAst: async (expression: ExpressionAstExpression) => {
return {};
},
};
},
getNotifications: jest.fn(() => {
return {
toasts: {
addError: jest.fn(() => {}),
},
};
}),
}));
describe('execute helper function', () => {
it('returns ExpressionDataHandler instance', () => {
const response = execute('test');
expect(response).toBeInstanceOf(ExpressionDataHandler);
});
});
describe('ExpressionDataHandler', () => {
const expressionString = 'test';
describe('constructor', () => {
it('accepts expression string', () => {
const expressionDataHandler = new ExpressionDataHandler(expressionString, {});
expect(expressionDataHandler.getExpression()).toEqual(expressionString);
});
it('accepts expression AST', () => {
const expressionAST = parseExpression(expressionString) as ExpressionAstExpression;
const expressionDataHandler = new ExpressionDataHandler(expressionAST, {});
expect(expressionDataHandler.getExpression()).toEqual(expressionString);
expect(expressionDataHandler.getAst()).toEqual(expressionAST);
});
it('allows passing in context', () => {
const expressionDataHandler = new ExpressionDataHandler(expressionString, {
context: { test: 'hello' },
});
expect(expressionDataHandler.getExpression()).toEqual(expressionString);
});
it('allows passing in search context', () => {
const expressionDataHandler = new ExpressionDataHandler(expressionString, {
searchContext: { filters: [] },
});
expect(expressionDataHandler.getExpression()).toEqual(expressionString);
});
});
describe('getData()', () => {
it('returns a promise', () => {
const expressionDataHandler = new ExpressionDataHandler(expressionString, {});
expect(expressionDataHandler.getData()).toBeInstanceOf(Promise);
});
it('promise resolves with data', async () => {
const expressionDataHandler = new ExpressionDataHandler(expressionString, {});
expect(await expressionDataHandler.getData()).toEqual({});
});
});
it('cancel() aborts request', () => {
const expressionDataHandler = new ExpressionDataHandler(expressionString, {});
expressionDataHandler.cancel();
});
it('inspect() returns correct inspector adapters', () => {
const expressionDataHandler = new ExpressionDataHandler(expressionString, {});
expect(expressionDataHandler.inspect()).toHaveProperty('requests');
expect(expressionDataHandler.inspect()).toHaveProperty('data');
});
});

View file

@ -1,138 +0,0 @@
/*
* 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 { DataAdapter, RequestAdapter, Adapters } from '../../inspector/public';
import { getInterpreter } from './services';
import { IExpressionLoaderParams } from './types';
import {
ExpressionAstExpression,
parseExpression,
formatExpression,
ExpressionValue,
} from '../common';
/**
* The search context describes a specific context (filters, time range and query)
* that will be applied to the expression for execution. Not every expression will
* be effected by that. You have to use special functions
* that will pick up this search context and forward it to following functions that
* understand it.
*/
export class ExpressionDataHandler {
private abortController: AbortController;
private expression: string;
private ast: ExpressionAstExpression;
private inspectorAdapters: Adapters;
private promise: Promise<ExpressionValue>;
public isPending: boolean = true;
constructor(expression: string | ExpressionAstExpression, params: IExpressionLoaderParams) {
if (typeof expression === 'string') {
this.expression = expression;
this.ast = parseExpression(expression);
} else {
this.ast = expression;
this.expression = formatExpression(this.ast);
}
this.abortController = new AbortController();
this.inspectorAdapters = params.inspectorAdapters || this.getActiveInspectorAdapters();
const defaultInput = { type: 'null' };
const interpreter = getInterpreter();
this.promise = interpreter
.interpretAst<any, ExpressionValue>(this.ast, params.context || defaultInput, {
search: params.searchContext,
inspectorAdapters: this.inspectorAdapters,
abortSignal: this.abortController.signal,
variables: params.variables,
})
.then(
(v: ExpressionValue) => {
this.isPending = false;
return v;
},
() => {
this.isPending = false;
}
) as Promise<ExpressionValue>;
}
cancel = () => {
this.abortController.abort();
};
getData = async () => {
try {
return await this.promise;
} catch (e) {
return {
type: 'error',
error: {
type: e.type,
message: e.message,
stack: e.stack,
},
};
}
};
getExpression = () => {
return this.expression;
};
getAst = () => {
return this.ast;
};
inspect = () => {
return this.inspectorAdapters;
};
/**
* Returns an object of all inspectors for this vis object.
* This must only be called after this.type has properly be initialized,
* since we need to read out data from the the vis type to check which
* inspectors are available.
*/
private getActiveInspectorAdapters = (): Adapters => {
const adapters: Adapters = {};
// Add the requests inspector adapters if the vis type explicitly requested it via
// inspectorAdapters.requests: true in its definition or if it's using the courier
// request handler, since that will automatically log its requests.
adapters.requests = new RequestAdapter();
// Add the data inspector adapter if the vis type requested it or if the
// vis is using courier, since we know that courier supports logging
// its data.
adapters.data = new DataAdapter();
return adapters;
};
}
export function execute(
expression: string | ExpressionAstExpression,
params: IExpressionLoaderParams = {}
): ExpressionDataHandler {
return new ExpressionDataHandler(expression, params);
}

View file

@ -37,7 +37,6 @@ export {
ReactExpressionRendererProps,
ReactExpressionRendererType,
} from './react_expression_renderer';
export { ExpressionDataHandler } from './execute';
export { ExpressionRenderHandler } from './render';
export {
AnyExpressionFunctionDefinition,
@ -48,6 +47,7 @@ export {
DatatableColumnType,
DatatableRow,
Execution,
ExecutionContract,
ExecutionContainer,
ExecutionContext,
ExecutionParams,

View file

@ -17,12 +17,14 @@
* under the License.
*/
import { Observable } from 'rxjs';
import { first, skip, toArray } from 'rxjs/operators';
import { loader, ExpressionLoader } from './loader';
import { ExpressionDataHandler } from './execute';
import { Observable } from 'rxjs';
import { ExpressionAstExpression, parseExpression, IInterpreterRenderHandlers } from '../common';
// eslint-disable-next-line
const { __getLastExecution } = require('./services');
const element: HTMLElement = null as any;
jest.mock('./services', () => {
@ -33,7 +35,13 @@ jest.mock('./services', () => {
},
},
};
return {
// eslint-disable-next-line
const service = new (require('../common/service/expressions_services').ExpressionsService as any)();
const moduleMock = {
__execution: undefined,
__getLastExecution: () => moduleMock.__execution,
getInterpreter: () => {
return {
interpretAst: async (expression: ExpressionAstExpression) => {
@ -51,17 +59,19 @@ jest.mock('./services', () => {
},
};
}),
getExpressionsService: () => service,
};
});
jest.mock('./execute', () => {
const actual = jest.requireActual('./execute');
return {
ExpressionDataHandler: jest
.fn()
.mockImplementation((...args) => new actual.ExpressionDataHandler(...args)),
execute: jest.fn().mockReturnValue(actual.execute),
const execute = service.execute;
service.execute = (...args: any) => {
const execution = execute(...args);
jest.spyOn(execution, 'getData');
jest.spyOn(execution, 'cancel');
moduleMock.__execution = execution;
return execution;
};
return moduleMock;
});
describe('execute helper function', () => {
@ -97,9 +107,9 @@ describe('ExpressionLoader', () => {
});
it('emits on $data when data is available', async () => {
const expressionLoader = new ExpressionLoader(element, expressionString, {});
const expressionLoader = new ExpressionLoader(element, 'var foo', { variables: { foo: 123 } });
const response = await expressionLoader.data$.pipe(first()).toPromise();
expect(response).toEqual({ type: 'render', as: 'test' });
expect(response).toBe(123);
});
it('emits on loading$ on initial load and on updates', async () => {
@ -128,94 +138,13 @@ describe('ExpressionLoader', () => {
});
it('cancels the previous request when the expression is updated', () => {
const cancelMock = jest.fn();
const expressionLoader = new ExpressionLoader(element, 'var foo', {});
const execution = __getLastExecution();
jest.spyOn(execution, 'cancel');
(ExpressionDataHandler as jest.Mock).mockImplementationOnce(() => ({
getData: () => true,
cancel: cancelMock,
isPending: () => true,
inspect: () => {},
}));
const expressionLoader = new ExpressionLoader(element, expressionString, {});
expressionLoader.update('new', {});
expect(cancelMock).toHaveBeenCalledTimes(1);
});
it('does not send an observable message if a request was aborted', () => {
const cancelMock = jest.fn();
const getData = jest
.fn()
.mockResolvedValueOnce({
type: 'error',
error: {
name: 'AbortError',
},
})
.mockResolvedValueOnce({
type: 'real',
});
(ExpressionDataHandler as jest.Mock).mockImplementationOnce(() => ({
getData,
cancel: cancelMock,
isPending: () => true,
inspect: () => {},
}));
(ExpressionDataHandler as jest.Mock).mockImplementationOnce(() => ({
getData,
cancel: cancelMock,
isPending: () => true,
inspect: () => {},
}));
const expressionLoader = new ExpressionLoader(element, expressionString, {});
expect.assertions(2);
expressionLoader.data$.subscribe({
next(data) {
expect(data).toEqual({
type: 'real',
});
},
error() {
expect(false).toEqual('Should not be called');
},
});
expressionLoader.update('new expression', {});
expect(getData).toHaveBeenCalledTimes(2);
});
it('sends an observable error if the data fetching failed', () => {
const cancelMock = jest.fn();
const getData = jest.fn().mockResolvedValue('rejected');
(ExpressionDataHandler as jest.Mock).mockImplementationOnce(() => ({
getData,
cancel: cancelMock,
isPending: () => true,
inspect: () => {},
}));
const expressionLoader = new ExpressionLoader(element, expressionString, {});
expect.assertions(2);
expressionLoader.data$.subscribe({
next(data) {
expect(data).toEqual('Should not be called');
},
error(error) {
expect(error.message).toEqual('Could not fetch data');
},
});
expect(getData).toHaveBeenCalledTimes(1);
expect(execution.cancel).toHaveBeenCalledTimes(0);
expressionLoader.update('var bar', {});
expect(execution.cancel).toHaveBeenCalledTimes(1);
});
it('inspect() returns correct inspector adapters', () => {

View file

@ -20,11 +20,11 @@
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { Adapters, InspectorSession } from '../../inspector/public';
import { ExpressionDataHandler } from './execute';
import { ExpressionRenderHandler } from './render';
import { IExpressionLoaderParams } from './types';
import { ExpressionAstExpression } from '../common';
import { getInspector } from './services';
import { getInspector, getExpressionsService } from './services';
import { ExecutionContract } from '../common/execution/execution_contract';
type Data = any;
@ -35,7 +35,7 @@ export class ExpressionLoader {
events$: ExpressionRenderHandler['events$'];
loading$: Observable<void>;
private dataHandler: ExpressionDataHandler | undefined;
private execution: ExecutionContract | undefined;
private renderHandler: ExpressionRenderHandler;
private dataSubject: Subject<Data>;
private loadingSubject: Subject<boolean>;
@ -93,26 +93,26 @@ export class ExpressionLoader {
this.dataSubject.complete();
this.loadingSubject.complete();
this.renderHandler.destroy();
if (this.dataHandler) {
this.dataHandler.cancel();
if (this.execution) {
this.execution.cancel();
}
}
cancel() {
if (this.dataHandler) {
this.dataHandler.cancel();
if (this.execution) {
this.execution.cancel();
}
}
getExpression(): string | undefined {
if (this.dataHandler) {
return this.dataHandler.getExpression();
if (this.execution) {
return this.execution.getExpression();
}
}
getAst(): ExpressionAstExpression | undefined {
if (this.dataHandler) {
return this.dataHandler.getAst();
if (this.execution) {
return this.execution.getAst();
}
}
@ -130,9 +130,7 @@ export class ExpressionLoader {
}
inspect(): Adapters | undefined {
if (this.dataHandler) {
return this.dataHandler.inspect();
}
return this.execution ? (this.execution.inspect() as Adapters) : undefined;
}
update(expression?: string | ExpressionAstExpression, params?: IExpressionLoaderParams): void {
@ -150,15 +148,19 @@ export class ExpressionLoader {
expression: string | ExpressionAstExpression,
params: IExpressionLoaderParams
): Promise<void> => {
if (this.dataHandler && this.dataHandler.isPending) {
this.dataHandler.cancel();
if (this.execution && this.execution.isPending) {
this.execution.cancel();
}
this.setParams(params);
this.dataHandler = new ExpressionDataHandler(expression, params);
if (!params.inspectorAdapters) params.inspectorAdapters = this.dataHandler.inspect();
const prevDataHandler = this.dataHandler;
this.execution = getExpressionsService().execute(expression, params.context, {
search: params.searchContext,
variables: params.variables || {},
inspectorAdapters: params.inspectorAdapters,
});
if (!params.inspectorAdapters) params.inspectorAdapters = this.execution.inspect() as Adapters;
const prevDataHandler = this.execution;
const data = await prevDataHandler.getData();
if (this.dataHandler !== prevDataHandler) {
if (this.execution !== prevDataHandler) {
return;
}
this.dataSubject.next(data);

View file

@ -65,7 +65,6 @@ const createSetupContract = (): Setup => {
const createStartContract = (): Start => {
return {
execute: jest.fn(),
ExpressionDataHandler: jest.fn(),
ExpressionLoader: jest.fn(),
ExpressionRenderHandler: jest.fn(),
getFunction: jest.fn(),

View file

@ -65,6 +65,15 @@ describe('ExpressionsPublicPlugin', () => {
}
`);
});
test('"kibana" function return value of type "kibana_context"', async () => {
const { doStart } = await expressionsPluginMock.createPlugin();
const start = await doStart();
const execution = start.execute('kibana');
const result = await execution.getData();
expect((result as any).type).toBe('kibana_context');
});
});
});
});

View file

@ -36,11 +36,11 @@ import {
setInterpreter,
setRenderersRegistry,
setNotifications,
setExpressionsService,
} from './services';
import { kibanaContext as kibanaContextFunction } from './expression_functions/kibana_context';
import { ReactExpressionRenderer } from './react_expression_renderer';
import { ExpressionLoader, loader } from './loader';
import { ExpressionDataHandler, execute } from './execute';
import { render, ExpressionRenderHandler } from './render';
export interface ExpressionsSetupDeps {
@ -92,8 +92,6 @@ export interface ExpressionsSetup extends ExpressionsServiceSetup {
}
export interface ExpressionsStart extends ExpressionsServiceStart {
execute: typeof execute;
ExpressionDataHandler: typeof ExpressionDataHandler;
ExpressionLoader: typeof ExpressionLoader;
ExpressionRenderHandler: typeof ExpressionRenderHandler;
loader: typeof loader;
@ -118,6 +116,7 @@ export class ExpressionsPublicPlugin
executor.registerFunction(kibanaContextFunction());
setRenderersRegistry(renderers);
setExpressionsService(this.expressions);
const expressionsSetup = expressions.setup();
@ -180,8 +179,6 @@ export class ExpressionsPublicPlugin
return {
...expressionsStart,
execute,
ExpressionDataHandler,
ExpressionLoader,
ExpressionRenderHandler,
loader,

View file

@ -22,6 +22,7 @@ import { createKibanaUtilsCore, createGetterSetter } from '../../kibana_utils/pu
import { ExpressionInterpreter } from './types';
import { Start as IInspector } from '../../inspector/public';
import { ExpressionsSetup } from './plugin';
import { ExpressionsService } from '../common';
export const { getCoreStart, setCoreStart, savedObjects } = createKibanaUtilsCore();
@ -37,3 +38,7 @@ export const [getNotifications, setNotifications] = createGetterSetter<Notificat
export const [getRenderersRegistry, setRenderersRegistry] = createGetterSetter<
ExpressionsSetup['__LEGACY']['renderers']
>('Renderers registry');
export const [getExpressionsService, setExpressionsService] = createGetterSetter<
ExpressionsService
>('ExpressionsService');

View file

@ -22,7 +22,7 @@ import { EuiPage, EuiPageBody, EuiPageContent, EuiPageContentHeader } from '@ela
import { first } from 'rxjs/operators';
import { IInterpreterRenderHandlers, ExpressionValue } from 'src/plugins/expressions';
import { RequestAdapter, DataAdapter } from '../../../../../../../../src/plugins/inspector';
import { Adapters, ExpressionRenderHandler, ExpressionDataHandler } from '../../types';
import { Adapters, ExpressionRenderHandler } from '../../types';
import { getExpressions } from '../../services';
declare global {
@ -31,7 +31,7 @@ declare global {
expressions: string,
context?: ExpressionValue,
initialContext?: ExpressionValue
) => ReturnType<ExpressionDataHandler['getData']>;
) => any;
renderPipelineResponse: (context?: ExpressionValue) => Promise<any>;
}
}
@ -61,12 +61,9 @@ class Main extends React.Component<{}, State> {
data: new DataAdapter(),
};
return getExpressions()
.execute(expression, {
.execute(expression, context || { type: 'null' }, {
inspectorAdapters: adapters,
context,
// TODO: naming / typing is confusing and doesn't match here
// searchContext is also a way to set initialContext and Context can't be set to SearchContext
searchContext: initialContext as any,
search: initialContext as any,
})
.getData();
};

View file

@ -17,12 +17,7 @@
* under the License.
*/
import {
ExpressionsStart,
ExpressionRenderHandler,
ExpressionDataHandler,
} from 'src/plugins/expressions/public';
import { ExpressionsStart, ExpressionRenderHandler } from 'src/plugins/expressions/public';
import { Adapters } from 'src/plugins/inspector/public';
export { ExpressionsStart, ExpressionRenderHandler, ExpressionDataHandler, Adapters };
export { ExpressionsStart, ExpressionRenderHandler, Adapters };

View file

@ -22,7 +22,7 @@ import { ExpectExpression, expectExpressionProvider } from './helpers';
import { FtrProviderContext } from '../../../functional/ftr_provider_context';
// this file showcases how to use testing utilities defined in helpers.ts together with the kbn_tp_run_pipeline
// test plugin to write autmated tests for interprete
// test plugin to write automated tests for interpreter
export default function({
getService,
updateBaselines,

View file

@ -20,10 +20,8 @@
import expect from '@kbn/expect';
import { ExpressionValue } from 'src/plugins/expressions';
import { FtrProviderContext } from '../../../functional/ftr_provider_context';
import { ExpressionDataHandler } from '../../plugins/kbn_tp_run_pipeline/public/np_ready/types';
type UnWrapPromise<T> = T extends Promise<infer U> ? U : T;
export type ExpressionResult = UnWrapPromise<ReturnType<ExpressionDataHandler['getData']>>;
export type ExpressionResult = any;
export type ExpectExpression = (
name: string,
@ -112,7 +110,7 @@ export function expectExpressionProvider({
if (!_currentContext.type) _currentContext.type = 'null';
return window
.runPipeline(_expression, _currentContext, _initialContext)
.then(expressionResult => {
.then((expressionResult: any) => {
done(expressionResult);
return expressionResult;
});