[Expressions] Fix setup and start contracts (#110841)
* Refactor executor forking to implement state inheritance * Fix setup and start contracts typings * Add support of named forks * Add expressions service life-cycle assertions
This commit is contained in:
parent
5b73ab1432
commit
c06d604785
|
@ -180,7 +180,7 @@ export class Execution<
|
|||
const ast = execution.ast || parseExpression(this.expression);
|
||||
|
||||
this.state = createExecutionContainer({
|
||||
...executor.state.get(),
|
||||
...executor.state,
|
||||
state: 'not-started',
|
||||
ast,
|
||||
});
|
||||
|
|
|
@ -49,7 +49,7 @@ export class TypesRegistry implements IRegistry<ExpressionType> {
|
|||
}
|
||||
|
||||
public get(id: string): ExpressionType | null {
|
||||
return this.executor.state.selectors.getType(id);
|
||||
return this.executor.getType(id) ?? null;
|
||||
}
|
||||
|
||||
public toJS(): Record<string, ExpressionType> {
|
||||
|
@ -71,7 +71,7 @@ export class FunctionsRegistry implements IRegistry<ExpressionFunction> {
|
|||
}
|
||||
|
||||
public get(id: string): ExpressionFunction | null {
|
||||
return this.executor.state.selectors.getFunction(id);
|
||||
return this.executor.getFunction(id) ?? null;
|
||||
}
|
||||
|
||||
public toJS(): Record<string, ExpressionFunction> {
|
||||
|
@ -95,22 +95,44 @@ export class Executor<Context extends Record<string, unknown> = Record<string, u
|
|||
return executor;
|
||||
}
|
||||
|
||||
public readonly state: ExecutorContainer<Context>;
|
||||
public readonly container: ExecutorContainer<Context>;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public readonly functions: FunctionsRegistry;
|
||||
public readonly functions = new FunctionsRegistry(this);
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public readonly types: TypesRegistry;
|
||||
public readonly types = new TypesRegistry(this);
|
||||
|
||||
protected parent?: Executor<Context>;
|
||||
|
||||
constructor(state?: ExecutorState<Context>) {
|
||||
this.state = createExecutorContainer<Context>(state);
|
||||
this.functions = new FunctionsRegistry(this);
|
||||
this.types = new TypesRegistry(this);
|
||||
this.container = createExecutorContainer<Context>(state);
|
||||
}
|
||||
|
||||
public get state(): ExecutorState<Context> {
|
||||
const parent = this.parent?.state;
|
||||
const state = this.container.get();
|
||||
|
||||
return {
|
||||
...(parent ?? {}),
|
||||
...state,
|
||||
types: {
|
||||
...(parent?.types ?? {}),
|
||||
...state.types,
|
||||
},
|
||||
functions: {
|
||||
...(parent?.functions ?? {}),
|
||||
...state.functions,
|
||||
},
|
||||
context: {
|
||||
...(parent?.context ?? {}),
|
||||
...state.context,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public registerFunction(
|
||||
|
@ -119,15 +141,18 @@ export class Executor<Context extends Record<string, unknown> = Record<string, u
|
|||
const fn = new ExpressionFunction(
|
||||
typeof functionDefinition === 'object' ? functionDefinition : functionDefinition()
|
||||
);
|
||||
this.state.transitions.addFunction(fn);
|
||||
this.container.transitions.addFunction(fn);
|
||||
}
|
||||
|
||||
public getFunction(name: string): ExpressionFunction | undefined {
|
||||
return this.state.get().functions[name];
|
||||
return this.container.get().functions[name] ?? this.parent?.getFunction(name);
|
||||
}
|
||||
|
||||
public getFunctions(): Record<string, ExpressionFunction> {
|
||||
return { ...this.state.get().functions };
|
||||
return {
|
||||
...(this.parent?.getFunctions() ?? {}),
|
||||
...this.container.get().functions,
|
||||
};
|
||||
}
|
||||
|
||||
public registerType(
|
||||
|
@ -136,23 +161,30 @@ export class Executor<Context extends Record<string, unknown> = Record<string, u
|
|||
const type = new ExpressionType(
|
||||
typeof typeDefinition === 'object' ? typeDefinition : typeDefinition()
|
||||
);
|
||||
this.state.transitions.addType(type);
|
||||
|
||||
this.container.transitions.addType(type);
|
||||
}
|
||||
|
||||
public getType(name: string): ExpressionType | undefined {
|
||||
return this.state.get().types[name];
|
||||
return this.container.get().types[name] ?? this.parent?.getType(name);
|
||||
}
|
||||
|
||||
public getTypes(): Record<string, ExpressionType> {
|
||||
return { ...this.state.get().types };
|
||||
return {
|
||||
...(this.parent?.getTypes() ?? {}),
|
||||
...this.container.get().types,
|
||||
};
|
||||
}
|
||||
|
||||
public extendContext(extraContext: Record<string, unknown>) {
|
||||
this.state.transitions.extendContext(extraContext);
|
||||
this.container.transitions.extendContext(extraContext);
|
||||
}
|
||||
|
||||
public get context(): Record<string, unknown> {
|
||||
return this.state.selectors.getContext();
|
||||
return {
|
||||
...(this.parent?.context ?? {}),
|
||||
...this.container.selectors.getContext(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -199,18 +231,15 @@ export class Executor<Context extends Record<string, unknown> = Record<string, u
|
|||
) {
|
||||
for (const link of ast.chain) {
|
||||
const { function: fnName, arguments: fnArgs } = link;
|
||||
const fn = getByAlias(this.state.get().functions, fnName);
|
||||
const fn = getByAlias(this.getFunctions(), fnName);
|
||||
|
||||
if (fn) {
|
||||
// if any of arguments are expressions we should migrate those first
|
||||
link.arguments = mapValues(fnArgs, (asts, argName) => {
|
||||
return asts.map((arg) => {
|
||||
if (arg && typeof arg === 'object') {
|
||||
return this.walkAst(arg, action);
|
||||
}
|
||||
return arg;
|
||||
});
|
||||
});
|
||||
link.arguments = mapValues(fnArgs, (asts) =>
|
||||
asts.map((arg) =>
|
||||
arg != null && typeof arg === 'object' ? this.walkAst(arg, action) : arg
|
||||
)
|
||||
);
|
||||
|
||||
action(fn, link);
|
||||
}
|
||||
|
@ -275,39 +304,19 @@ export class Executor<Context extends Record<string, unknown> = Record<string, u
|
|||
|
||||
private migrate(ast: SerializableRecord, version: string) {
|
||||
return this.walkAst(cloneDeep(ast) as ExpressionAstExpression, (fn, link) => {
|
||||
if (!fn.migrations[version]) return link;
|
||||
const updatedAst = fn.migrations[version](link) as ExpressionAstFunction;
|
||||
link.arguments = updatedAst.arguments;
|
||||
link.type = updatedAst.type;
|
||||
if (!fn.migrations[version]) {
|
||||
return;
|
||||
}
|
||||
|
||||
({ arguments: link.arguments, type: link.type } = fn.migrations[version](
|
||||
link
|
||||
) as ExpressionAstFunction);
|
||||
});
|
||||
}
|
||||
|
||||
public fork(): Executor<Context> {
|
||||
const initialState = this.state.get();
|
||||
const fork = new Executor<Context>(initialState);
|
||||
|
||||
/**
|
||||
* Synchronize registry state - make any new types, functions and context
|
||||
* also available in the forked instance of `Executor`.
|
||||
*/
|
||||
this.state.state$.subscribe(({ types, functions, context }) => {
|
||||
const state = fork.state.get();
|
||||
fork.state.set({
|
||||
...state,
|
||||
types: {
|
||||
...types,
|
||||
...state.types,
|
||||
},
|
||||
functions: {
|
||||
...functions,
|
||||
...state.functions,
|
||||
},
|
||||
context: {
|
||||
...context,
|
||||
...state.context,
|
||||
},
|
||||
});
|
||||
});
|
||||
const fork = new Executor<Context>();
|
||||
fork.parent = this;
|
||||
|
||||
return fork;
|
||||
}
|
||||
|
|
|
@ -17,11 +17,16 @@ describe('ExpressionsService', () => {
|
|||
const expressions = new ExpressionsService();
|
||||
|
||||
expect(expressions.setup()).toMatchObject({
|
||||
getFunction: expect.any(Function),
|
||||
getFunctions: expect.any(Function),
|
||||
getRenderer: expect.any(Function),
|
||||
getRenderers: expect.any(Function),
|
||||
getType: expect.any(Function),
|
||||
getTypes: expect.any(Function),
|
||||
registerFunction: expect.any(Function),
|
||||
registerType: expect.any(Function),
|
||||
registerRenderer: expect.any(Function),
|
||||
run: expect.any(Function),
|
||||
fork: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -30,7 +35,16 @@ describe('ExpressionsService', () => {
|
|||
expressions.setup();
|
||||
|
||||
expect(expressions.start()).toMatchObject({
|
||||
getFunction: expect.any(Function),
|
||||
getFunctions: expect.any(Function),
|
||||
getRenderer: expect.any(Function),
|
||||
getRenderers: expect.any(Function),
|
||||
getType: expect.any(Function),
|
||||
getTypes: expect.any(Function),
|
||||
registerFunction: expect.any(Function),
|
||||
registerType: expect.any(Function),
|
||||
registerRenderer: expect.any(Function),
|
||||
execute: expect.any(Function),
|
||||
run: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
@ -54,21 +68,21 @@ describe('ExpressionsService', () => {
|
|||
const service = new ExpressionsService();
|
||||
const fork = service.fork();
|
||||
|
||||
expect(fork.executor.state.get().types).toEqual(service.executor.state.get().types);
|
||||
expect(fork.getTypes()).toEqual(service.getTypes());
|
||||
});
|
||||
|
||||
test('fork keeps all functions of the origin service', () => {
|
||||
const service = new ExpressionsService();
|
||||
const fork = service.fork();
|
||||
|
||||
expect(fork.executor.state.get().functions).toEqual(service.executor.state.get().functions);
|
||||
expect(fork.getFunctions()).toEqual(service.getFunctions());
|
||||
});
|
||||
|
||||
test('fork keeps context of the origin service', () => {
|
||||
const service = new ExpressionsService();
|
||||
const fork = service.fork();
|
||||
|
||||
expect(fork.executor.state.get().context).toEqual(service.executor.state.get().context);
|
||||
expect(fork.executor.state.context).toEqual(service.executor.state.context);
|
||||
});
|
||||
|
||||
test('newly registered functions in origin are also available in fork', () => {
|
||||
|
@ -82,7 +96,7 @@ describe('ExpressionsService', () => {
|
|||
fn: () => {},
|
||||
});
|
||||
|
||||
expect(fork.executor.state.get().functions).toEqual(service.executor.state.get().functions);
|
||||
expect(fork.getFunctions()).toEqual(service.getFunctions());
|
||||
});
|
||||
|
||||
test('newly registered functions in fork are NOT available in origin', () => {
|
||||
|
@ -96,14 +110,15 @@ describe('ExpressionsService', () => {
|
|||
fn: () => {},
|
||||
});
|
||||
|
||||
expect(Object.values(fork.executor.state.get().functions)).toHaveLength(
|
||||
Object.values(service.executor.state.get().functions).length + 1
|
||||
expect(Object.values(fork.getFunctions())).toHaveLength(
|
||||
Object.values(service.getFunctions()).length + 1
|
||||
);
|
||||
});
|
||||
|
||||
test('fork can execute an expression with newly registered function', async () => {
|
||||
const service = new ExpressionsService();
|
||||
const fork = service.fork();
|
||||
fork.start();
|
||||
|
||||
service.registerFunction({
|
||||
name: '__test__',
|
||||
|
@ -118,5 +133,28 @@ describe('ExpressionsService', () => {
|
|||
|
||||
expect(result).toBe('123');
|
||||
});
|
||||
|
||||
test('throw on fork if the service is already started', async () => {
|
||||
const service = new ExpressionsService();
|
||||
service.start();
|
||||
|
||||
expect(() => service.fork()).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('.execute()', () => {
|
||||
test('throw if the service is not started', () => {
|
||||
const expressions = new ExpressionsService();
|
||||
|
||||
expect(() => expressions.execute('foo', null)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('.run()', () => {
|
||||
test('throw if the service is not started', () => {
|
||||
const expressions = new ExpressionsService();
|
||||
|
||||
expect(() => expressions.run('foo', null)).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -41,22 +41,86 @@ import {
|
|||
* The public contract that `ExpressionsService` provides to other plugins
|
||||
* in Kibana Platform in *setup* life-cycle.
|
||||
*/
|
||||
export type ExpressionsServiceSetup = Pick<
|
||||
ExpressionsService,
|
||||
| 'getFunction'
|
||||
| 'getFunctions'
|
||||
| 'getRenderer'
|
||||
| 'getRenderers'
|
||||
| 'getType'
|
||||
| 'getTypes'
|
||||
| 'registerFunction'
|
||||
| 'registerRenderer'
|
||||
| 'registerType'
|
||||
| 'run'
|
||||
| 'fork'
|
||||
| 'extract'
|
||||
| 'inject'
|
||||
>;
|
||||
export interface ExpressionsServiceSetup {
|
||||
/**
|
||||
* Get a registered `ExpressionFunction` by its name, which was registered
|
||||
* using the `registerFunction` method. The returned `ExpressionFunction`
|
||||
* instance is an internal representation of the function in Expressions
|
||||
* service - do not mutate that object.
|
||||
* @deprecated Use start contract instead.
|
||||
*/
|
||||
getFunction(name: string): ReturnType<Executor['getFunction']>;
|
||||
|
||||
/**
|
||||
* Returns POJO map of all registered expression functions, where keys are
|
||||
* names of the functions and values are `ExpressionFunction` instances.
|
||||
* @deprecated Use start contract instead.
|
||||
*/
|
||||
getFunctions(): ReturnType<Executor['getFunctions']>;
|
||||
|
||||
/**
|
||||
* Returns POJO map of all registered expression types, where keys are
|
||||
* names of the types and values are `ExpressionType` instances.
|
||||
* @deprecated Use start contract instead.
|
||||
*/
|
||||
getTypes(): ReturnType<Executor['getTypes']>;
|
||||
|
||||
/**
|
||||
* Create a new instance of `ExpressionsService`. The new instance inherits
|
||||
* all state of the original `ExpressionsService`, including all expression
|
||||
* types, expression functions and context. Also, all new types and functions
|
||||
* registered in the original services AFTER the forking event will be
|
||||
* available in the forked instance. However, all new types and functions
|
||||
* registered in the forked instances will NOT be available to the original
|
||||
* service.
|
||||
* @param name A fork name that can be used to get fork instance later.
|
||||
*/
|
||||
fork(name?: string): ExpressionsService;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
registerFunction(
|
||||
functionDefinition: AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition)
|
||||
): void;
|
||||
|
||||
registerType(
|
||||
typeDefinition: AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition)
|
||||
): void;
|
||||
|
||||
registerRenderer(
|
||||
definition: AnyExpressionRenderDefinition | (() => AnyExpressionRenderDefinition)
|
||||
): void;
|
||||
}
|
||||
|
||||
export interface ExpressionExecutionParams {
|
||||
searchContext?: SerializableRecord;
|
||||
|
@ -97,7 +161,13 @@ export interface ExpressionsServiceStart {
|
|||
* instance is an internal representation of the function in Expressions
|
||||
* service - do not mutate that object.
|
||||
*/
|
||||
getFunction: (name: string) => ReturnType<Executor['getFunction']>;
|
||||
getFunction(name: string): ReturnType<Executor['getFunction']>;
|
||||
|
||||
/**
|
||||
* Returns POJO map of all registered expression functions, where keys are
|
||||
* names of the functions and values are `ExpressionFunction` instances.
|
||||
*/
|
||||
getFunctions(): ReturnType<Executor['getFunctions']>;
|
||||
|
||||
/**
|
||||
* Get a registered `ExpressionRenderer` by its name, which was registered
|
||||
|
@ -105,7 +175,13 @@ export interface ExpressionsServiceStart {
|
|||
* instance is an internal representation of the renderer in Expressions
|
||||
* service - do not mutate that object.
|
||||
*/
|
||||
getRenderer: (name: string) => ReturnType<ExpressionRendererRegistry['get']>;
|
||||
getRenderer(name: string): ReturnType<ExpressionRendererRegistry['get']>;
|
||||
|
||||
/**
|
||||
* Returns POJO map of all registered expression renderers, where keys are
|
||||
* names of the renderers and values are `ExpressionRenderer` instances.
|
||||
*/
|
||||
getRenderers(): ReturnType<ExpressionRendererRegistry['toJS']>;
|
||||
|
||||
/**
|
||||
* Get a registered `ExpressionType` by its name, which was registered
|
||||
|
@ -113,7 +189,13 @@ export interface ExpressionsServiceStart {
|
|||
* instance is an internal representation of the type in Expressions
|
||||
* service - do not mutate that object.
|
||||
*/
|
||||
getType: (name: string) => ReturnType<Executor['getType']>;
|
||||
getType(name: string): ReturnType<Executor['getType']>;
|
||||
|
||||
/**
|
||||
* Returns POJO map of all registered expression types, where keys are
|
||||
* names of the types and values are `ExpressionType` instances.
|
||||
*/
|
||||
getTypes(): ReturnType<Executor['getTypes']>;
|
||||
|
||||
/**
|
||||
* Executes expression string or a parsed expression AST and immediately
|
||||
|
@ -139,34 +221,23 @@ export interface ExpressionsServiceStart {
|
|||
* expressions.run('...', null, { elasticsearchClient });
|
||||
* ```
|
||||
*/
|
||||
run: <Input, Output>(
|
||||
run<Input, Output>(
|
||||
ast: string | ExpressionAstExpression,
|
||||
input: Input,
|
||||
params?: ExpressionExecutionParams
|
||||
) => Observable<ExecutionResult<Output | ExpressionValueError>>;
|
||||
): Observable<ExecutionResult<Output | ExpressionValueError>>;
|
||||
|
||||
/**
|
||||
* Starts expression execution and immediately returns `ExecutionContract`
|
||||
* instance that tracks the progress of the execution and can be used to
|
||||
* interact with the execution.
|
||||
*/
|
||||
execute: <Input = unknown, Output = unknown>(
|
||||
execute<Input = unknown, Output = unknown>(
|
||||
ast: string | ExpressionAstExpression,
|
||||
// This any is for legacy reasons.
|
||||
input: Input,
|
||||
params?: ExpressionExecutionParams
|
||||
) => ExecutionContract<Input, Output>;
|
||||
|
||||
/**
|
||||
* Create a new instance of `ExpressionsService`. The new instance inherits
|
||||
* all state of the original `ExpressionsService`, including all expression
|
||||
* types, expression functions and context. Also, all new types and functions
|
||||
* registered in the original services AFTER the forking event will be
|
||||
* available in the forked instance. However, all new types and functions
|
||||
* registered in the forked instances will NOT be available to the original
|
||||
* service.
|
||||
*/
|
||||
fork: () => ExpressionsService;
|
||||
): ExecutionContract<Input, Output>;
|
||||
}
|
||||
|
||||
export interface ExpressionServiceParams {
|
||||
|
@ -193,7 +264,19 @@ export interface ExpressionServiceParams {
|
|||
*
|
||||
* so that JSDoc appears in developers IDE when they use those `plugins.expressions.registerFunction(`.
|
||||
*/
|
||||
export class ExpressionsService implements PersistableStateService<ExpressionAstExpression> {
|
||||
export class ExpressionsService
|
||||
implements
|
||||
PersistableStateService<ExpressionAstExpression>,
|
||||
ExpressionsServiceSetup,
|
||||
ExpressionsServiceStart
|
||||
{
|
||||
/**
|
||||
* @note Workaround since the expressions service is frozen.
|
||||
*/
|
||||
private static started = new WeakSet<ExpressionsService>();
|
||||
private children = new Map<string, ExpressionsService>();
|
||||
private parent?: ExpressionsService;
|
||||
|
||||
public readonly executor: Executor;
|
||||
public readonly renderers: ExpressionRendererRegistry;
|
||||
|
||||
|
@ -205,96 +288,87 @@ export class ExpressionsService implements PersistableStateService<ExpressionAst
|
|||
this.renderers = renderers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = (
|
||||
functionDefinition: AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition)
|
||||
): void => this.executor.registerFunction(functionDefinition);
|
||||
private isStarted(): boolean {
|
||||
return !!(ExpressionsService.started.has(this) || this.parent?.isStarted());
|
||||
}
|
||||
|
||||
public readonly registerType = (
|
||||
typeDefinition: AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition)
|
||||
): void => this.executor.registerType(typeDefinition);
|
||||
private assertSetup() {
|
||||
if (this.isStarted()) {
|
||||
throw new Error('The expression service is already started and can no longer be configured.');
|
||||
}
|
||||
}
|
||||
|
||||
public readonly registerRenderer = (
|
||||
definition: AnyExpressionRenderDefinition | (() => AnyExpressionRenderDefinition)
|
||||
): void => this.renderers.register(definition);
|
||||
|
||||
public readonly run: ExpressionsServiceStart['run'] = (ast, input, params) =>
|
||||
this.executor.run(ast, input, params);
|
||||
private assertStart() {
|
||||
if (!this.isStarted()) {
|
||||
throw new Error('The expressions service has not started yet.');
|
||||
}
|
||||
}
|
||||
|
||||
public readonly getFunction: ExpressionsServiceStart['getFunction'] = (name) =>
|
||||
this.executor.getFunction(name);
|
||||
|
||||
/**
|
||||
* Returns POJO map of all registered expression functions, where keys are
|
||||
* names of the functions and values are `ExpressionFunction` instances.
|
||||
*/
|
||||
public readonly getFunctions = (): ReturnType<Executor['getFunctions']> =>
|
||||
public readonly getFunctions: ExpressionsServiceStart['getFunctions'] = () =>
|
||||
this.executor.getFunctions();
|
||||
|
||||
public readonly getRenderer: ExpressionsServiceStart['getRenderer'] = (name) =>
|
||||
this.renderers.get(name);
|
||||
public readonly getRenderer: ExpressionsServiceStart['getRenderer'] = (name) => {
|
||||
this.assertStart();
|
||||
|
||||
/**
|
||||
* Returns POJO map of all registered expression renderers, where keys are
|
||||
* names of the renderers and values are `ExpressionRenderer` instances.
|
||||
*/
|
||||
public readonly getRenderers = (): ReturnType<ExpressionRendererRegistry['toJS']> =>
|
||||
this.renderers.toJS();
|
||||
return this.renderers.get(name);
|
||||
};
|
||||
|
||||
public readonly getType: ExpressionsServiceStart['getType'] = (name) =>
|
||||
this.executor.getType(name);
|
||||
public readonly getRenderers: ExpressionsServiceStart['getRenderers'] = () => {
|
||||
this.assertStart();
|
||||
|
||||
/**
|
||||
* Returns POJO map of all registered expression types, where keys are
|
||||
* names of the types and values are `ExpressionType` instances.
|
||||
*/
|
||||
public readonly getTypes = (): ReturnType<Executor['getTypes']> => this.executor.getTypes();
|
||||
return this.renderers.toJS();
|
||||
};
|
||||
|
||||
public readonly execute: ExpressionsServiceStart['execute'] = ((ast, input, params) => {
|
||||
const execution = this.executor.createExecution(ast, params);
|
||||
execution.start(input);
|
||||
return execution.contract;
|
||||
}) as ExpressionsServiceStart['execute'];
|
||||
public readonly getType: ExpressionsServiceStart['getType'] = (name) => {
|
||||
this.assertStart();
|
||||
|
||||
return this.executor.getType(name);
|
||||
};
|
||||
|
||||
public readonly getTypes: ExpressionsServiceStart['getTypes'] = () => this.executor.getTypes();
|
||||
|
||||
public readonly registerFunction: ExpressionsServiceSetup['registerFunction'] = (
|
||||
functionDefinition
|
||||
) => this.executor.registerFunction(functionDefinition);
|
||||
|
||||
public readonly registerType: ExpressionsServiceSetup['registerType'] = (typeDefinition) =>
|
||||
this.executor.registerType(typeDefinition);
|
||||
|
||||
public readonly registerRenderer: ExpressionsServiceSetup['registerRenderer'] = (definition) =>
|
||||
this.renderers.register(definition);
|
||||
|
||||
public readonly fork: ExpressionsServiceSetup['fork'] = (name) => {
|
||||
this.assertSetup();
|
||||
|
||||
public readonly fork = () => {
|
||||
const executor = this.executor.fork();
|
||||
const renderers = this.renderers;
|
||||
const fork = new (this.constructor as typeof ExpressionsService)({ executor, renderers });
|
||||
fork.parent = this;
|
||||
|
||||
if (name) {
|
||||
this.children.set(name, fork);
|
||||
}
|
||||
|
||||
return fork;
|
||||
};
|
||||
|
||||
public readonly execute: ExpressionsServiceStart['execute'] = ((ast, input, params) => {
|
||||
this.assertStart();
|
||||
const execution = this.executor.createExecution(ast, params);
|
||||
execution.start(input);
|
||||
|
||||
return execution.contract;
|
||||
}) as ExpressionsServiceStart['execute'];
|
||||
|
||||
public readonly run: ExpressionsServiceStart['run'] = (ast, input, params) => {
|
||||
this.assertStart();
|
||||
|
||||
return this.executor.run(ast, input, params);
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts telemetry from expression AST
|
||||
* @param state expression AST to extract references from
|
||||
|
@ -371,8 +445,12 @@ export class ExpressionsService implements PersistableStateService<ExpressionAst
|
|||
* same contract on server-side and browser-side.
|
||||
*/
|
||||
public start(...args: unknown[]): ExpressionsServiceStart {
|
||||
ExpressionsService.started.add(this);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
public stop() {
|
||||
ExpressionsService.started.delete(this);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,6 +52,8 @@ jest.mock('./services', () => {
|
|||
service.registerFunction(func);
|
||||
}
|
||||
|
||||
service.start();
|
||||
|
||||
const moduleMock = {
|
||||
__execution: undefined,
|
||||
__getLastExecution: () => moduleMock.__execution,
|
||||
|
|
|
@ -16,19 +16,13 @@ export type Start = jest.Mocked<ExpressionsStart>;
|
|||
|
||||
const createSetupContract = (): Setup => {
|
||||
const setupContract: Setup = {
|
||||
extract: jest.fn(),
|
||||
fork: jest.fn(),
|
||||
getFunction: jest.fn(),
|
||||
getFunctions: jest.fn(),
|
||||
getRenderer: jest.fn(),
|
||||
getRenderers: jest.fn(),
|
||||
getType: jest.fn(),
|
||||
getTypes: jest.fn(),
|
||||
inject: jest.fn(),
|
||||
registerFunction: jest.fn(),
|
||||
registerRenderer: jest.fn(),
|
||||
registerType: jest.fn(),
|
||||
run: jest.fn(),
|
||||
};
|
||||
return setupContract;
|
||||
};
|
||||
|
@ -38,10 +32,12 @@ const createStartContract = (): Start => {
|
|||
execute: jest.fn(),
|
||||
ExpressionLoader: jest.fn(),
|
||||
ExpressionRenderHandler: jest.fn(),
|
||||
fork: jest.fn(),
|
||||
getFunction: jest.fn(),
|
||||
getFunctions: jest.fn(),
|
||||
getRenderer: jest.fn(),
|
||||
getRenderers: jest.fn(),
|
||||
getType: jest.fn(),
|
||||
getTypes: jest.fn(),
|
||||
loader: jest.fn(),
|
||||
ReactExpressionRenderer: jest.fn((props) => <></>),
|
||||
render: jest.fn(),
|
||||
|
|
|
@ -32,16 +32,6 @@ describe('ExpressionsPublicPlugin', () => {
|
|||
expect(setup.getFunctions().add.name).toBe('add');
|
||||
});
|
||||
});
|
||||
|
||||
describe('.run()', () => {
|
||||
test('can execute simple expression', async () => {
|
||||
const { setup } = await expressionsPluginMock.createPlugin();
|
||||
const { result } = await setup
|
||||
.run('var_set name="foo" value="bar" | var name="foo"', null)
|
||||
.toPromise();
|
||||
expect(result).toBe('bar');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('start contract', () => {
|
||||
|
|
|
@ -13,37 +13,24 @@ import { coreMock } from '../../../core/server/mocks';
|
|||
export type Setup = jest.Mocked<ExpressionsServerSetup>;
|
||||
export type Start = jest.Mocked<ExpressionsServerStart>;
|
||||
|
||||
const createSetupContract = (): Setup => {
|
||||
const setupContract: Setup = {
|
||||
extract: jest.fn(),
|
||||
fork: jest.fn(),
|
||||
getFunction: jest.fn(),
|
||||
getFunctions: jest.fn(),
|
||||
getRenderer: jest.fn(),
|
||||
getRenderers: jest.fn(),
|
||||
getType: jest.fn(),
|
||||
getTypes: jest.fn(),
|
||||
inject: jest.fn(),
|
||||
registerFunction: jest.fn(),
|
||||
registerRenderer: jest.fn(),
|
||||
registerType: jest.fn(),
|
||||
run: jest.fn(),
|
||||
};
|
||||
return setupContract;
|
||||
};
|
||||
const createSetupContract = (): Setup => ({
|
||||
fork: jest.fn(),
|
||||
getFunction: jest.fn(),
|
||||
getFunctions: jest.fn(),
|
||||
getTypes: jest.fn(),
|
||||
registerFunction: jest.fn(),
|
||||
registerRenderer: jest.fn(),
|
||||
registerType: jest.fn(),
|
||||
});
|
||||
|
||||
const createStartContract = (): Start => {
|
||||
const startContract: Start = {
|
||||
const createStartContract = (): Start =>
|
||||
({
|
||||
execute: jest.fn(),
|
||||
fork: jest.fn(),
|
||||
getFunction: jest.fn(),
|
||||
getRenderer: jest.fn(),
|
||||
getType: jest.fn(),
|
||||
run: jest.fn(),
|
||||
};
|
||||
|
||||
return startContract;
|
||||
};
|
||||
} as unknown as Start);
|
||||
|
||||
const createPlugin = async () => {
|
||||
const pluginInitializerContext = coreMock.createPluginInitializerContext();
|
||||
|
|
|
@ -24,15 +24,5 @@ describe('ExpressionsServerPlugin', () => {
|
|||
expect(setup.getFunctions().add.name).toBe('add');
|
||||
});
|
||||
});
|
||||
|
||||
describe('.run()', () => {
|
||||
test('can execute simple expression', async () => {
|
||||
const { setup } = await expressionsPluginMock.createPlugin();
|
||||
const { result } = await setup
|
||||
.run('var_set name="foo" value="bar" | var name="foo"', null)
|
||||
.toPromise();
|
||||
expect(result).toBe('bar');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -98,11 +98,10 @@ export const initializeCanvas = async (
|
|||
setupPlugins: CanvasSetupDeps,
|
||||
startPlugins: CanvasStartDeps,
|
||||
registries: SetupRegistries,
|
||||
appUpdater: BehaviorSubject<AppUpdater>,
|
||||
pluginServices: PluginServices<CanvasPluginServices>
|
||||
appUpdater: BehaviorSubject<AppUpdater>
|
||||
) => {
|
||||
await startLegacyServices(coreSetup, coreStart, setupPlugins, startPlugins, appUpdater);
|
||||
const { expressions } = pluginServices.getServices();
|
||||
const { expressions } = setupPlugins;
|
||||
|
||||
// Adding these functions here instead of in plugin.ts.
|
||||
// Some of these functions have deep dependencies into Canvas, which was bulking up the size
|
||||
|
|
|
@ -132,8 +132,7 @@ export class CanvasPlugin
|
|||
setupPlugins,
|
||||
startPlugins,
|
||||
registries,
|
||||
this.appUpdater,
|
||||
pluginServices
|
||||
this.appUpdater
|
||||
);
|
||||
|
||||
const unmount = renderApp({ coreStart, startPlugins, params, canvasStore, pluginServices });
|
||||
|
|
|
@ -5,6 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ExpressionsService } from '../../../../../src/plugins/expressions/public';
|
||||
import { ExpressionsServiceStart } from '../../../../../src/plugins/expressions/public';
|
||||
|
||||
export type CanvasExpressionsService = ExpressionsService;
|
||||
export type CanvasExpressionsService = ExpressionsServiceStart;
|
||||
|
|
|
@ -16,4 +16,4 @@ export type CanvasExpressionsServiceFactory = KibanaPluginServiceFactory<
|
|||
>;
|
||||
|
||||
export const expressionsServiceFactory: CanvasExpressionsServiceFactory = ({ startPlugins }) =>
|
||||
startPlugins.expressions.fork();
|
||||
startPlugins.expressions;
|
||||
|
|
|
@ -6,14 +6,15 @@
|
|||
*/
|
||||
|
||||
import { fromExpression, toExpression } from '@kbn/interpreter/common';
|
||||
import { PersistableStateService } from '../../../../../src/plugins/kibana_utils/common';
|
||||
import { SavedObjectReference } from '../../../../../src/core/server';
|
||||
import { WorkpadAttributes } from '../routes/workpad/workpad_attributes';
|
||||
|
||||
import { ExpressionsServerSetup } from '../../../../../src/plugins/expressions/server';
|
||||
import type { ExpressionAstExpression } from '../../../../../src/plugins/expressions';
|
||||
|
||||
export const extractReferences = (
|
||||
workpad: WorkpadAttributes,
|
||||
expressions: ExpressionsServerSetup
|
||||
expressions: PersistableStateService<ExpressionAstExpression>
|
||||
): { workpad: WorkpadAttributes; references: SavedObjectReference[] } => {
|
||||
// We need to find every element in the workpad and extract references
|
||||
const references: SavedObjectReference[] = [];
|
||||
|
@ -42,7 +43,7 @@ export const extractReferences = (
|
|||
export const injectReferences = (
|
||||
workpad: WorkpadAttributes,
|
||||
references: SavedObjectReference[],
|
||||
expressions: ExpressionsServerSetup
|
||||
expressions: PersistableStateService<ExpressionAstExpression>
|
||||
) => {
|
||||
const pages = workpad.pages.map((page) => {
|
||||
const elements = page.elements.map((element) => {
|
||||
|
|
Loading…
Reference in a new issue