[Expressions] Remove the any type usages (#113477)

* Update ESLint config to disallow usage of the any type
* Remove the any type usages from the expressions plugin
* Update plugins using expressions according to the updated public API
This commit is contained in:
Michael Dokolin 2021-10-04 18:30:10 +02:00 committed by GitHub
parent fed0dc6563
commit 0d9825d03c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
66 changed files with 415 additions and 296 deletions

View file

@ -47,11 +47,11 @@ export function ActionsExpressionsExample({ expressions, actions }: Props) {
};
const handleEvents = (event: any) => {
if (event.id !== 'NAVIGATE') return;
if (event.name !== 'NAVIGATE') return;
// enrich event context with some extra data
event.baseUrl = 'http://www.google.com';
actions.executeTriggerActions(NAVIGATE_TRIGGER_ID, event.value);
actions.executeTriggerActions(NAVIGATE_TRIGGER_ID, event.data);
};
return (

View file

@ -50,7 +50,7 @@ export function ActionsExpressionsExample2({ expressions, actions }: Props) {
};
const handleEvents = (event: any) => {
updateVariables({ color: event.value.href === 'http://www.google.com' ? 'red' : 'blue' });
updateVariables({ color: event.data.href === 'http://www.google.com' ? 'red' : 'blue' });
};
return (

View file

@ -18,8 +18,8 @@ export const buttonRenderer: ExpressionRenderDefinition<any> = {
render(domNode, config, handlers) {
const buttonClick = () => {
handlers.event({
id: 'NAVIGATE',
value: {
name: 'NAVIGATE',
data: {
href: config.href,
},
});

View file

@ -12,6 +12,8 @@ import { functionWrapper } from '../../../../expressions/common/expression_funct
import { ExpressionValueVisDimension } from '../../../../visualizations/public';
import { Datatable } from '../../../../expressions/common/expression_types/specs';
type Arguments = Parameters<ReturnType<typeof tagcloudFunction>['fn']>[1];
describe('interpreter/functions#tagcloud', () => {
const fn = functionWrapper(tagcloudFunction());
const column1 = 'Count';
@ -26,7 +28,7 @@ describe('interpreter/functions#tagcloud', () => {
{ [column1]: 0, [column2]: 'US' },
{ [column1]: 10, [column2]: 'UK' },
],
};
} as unknown as Datatable;
const visConfig = {
scale: 'linear',
orientation: 'single',
@ -73,12 +75,12 @@ describe('interpreter/functions#tagcloud', () => {
};
it('returns an object with the correct structure for number accessors', () => {
const actual = fn(context, { ...visConfig, ...numberAccessors }, undefined);
const actual = fn(context, { ...visConfig, ...numberAccessors } as Arguments, undefined);
expect(actual).toMatchSnapshot();
});
it('returns an object with the correct structure for string accessors', () => {
const actual = fn(context, { ...visConfig, ...stringAccessors }, undefined);
const actual = fn(context, { ...visConfig, ...stringAccessors } as Arguments, undefined);
expect(actual).toMatchSnapshot();
});
@ -93,7 +95,7 @@ describe('interpreter/functions#tagcloud', () => {
},
},
};
await fn(context, { ...visConfig, ...numberAccessors }, handlers as any);
await fn(context, { ...visConfig, ...numberAccessors } as Arguments, handlers as any);
expect(loggedTable!).toMatchSnapshot();
});

View file

@ -1,5 +1,6 @@
{
"rules": {
"@typescript-eslint/consistent-type-definitions": 0
"@typescript-eslint/consistent-type-definitions": 0,
"@typescript-eslint/no-explicit-any": ["error", { "ignoreRestArgs": true }]
}
}

View file

@ -32,13 +32,13 @@ import { parse } from './parse';
* @param val Value you want to check.
* @return boolean
*/
export function isExpressionAstBuilder(val: any): val is ExpressionAstExpressionBuilder {
return val?.type === 'expression_builder';
export function isExpressionAstBuilder(val: unknown): val is ExpressionAstExpressionBuilder {
return (val as Record<string, unknown> | undefined)?.type === 'expression_builder';
}
/** @internal */
export function isExpressionAst(val: any): val is ExpressionAstExpression {
return val?.type === 'expression';
export function isExpressionAst(val: unknown): val is ExpressionAstExpression {
return (val as Record<string, unknown> | undefined)?.type === 'expression';
}
export interface ExpressionAstExpressionBuilder {

View file

@ -64,7 +64,7 @@ export type ExpressionAstFunctionDebug = {
/**
* Raw error that was thrown by the function, if any.
*/
rawError?: any | Error;
rawError?: any | Error; // eslint-disable-line @typescript-eslint/no-explicit-any
/**
* Time in milliseconds it took to execute the function. Duration can be

View file

@ -90,7 +90,7 @@ describe('Execution abortion tests', () => {
const completed = jest.fn();
const aborted = jest.fn();
const defer: ExpressionFunctionDefinition<'defer', any, { time: number }, any> = {
const defer: ExpressionFunctionDefinition<'defer', unknown, { time: number }, unknown> = {
name: 'defer',
args: {
time: {

View file

@ -17,7 +17,7 @@ import { ExecutionContract } from './execution_contract';
beforeAll(() => {
if (typeof performance === 'undefined') {
(global as any).performance = { now: Date.now };
global.performance = { now: Date.now } as typeof performance;
}
});
@ -41,7 +41,7 @@ const createExecution = (
const run = async (
expression: string = 'foo bar=123',
context?: Record<string, unknown>,
input: any = null
input: unknown = null
) => {
const execution = createExecution(expression, context);
execution.start(input);
@ -262,45 +262,45 @@ describe('Execution', () => {
describe('execution context', () => {
test('context.variables is an object', async () => {
const { result } = (await run('introspectContext key="variables"')) as any;
const { result } = await run('introspectContext key="variables"');
expect(result).toHaveProperty('result', expect.any(Object));
});
test('context.types is an object', async () => {
const { result } = (await run('introspectContext key="types"')) as any;
const { result } = await run('introspectContext key="types"');
expect(result).toHaveProperty('result', expect.any(Object));
});
test('context.abortSignal is an object', async () => {
const { result } = (await run('introspectContext key="abortSignal"')) as any;
const { result } = await run('introspectContext key="abortSignal"');
expect(result).toHaveProperty('result', expect.any(Object));
});
test('context.inspectorAdapters is an object', async () => {
const { result } = (await run('introspectContext key="inspectorAdapters"')) as any;
const { result } = await run('introspectContext key="inspectorAdapters"');
expect(result).toHaveProperty('result', expect.any(Object));
});
test('context.getKibanaRequest is a function if provided', async () => {
const { result } = (await run('introspectContext key="getKibanaRequest"', {
const { result } = await run('introspectContext key="getKibanaRequest"', {
kibanaRequest: {},
})) as any;
});
expect(result).toHaveProperty('result', expect.any(Function));
});
test('context.getKibanaRequest is undefined if not provided', async () => {
const { result } = (await run('introspectContext key="getKibanaRequest"')) as any;
const { result } = await run('introspectContext key="getKibanaRequest"');
expect(result).toHaveProperty('result', undefined);
});
test('unknown context key is undefined', async () => {
const { result } = (await run('introspectContext key="foo"')) as any;
const { result } = await run('introspectContext key="foo"');
expect(result).toHaveProperty('result', undefined);
});
@ -314,7 +314,7 @@ describe('Execution', () => {
describe('inspector adapters', () => {
test('by default, "tables" and "requests" inspector adapters are available', async () => {
const { result } = (await run('introspectContext key="inspectorAdapters"')) as any;
const { result } = await run('introspectContext key="inspectorAdapters"');
expect(result).toHaveProperty(
'result',
expect.objectContaining({
@ -326,9 +326,9 @@ describe('Execution', () => {
test('can set custom inspector adapters', async () => {
const inspectorAdapters = {};
const { result } = (await run('introspectContext key="inspectorAdapters"', {
const { result } = await run('introspectContext key="inspectorAdapters"', {
inspectorAdapters,
})) as any;
});
expect(result).toHaveProperty('result', inspectorAdapters);
});
@ -351,7 +351,7 @@ describe('Execution', () => {
describe('expression abortion', () => {
test('context has abortSignal object', async () => {
const { result } = (await run('introspectContext key="abortSignal"')) as any;
const { result } = await run('introspectContext key="abortSignal"');
expect(result).toHaveProperty('result.aborted', false);
});
@ -400,7 +400,7 @@ describe('Execution', () => {
testScheduler.run(({ cold, expectObservable }) => {
const arg = cold(' -a-b-c|', { a: 1, b: 2, c: 3 });
const expected = ' -a-b-c|';
const observable: ExpressionFunctionDefinition<'observable', any, {}, any> = {
const observable: ExpressionFunctionDefinition<'observable', unknown, {}, unknown> = {
name: 'observable',
args: {},
help: '',
@ -468,7 +468,7 @@ describe('Execution', () => {
});
test('does not execute remaining functions in pipeline', async () => {
const spy: ExpressionFunctionDefinition<'spy', any, {}, any> = {
const spy: ExpressionFunctionDefinition<'spy', unknown, {}, unknown> = {
name: 'spy',
args: {},
help: '',
@ -621,7 +621,12 @@ describe('Execution', () => {
help: '',
fn: () => arg2,
};
const max: ExpressionFunctionDefinition<'max', any, { val1: number; val2: number }, any> = {
const max: ExpressionFunctionDefinition<
'max',
unknown,
{ val1: number; val2: number },
unknown
> = {
name: 'max',
args: {
val1: { help: '', types: ['number'] },
@ -679,7 +684,12 @@ describe('Execution', () => {
describe('when arguments are missing', () => {
it('when required argument is missing and has not alias, returns error', async () => {
const requiredArg: ExpressionFunctionDefinition<'requiredArg', any, { arg: any }, any> = {
const requiredArg: ExpressionFunctionDefinition<
'requiredArg',
unknown,
{ arg: unknown },
unknown
> = {
name: 'requiredArg',
args: {
arg: {

View file

@ -8,6 +8,7 @@
import { i18n } from '@kbn/i18n';
import { isPromise } from '@kbn/std';
import { ObservableLike, UnwrapObservable, UnwrapPromiseOrReturn } from '@kbn/utility-types';
import { keys, last, mapValues, reduce, zipObject } from 'lodash';
import {
combineLatest,
@ -44,6 +45,18 @@ import { ExecutionContract } from './execution_contract';
import { ExpressionExecutionParams } from '../service';
import { createDefaultInspectorAdapters } from '../util/create_default_inspector_adapters';
type UnwrapReturnType<Function extends (...args: any[]) => unknown> =
ReturnType<Function> extends ObservableLike<unknown>
? UnwrapObservable<ReturnType<Function>>
: UnwrapPromiseOrReturn<ReturnType<Function>>;
// type ArgumentsOf<Function extends ExpressionFunction> = Function extends ExpressionFunction<
// unknown,
// infer Arguments
// >
// ? Arguments
// : never;
/**
* The result returned after an expression function execution.
*/
@ -83,7 +96,7 @@ const createAbortErrorValue = () =>
});
export interface ExecutionParams {
executor: Executor<any>;
executor: Executor;
ast?: ExpressionAstExpression;
expression?: string;
params: ExpressionExecutionParams;
@ -107,7 +120,7 @@ export class 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;
public input = null as unknown as Input;
/**
* Input of the started execution.
@ -186,13 +199,13 @@ export class Execution<
});
const inspectorAdapters =
execution.params.inspectorAdapters || createDefaultInspectorAdapters();
(execution.params.inspectorAdapters as InspectorAdapters) || createDefaultInspectorAdapters();
this.context = {
getSearchContext: () => this.execution.params.searchContext || {},
getSearchSessionId: () => execution.params.searchSessionId,
getKibanaRequest: execution.params.kibanaRequest
? () => execution.params.kibanaRequest
? () => execution.params.kibanaRequest!
: undefined,
variables: execution.params.variables || {},
types: executor.getTypes(),
@ -201,14 +214,14 @@ export class Execution<
logDatatable: (name: string, datatable: Datatable) => {
inspectorAdapters.tables[name] = datatable;
},
isSyncColorsEnabled: () => execution.params.syncColors,
...(execution.params as any).extraContext,
isSyncColorsEnabled: () => execution.params.syncColors!,
...execution.params.extraContext,
getExecutionContext: () => execution.params.executionContext,
};
this.result = this.input$.pipe(
switchMap((input) =>
this.race(this.invokeChain(this.state.get().ast.chain, input)).pipe(
this.race(this.invokeChain<Output>(this.state.get().ast.chain, input)).pipe(
(source) =>
new Observable<ExecutionResult<Output>>((subscriber) => {
let latest: ExecutionResult<Output> | undefined;
@ -270,8 +283,8 @@ export class 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,
start(
input = null as unknown as Input,
isSubExpression?: boolean
): Observable<ExecutionResult<Output | ExpressionValueError>> {
if (this.hasStarted) throw new Error('Execution already started.');
@ -294,7 +307,10 @@ export class Execution<
return this.result;
}
invokeChain(chainArr: ExpressionAstFunction[], input: unknown): Observable<any> {
invokeChain<ChainOutput = unknown>(
chainArr: ExpressionAstFunction[],
input: unknown
): Observable<ChainOutput> {
return of(input).pipe(
...(chainArr.map((link) =>
switchMap((currentInput) => {
@ -364,19 +380,24 @@ export class Execution<
})
) as Parameters<Observable<unknown>['pipe']>),
catchError((error) => of(error))
);
) as Observable<ChainOutput>;
}
invokeFunction(
fn: ExpressionFunction,
invokeFunction<Fn extends ExpressionFunction>(
fn: Fn,
input: unknown,
args: Record<string, unknown>
): Observable<any> {
): Observable<UnwrapReturnType<Fn['fn']>> {
return of(input).pipe(
map((currentInput) => this.cast(currentInput, fn.inputTypes)),
switchMap((normalizedInput) => this.race(of(fn.fn(normalizedInput, args, this.context)))),
switchMap((fnResult: any) =>
isObservable(fnResult) ? fnResult : from(isPromise(fnResult) ? fnResult : [fnResult])
switchMap(
(fnResult) =>
(isObservable(fnResult)
? fnResult
: from(isPromise(fnResult) ? fnResult : [fnResult])) as Observable<
UnwrapReturnType<Fn['fn']>
>
),
map((output) => {
// Validate that the function returned the type it said it would.
@ -405,39 +426,49 @@ export class Execution<
);
}
public cast(value: any, toTypeNames?: string[]) {
public cast<Type = unknown>(value: unknown, toTypeNames?: string[]): Type {
// If you don't give us anything to cast to, you'll get your input back
if (!toTypeNames || toTypeNames.length === 0) return value;
if (!toTypeNames?.length) {
return value as Type;
}
// No need to cast if node is already one of the valid types
const fromTypeName = getType(value);
if (toTypeNames.includes(fromTypeName)) return value;
if (toTypeNames.includes(fromTypeName)) {
return value as Type;
}
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)) {
if (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);
if (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
resolveArgs(fnDef: ExpressionFunction, input: unknown, argAsts: any): Observable<any> {
resolveArgs<Fn extends ExpressionFunction>(
fnDef: Fn,
input: unknown,
argAsts: Record<string, ExpressionAstArgument[]>
): Observable<Record<string, unknown>> {
return defer(() => {
const { args: argDefs } = fnDef;
// Use the non-alias name from the argument definition
const dealiasedArgAsts = reduce(
argAsts as Record<string, ExpressionAstArgument>,
argAsts,
(acc, argAst, argName) => {
const argDef = getByAlias(argDefs, argName);
if (!argDef) {
@ -452,7 +483,7 @@ export class Execution<
// Check for missing required arguments.
for (const { aliases, default: argDefault, name, required } of Object.values(argDefs)) {
if (!(name in dealiasedArgAsts) && typeof argDefault !== 'undefined') {
dealiasedArgAsts[name] = [parse(argDefault, 'argument')];
dealiasedArgAsts[name] = [parse(argDefault as string, 'argument')];
}
if (!required || name in dealiasedArgAsts) {
@ -490,7 +521,7 @@ export class Execution<
const argNames = keys(resolveArgFns);
if (!argNames.length) {
return from([[]]);
return from([{}]);
}
const resolvedArgValuesObservable = combineLatest(
@ -523,7 +554,7 @@ export class Execution<
});
}
public interpret<T>(ast: ExpressionAstNode, input: T): Observable<ExecutionResult<unknown>> {
interpret<T>(ast: ExpressionAstNode, input: T): Observable<ExecutionResult<unknown>> {
switch (getType(ast)) {
case 'expression':
const execution = this.execution.executor.createExecution(

View file

@ -11,7 +11,7 @@ import type { SerializableRecord } from '@kbn/utility-types';
import type { KibanaRequest } from 'src/core/server';
import type { KibanaExecutionContext } from 'src/core/public';
import { ExpressionType } from '../expression_types';
import { Datatable, ExpressionType } from '../expression_types';
import { Adapters, RequestAdapter } from '../../../inspector/common';
import { TablesAdapter } from '../util/tables_adapter';
@ -69,6 +69,11 @@ export interface ExecutionContext<
* Contains the meta-data about the source of the expression.
*/
getExecutionContext: () => KibanaExecutionContext | undefined;
/**
* Logs datatable.
*/
logDatatable?(name: string, datatable: Datatable): void;
}
/**

View file

@ -19,7 +19,7 @@ export interface ExecutorState<Context extends Record<string, unknown> = Record<
context: Context;
}
export const defaultState: ExecutorState<any> = {
export const defaultState: ExecutorState = {
functions: {},
types: {},
context: {},
@ -61,7 +61,7 @@ export type ExecutorContainer<Context extends Record<string, unknown> = Record<s
export const createExecutorContainer = <
Context extends Record<string, unknown> = Record<string, unknown>
>(
state: ExecutorState<Context> = defaultState
state = defaultState as ExecutorState<Context>
): ExecutorContainer<Context> => {
const container = createStateContainer<
ExecutorState<Context>,

View file

@ -8,20 +8,14 @@
import { Executor } from './executor';
import { parseExpression } from '../ast';
import { Execution } from '../execution/execution';
// eslint-disable-next-line
const { __getArgs } = require('../execution/execution');
jest.mock('../execution/execution', () => ({
Execution: jest.fn(),
}));
jest.mock('../execution/execution', () => {
const mockedModule = {
args: undefined,
__getArgs: () => mockedModule.args,
Execution: function ExecutionMock(...args: any) {
mockedModule.args = args;
},
};
return mockedModule;
beforeEach(() => {
jest.clearAllMocks();
});
describe('Executor mocked execution tests', () => {
@ -31,7 +25,9 @@ describe('Executor mocked execution tests', () => {
const executor = new Executor();
executor.createExecution('foo bar="baz"');
expect(__getArgs()[0].expression).toBe('foo bar="baz"');
expect(Execution).toHaveBeenCalledWith(
expect.objectContaining({ expression: 'foo bar="baz"' })
);
});
});
@ -41,7 +37,9 @@ describe('Executor mocked execution tests', () => {
const ast = parseExpression('foo bar="baz"');
executor.createExecution(ast);
expect(__getArgs()[0].expression).toBe(undefined);
expect(Execution).toHaveBeenCalledWith(
expect.not.objectContaining({ expression: expect.anything() })
);
});
});
});

View file

@ -145,7 +145,7 @@ describe('Executor', () => {
executor.extendContext({ foo });
const execution = executor.createExecution('foo bar="baz"');
expect((execution.context as any).foo).toBe(foo);
expect(execution.context).toHaveProperty('foo', foo);
});
});
});
@ -175,10 +175,10 @@ describe('Executor', () => {
migrations: {
'7.10.0': ((state: ExpressionAstFunction, version: string): ExpressionAstFunction => {
return migrateFn(state, version);
}) as any as MigrateFunction,
}) as unknown as MigrateFunction,
'7.10.1': ((state: ExpressionAstFunction, version: string): ExpressionAstFunction => {
return migrateFn(state, version);
}) as any as MigrateFunction,
}) as unknown as MigrateFunction,
},
fn: jest.fn(),
};

View file

@ -40,7 +40,7 @@ export interface ExpressionExecOptions {
}
export class TypesRegistry implements IRegistry<ExpressionType> {
constructor(private readonly executor: Executor<any>) {}
constructor(private readonly executor: Executor) {}
public register(
typeDefinition: AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition)
@ -62,7 +62,7 @@ export class TypesRegistry implements IRegistry<ExpressionType> {
}
export class FunctionsRegistry implements IRegistry<ExpressionFunction> {
constructor(private readonly executor: Executor<any>) {}
constructor(private readonly executor: Executor) {}
public register(
functionDefinition: AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition)
@ -100,12 +100,12 @@ export class Executor<Context extends Record<string, unknown> = Record<string, u
/**
* @deprecated
*/
public readonly functions = new FunctionsRegistry(this);
public readonly functions = new FunctionsRegistry(this as Executor);
/**
* @deprecated
*/
public readonly types = new TypesRegistry(this);
public readonly types = new TypesRegistry(this as Executor);
protected parent?: Executor<Context>;
@ -207,15 +207,15 @@ export class Executor<Context extends Record<string, unknown> = Record<string, u
ast: string | ExpressionAstExpression,
params: ExpressionExecutionParams = {}
): Execution<Input, Output> {
const executionParams: ExecutionParams = {
const executionParams = {
executor: this,
params: {
...params,
// for canvas we are passing this in,
// canvas should be refactored to not pass any extra context in
extraContext: this.context,
} as any,
};
},
} as ExecutionParams;
if (typeof ast === 'string') executionParams.expression = ast;
else executionParams.ast = ast;
@ -273,7 +273,7 @@ export class Executor<Context extends Record<string, unknown> = Record<string, u
return { state: newAst, references: allReferences };
}
public telemetry(ast: ExpressionAstExpression, telemetryData: Record<string, any>) {
public telemetry(ast: ExpressionAstExpression, telemetryData: Record<string, unknown>) {
this.walkAst(cloneDeep(ast), (fn, link) => {
telemetryData = fn.telemetry(link.arguments, telemetryData);
});

View file

@ -24,9 +24,9 @@ export type ArgumentType<T> =
* representation of the type.
*/
// prettier-ignore
type ArrayTypeToArgumentString<T> =
T extends Array<infer ElementType> ? TypeString<ElementType> :
T extends null ? 'null' :
type ArrayTypeToArgumentString<T> =
T extends Array<infer ElementType> ? TypeString<ElementType> :
T extends null ? 'null' :
never;
/**
@ -34,9 +34,9 @@ type ArrayTypeToArgumentString<T> =
* string-based representation of the return type.
*/
// prettier-ignore
type UnresolvedTypeToArgumentString<T> =
T extends (...args: any) => infer ElementType ? TypeString<ElementType> :
T extends null ? 'null' :
type UnresolvedTypeToArgumentString<T> =
T extends (...args: any[]) => infer ElementType ? TypeString<ElementType> :
T extends null ? 'null' :
never;
/**
@ -44,10 +44,10 @@ type UnresolvedTypeToArgumentString<T> =
* string-based representation of the return type.
*/
// prettier-ignore
type UnresolvedArrayTypeToArgumentString<T> =
T extends Array<(...args: any) => infer ElementType> ? TypeString<ElementType> :
T extends (...args: any) => infer ElementType ? ArrayTypeToArgumentString<ElementType> :
T extends null ? 'null' :
type UnresolvedArrayTypeToArgumentString<T> =
T extends Array<(...args: any[]) => infer ElementType> ? TypeString<ElementType> :
T extends (...args: any[]) => infer ElementType ? ArrayTypeToArgumentString<ElementType> :
T extends null ? 'null' :
never;
/** A type containing properties common to all Function Arguments. */

View file

@ -36,7 +36,11 @@ export class ExpressionFunction implements PersistableState<ExpressionAstFunctio
/**
* Function to run function (context, args)
*/
fn: (input: ExpressionValue, params: Record<string, any>, handlers: object) => ExpressionValue;
fn: (
input: ExpressionValue,
params: Record<string, unknown>,
handlers: object
) => ExpressionValue;
/**
* A short help text.
@ -56,8 +60,8 @@ export class ExpressionFunction implements PersistableState<ExpressionAstFunctio
disabled: boolean;
telemetry: (
state: ExpressionAstFunction['arguments'],
telemetryData: Record<string, any>
) => Record<string, any>;
telemetryData: Record<string, unknown>
) => Record<string, unknown>;
extract: (state: ExpressionAstFunction['arguments']) => {
state: ExpressionAstFunction['arguments'];
references: SavedObjectReference[];
@ -100,13 +104,12 @@ export class ExpressionFunction implements PersistableState<ExpressionAstFunctio
this.migrations = migrations || {};
for (const [key, arg] of Object.entries(args || {})) {
this.args[key] = new ExpressionFunctionParameter(key, arg);
this.args[key as keyof typeof args] = 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;
return this.inputTypes?.includes(type) ?? true;
};
}

View file

@ -6,20 +6,21 @@
* Side Public License, v 1.
*/
import { KnownTypeToString } from '../types';
import { ArgumentType } from './arguments';
export class ExpressionFunctionParameter {
export class ExpressionFunctionParameter<T = unknown> {
name: string;
required: boolean;
help: string;
types: string[];
default: any;
types: ArgumentType<T>['types'];
default?: ArgumentType<T>['default'];
aliases: string[];
multi: boolean;
resolve: boolean;
options: any[];
options: T[];
constructor(name: string, arg: ArgumentType<any>) {
constructor(name: string, arg: ArgumentType<T>) {
const { required, help, types, aliases, multi, resolve, options } = arg;
if (name === '_') {
@ -38,7 +39,6 @@ export class ExpressionFunctionParameter {
}
accepts(type: string) {
if (!this.types.length) return true;
return this.types.indexOf(type) > -1;
return !this.types?.length || this.types.includes(type as KnownTypeToString<T>);
}
}

View file

@ -21,7 +21,7 @@ describe('ExpressionFunctionParameter', () => {
const param = new ExpressionFunctionParameter('foo', {
help: 'bar',
types: ['baz', 'quux'],
});
} as ConstructorParameters<typeof ExpressionFunctionParameter>[1]);
expect(param.accepts('baz')).toBe(true);
expect(param.accepts('quux')).toBe(true);

View file

@ -11,9 +11,9 @@ import { ExpressionFunctionDefinition } from '../types';
import { Datatable, DatatableColumn } from '../../expression_types';
export interface CreateTableArguments {
ids: string[];
names: string[] | null;
rowCount: number;
ids?: string[];
names?: string[] | null;
rowCount?: number;
}
export const createTable: ExpressionFunctionDefinition<

View file

@ -30,7 +30,7 @@ const inlineStyle = (obj: Record<string, string | number>) => {
return styles.join(';');
};
interface Arguments {
export interface FontArguments {
align?: TextAlignment;
color?: string;
family?: FontFamily;
@ -41,7 +41,12 @@ interface Arguments {
weight?: FontWeight;
}
export type ExpressionFunctionFont = ExpressionFunctionDefinition<'font', null, Arguments, Style>;
export type ExpressionFunctionFont = ExpressionFunctionDefinition<
'font',
null,
FontArguments,
Style
>;
export const font: ExpressionFunctionFont = {
name: 'font',

View file

@ -110,7 +110,7 @@ export const mapColumn: ExpressionFunctionDefinition<
map((rows) => {
let type: DatatableColumnType = 'null';
for (const row of rows) {
const rowType = getType(row[id]);
const rowType = getType(row[id]) as DatatableColumnType;
if (rowType !== 'null') {
type = rowType;
break;

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { map, zipObject } from 'lodash';
import { map, zipObject, isString } from 'lodash';
import { i18n } from '@kbn/i18n';
import { evaluate } from '@kbn/tinymath';
import { ExpressionFunctionDefinition } from '../types';
@ -23,19 +23,18 @@ const TINYMATH = '`TinyMath`';
const TINYMATH_URL =
'https://www.elastic.co/guide/en/kibana/current/canvas-tinymath-functions.html';
const isString = (val: any): boolean => typeof val === 'string';
function pivotObjectArray<
RowType extends { [key: string]: any },
ReturnColumns extends string | number | symbol = keyof RowType
>(rows: RowType[], columns?: string[]): Record<string, ReturnColumns[]> {
RowType extends { [key: string]: unknown },
ReturnColumns extends keyof RowType & string
>(rows: RowType[], columns?: ReturnColumns[]) {
const columnNames = columns || Object.keys(rows[0]);
if (!columnNames.every(isString)) {
throw new Error('Columns should be an array of strings');
}
const columnValues = map(columnNames, (name) => map(rows, name));
return zipObject(columnNames, columnValues);
return zipObject(columnNames, columnValues) as { [K in ReturnColumns]: Array<RowType[K]> };
}
export const errors = {

View file

@ -107,7 +107,7 @@ export const mathColumn: ExpressionFunctionDefinition<
let type: DatatableColumnType = 'null';
if (newRows.length) {
for (const row of newRows) {
const rowType = getType(row[args.id]);
const rowType = getType(row[args.id]) as DatatableColumnType;
if (rowType !== 'null') {
type = rowType;
break;

View file

@ -7,7 +7,8 @@
*/
import { openSans } from '../../../fonts';
import { font } from '../font';
import { FontWeight, TextAlignment } from '../../../types';
import { font, FontArguments } from '../font';
import { functionWrapper } from './utils';
describe('font', () => {
@ -22,7 +23,7 @@ describe('font', () => {
size: 14,
underline: false,
weight: 'normal',
};
} as unknown as FontArguments;
describe('default output', () => {
const result = fn(null, args);
@ -63,7 +64,7 @@ describe('font', () => {
describe('family', () => {
it('sets font family', () => {
const result = fn(null, { ...args, family: 'Optima, serif' });
const result = fn(null, { ...args, family: 'Optima, serif' } as unknown as FontArguments);
expect(result.spec.fontFamily).toBe('Optima, serif');
expect(result.css).toContain('font-family:Optima, serif');
});
@ -79,29 +80,29 @@ describe('font', () => {
describe('weight', () => {
it('sets font weight', () => {
let result = fn(null, { ...args, weight: 'normal' });
let result = fn(null, { ...args, weight: FontWeight.NORMAL });
expect(result.spec.fontWeight).toBe('normal');
expect(result.css).toContain('font-weight:normal');
result = fn(null, { ...args, weight: 'bold' });
result = fn(null, { ...args, weight: FontWeight.BOLD });
expect(result.spec.fontWeight).toBe('bold');
expect(result.css).toContain('font-weight:bold');
result = fn(null, { ...args, weight: 'bolder' });
result = fn(null, { ...args, weight: FontWeight.BOLDER });
expect(result.spec.fontWeight).toBe('bolder');
expect(result.css).toContain('font-weight:bolder');
result = fn(null, { ...args, weight: 'lighter' });
result = fn(null, { ...args, weight: FontWeight.LIGHTER });
expect(result.spec.fontWeight).toBe('lighter');
expect(result.css).toContain('font-weight:lighter');
result = fn(null, { ...args, weight: '400' });
result = fn(null, { ...args, weight: FontWeight.FOUR });
expect(result.spec.fontWeight).toBe('400');
expect(result.css).toContain('font-weight:400');
});
it('throws when provided an invalid weight', () => {
expect(() => fn(null, { ...args, weight: 'foo' })).toThrow();
expect(() => fn(null, { ...args, weight: 'foo' as FontWeight })).toThrow();
});
});
@ -131,25 +132,25 @@ describe('font', () => {
describe('align', () => {
it('sets text alignment', () => {
let result = fn(null, { ...args, align: 'left' });
let result = fn(null, { ...args, align: TextAlignment.LEFT });
expect(result.spec.textAlign).toBe('left');
expect(result.css).toContain('text-align:left');
result = fn(null, { ...args, align: 'center' });
result = fn(null, { ...args, align: TextAlignment.CENTER });
expect(result.spec.textAlign).toBe('center');
expect(result.css).toContain('text-align:center');
result = fn(null, { ...args, align: 'right' });
result = fn(null, { ...args, align: TextAlignment.RIGHT });
expect(result.spec.textAlign).toBe('right');
expect(result.css).toContain('text-align:right');
result = fn(null, { ...args, align: 'justify' });
result = fn(null, { ...args, align: TextAlignment.JUSTIFY });
expect(result.spec.textAlign).toBe('justify');
expect(result.css).toContain('text-align:justify');
});
it('throws when provided an invalid alignment', () => {
expect(() => fn(null, { ...args, align: 'foo' })).toThrow();
expect(() => fn(null, { ...args, align: 'foo' as TextAlignment })).toThrow();
});
});
});

View file

@ -6,19 +6,19 @@
* Side Public License, v 1.
*/
import { errors, math } from '../math';
import { errors, math, MathArguments, MathInput } from '../math';
import { emptyTable, functionWrapper, testTable } from './utils';
describe('math', () => {
const fn = functionWrapper<unknown>(math);
const fn = functionWrapper(math);
it('evaluates math expressions without reference to context', () => {
expect(fn(null, { expression: '10.5345' })).toBe(10.5345);
expect(fn(null, { expression: '123 + 456' })).toBe(579);
expect(fn(null, { expression: '100 - 46' })).toBe(54);
expect(fn(null as unknown as MathInput, { expression: '10.5345' })).toBe(10.5345);
expect(fn(null as unknown as MathInput, { expression: '123 + 456' })).toBe(579);
expect(fn(null as unknown as MathInput, { expression: '100 - 46' })).toBe(54);
expect(fn(1, { expression: '100 / 5' })).toBe(20);
expect(fn('foo', { expression: '100 / 5' })).toBe(20);
expect(fn(true, { expression: '100 / 5' })).toBe(20);
expect(fn('foo' as unknown as MathInput, { expression: '100 / 5' })).toBe(20);
expect(fn(true as unknown as MathInput, { expression: '100 / 5' })).toBe(20);
expect(fn(testTable, { expression: '100 * 5' })).toBe(500);
expect(fn(emptyTable, { expression: '100 * 5' })).toBe(500);
});
@ -54,7 +54,7 @@ describe('math', () => {
describe('args', () => {
describe('expression', () => {
it('sets the math expression to be evaluted', () => {
expect(fn(null, { expression: '10' })).toBe(10);
expect(fn(null as unknown as MathInput, { expression: '10' })).toBe(10);
expect(fn(23.23, { expression: 'floor(value)' })).toBe(23);
expect(fn(testTable, { expression: 'count(price)' })).toBe(9);
expect(fn(testTable, { expression: 'count(name)' })).toBe(9);
@ -99,11 +99,11 @@ describe('math', () => {
it('throws when missing expression', () => {
expect(() => fn(testTable)).toThrow(new RegExp(errors.emptyExpression().message));
expect(() => fn(testTable, { expession: '' })).toThrow(
expect(() => fn(testTable, { expession: '' } as unknown as MathArguments)).toThrow(
new RegExp(errors.emptyExpression().message)
);
expect(() => fn(testTable, { expession: ' ' })).toThrow(
expect(() => fn(testTable, { expession: ' ' } as unknown as MathArguments)).toThrow(
new RegExp(errors.emptyExpression().message)
);
});

View file

@ -26,14 +26,14 @@ describe('expression_functions', () => {
};
context = {
getSearchContext: () => ({} as any),
getSearchContext: () => ({}),
getSearchSessionId: () => undefined,
getExecutionContext: () => undefined,
types: {},
variables: { theme: themeProps },
abortSignal: {} as any,
inspectorAdapters: {} as any,
};
abortSignal: {},
inspectorAdapters: {},
} as unknown as typeof context;
});
it('returns the selected variable', () => {

View file

@ -10,13 +10,15 @@ jest.mock('../../../../common');
import { IUiSettingsClient } from 'src/core/public';
import { getUiSettingFn } from '../ui_setting';
import { functionWrapper } from './utils';
describe('uiSetting', () => {
describe('fn', () => {
let getStartDependencies: jest.MockedFunction<
Parameters<typeof getUiSettingFn>[0]['getStartDependencies']
>;
let uiSetting: ReturnType<typeof getUiSettingFn>;
const uiSettingWrapper = () => functionWrapper(getUiSettingFn({ getStartDependencies }));
let uiSetting: ReturnType<typeof uiSettingWrapper>;
let uiSettings: jest.Mocked<IUiSettingsClient>;
beforeEach(() => {
@ -27,13 +29,13 @@ describe('uiSetting', () => {
uiSettings,
})) as unknown as typeof getStartDependencies;
uiSetting = getUiSettingFn({ getStartDependencies });
uiSetting = uiSettingWrapper();
});
it('should return a value', () => {
uiSettings.get.mockReturnValueOnce('value');
expect(uiSetting.fn(null, { parameter: 'something' }, {} as any)).resolves.toEqual({
expect(uiSetting(null, { parameter: 'something' })).resolves.toEqual({
type: 'ui_setting',
key: 'something',
value: 'value',
@ -41,7 +43,7 @@ describe('uiSetting', () => {
});
it('should pass a default value', async () => {
await uiSetting.fn(null, { parameter: 'something', default: 'default' }, {} as any);
await uiSetting(null, { parameter: 'something', default: 'default' });
expect(uiSettings.get).toHaveBeenCalledWith('something', 'default');
});
@ -51,16 +53,16 @@ describe('uiSetting', () => {
throw new Error();
});
expect(uiSetting.fn(null, { parameter: 'something' }, {} as any)).rejects.toEqual(
expect(uiSetting(null, { parameter: 'something' })).rejects.toEqual(
new Error('Invalid parameter "something".')
);
});
it('should get a request instance on the server-side', async () => {
const request = {};
await uiSetting.fn(null, { parameter: 'something' }, {
await uiSetting(null, { parameter: 'something' }, {
getKibanaRequest: () => request,
} as any);
} as Parameters<typeof uiSetting>[2]);
const [[getKibanaRequest]] = getStartDependencies.mock.calls;
@ -68,7 +70,7 @@ describe('uiSetting', () => {
});
it('should throw an error if request is not provided on the server-side', async () => {
await uiSetting.fn(null, { parameter: 'something' }, {} as any);
await uiSetting(null, { parameter: 'something' });
const [[getKibanaRequest]] = getStartDependencies.mock.calls;

View file

@ -15,13 +15,15 @@ import { Datatable } from '../../../expression_types';
* Takes a function spec and passes in default args,
* overriding with any provided args.
*/
export const functionWrapper = <ContextType = object | null>(
spec: AnyExpressionFunctionDefinition
export const functionWrapper = <
ExpressionFunctionDefinition extends AnyExpressionFunctionDefinition
>(
spec: ExpressionFunctionDefinition
) => {
const defaultArgs = mapValues(spec.args, (argSpec) => argSpec.default);
return (
context: ContextType,
args: Record<string, any> = {},
context?: Parameters<ExpressionFunctionDefinition['fn']>[0] | null,
args: Parameters<ExpressionFunctionDefinition['fn']>[1] = {},
handlers: ExecutionContext = {} as ExecutionContext
) => spec.fn(context, { ...defaultArgs, ...args }, handlers);
};

View file

@ -24,9 +24,9 @@ describe('expression_functions', () => {
getExecutionContext: () => undefined,
types: {},
variables: { test: 1 },
abortSignal: {} as any,
inspectorAdapters: {} as any,
};
abortSignal: {},
inspectorAdapters: {},
} as unknown as typeof context;
});
it('returns the selected variable', () => {

View file

@ -17,7 +17,7 @@ describe('expression_functions', () => {
const fn = functionWrapper(variableSet);
let input: Partial<ReturnType<ExecutionContext['getSearchContext']>>;
let context: ExecutionContext;
let variables: Record<string, any>;
let variables: Record<string, unknown>;
beforeEach(() => {
input = { timeRange: { from: '0', to: '1' } };
@ -27,9 +27,9 @@ describe('expression_functions', () => {
getExecutionContext: () => undefined,
types: {},
variables: { test: 1 },
abortSignal: {} as any,
inspectorAdapters: {} as any,
};
abortSignal: {},
inspectorAdapters: {},
} as unknown as typeof context;
variables = context.variables;
});

View file

@ -12,10 +12,10 @@ import { ExpressionFunctionDefinition } from '../types';
interface Arguments {
variable: string;
default: string | number | boolean;
default?: string | number | boolean;
}
type Output = any;
type Output = unknown;
export type ExpressionFunctionTheme = ExpressionFunctionDefinition<
'theme',

View file

@ -36,7 +36,8 @@ export const variable: ExpressionFunctionVar = {
},
},
fn(input, args, context) {
const variables: Record<string, any> = context.variables;
const { variables } = context;
return variables[args.name];
},
};

View file

@ -7,11 +7,12 @@
*/
import { i18n } from '@kbn/i18n';
import type { Serializable } from '@kbn/utility-types';
import { ExpressionFunctionDefinition } from '../types';
interface Arguments {
name: string[];
value: any[];
value: Serializable[];
}
export type ExpressionFunctionVarSet = ExpressionFunctionDefinition<
@ -46,10 +47,11 @@ export const variableSet: ExpressionFunctionVarSet = {
},
},
fn(input, args, context) {
const variables: Record<string, any> = context.variables;
const { variables } = context;
args.name.forEach((name, i) => {
variables[name] = args.value[i] === undefined ? input : args.value[i];
});
return input;
},
};

View file

@ -30,7 +30,7 @@ import { PersistableStateDefinition } from '../../../kibana_utils/common';
export interface ExpressionFunctionDefinition<
Name extends string,
Input,
Arguments extends Record<string, any>,
Arguments extends Record<keyof unknown, unknown>,
Output,
Context extends ExecutionContext = ExecutionContext
> extends PersistableStateDefinition<ExpressionAstFunction['arguments']> {
@ -99,12 +99,14 @@ export interface ExpressionFunctionDefinition<
/**
* Type to capture every possible expression function definition.
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
export type AnyExpressionFunctionDefinition = ExpressionFunctionDefinition<
string,
any,
Record<string, any>,
any
>;
/* eslint-enable @typescript-eslint/no-explicit-any */
/**
* A mapping of `ExpressionFunctionDefinition`s for functions which the

View file

@ -6,6 +6,8 @@
* Side Public License, v 1.
*/
import { ExpressionAstExpression } from '../ast';
export interface ExpressionRenderDefinition<Config = unknown> {
/**
* Technical name of the renderer, used as ID to identify renderer in
@ -46,6 +48,7 @@ export interface ExpressionRenderDefinition<Config = unknown> {
) => void | Promise<void>;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyExpressionRenderDefinition = ExpressionRenderDefinition<any>;
/**
@ -59,24 +62,34 @@ export type AnyExpressionRenderDefinition = ExpressionRenderDefinition<any>;
*/
export type RenderMode = 'edit' | 'preview' | 'view';
export interface IInterpreterRenderUpdateParams<Params = unknown> {
newExpression?: string | ExpressionAstExpression;
newParams: Params;
}
export interface IInterpreterRenderEvent<Context = unknown> {
name: string;
data?: Context;
}
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;
hasCompatibleActions?: (event: any) => Promise<boolean>;
getRenderMode: () => RenderMode;
done(): void;
onDestroy(fn: () => void): void;
reload(): void;
update(params: IInterpreterRenderUpdateParams): void;
event(event: IInterpreterRenderEvent): void;
hasCompatibleActions?(event: IInterpreterRenderEvent): Promise<boolean>;
getRenderMode(): RenderMode;
/**
* The chart is rendered in a non-interactive environment and should not provide any affordances for interaction like brushing.
*/
isInteractive: () => boolean;
isInteractive(): boolean;
isSyncColorsEnabled: () => boolean;
isSyncColorsEnabled(): boolean;
/**
* This uiState interface is actually `PersistedState` from the visualizations plugin,
* but expressions cannot know about vis or it creates a mess of circular dependencies.

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
import type { Serializable } from '@kbn/utility-types';
import { AnyExpressionTypeDefinition, ExpressionValue, ExpressionValueConverter } from './types';
import { getType } from './get_type';
@ -20,15 +21,15 @@ export class ExpressionType {
/**
* Type validation, useful for checking function output.
*/
validate: (type: any) => void | Error;
validate: (type: unknown) => void | Error;
create: unknown;
/**
* Optional serialization (used when passing context around client/server).
*/
serialize?: (value: ExpressionValue) => any;
deserialize?: (serialized: any) => ExpressionValue;
serialize?: (value: Serializable) => unknown;
deserialize?: (serialized: unknown[]) => Serializable;
constructor(private readonly definition: AnyExpressionTypeDefinition) {
const { name, help, deserialize, serialize, validate } = definition;
@ -38,7 +39,7 @@ export class ExpressionType {
this.validate = validate || (() => {});
// Optional
this.create = (definition as any).create;
this.create = (definition as unknown as Record<'create', unknown>).create;
this.serialize = serialize;
this.deserialize = deserialize;

View file

@ -6,14 +6,24 @@
* Side Public License, v 1.
*/
export function getType(node: any) {
if (node == null) return 'null';
export function getType(node: unknown): string {
if (node == null) {
return 'null';
}
if (Array.isArray(node)) {
throw new Error('Unexpected array value encountered.');
}
if (typeof node === 'object') {
if (!node.type) throw new Error('Objects must have a type property');
return node.type;
if (typeof node !== 'object') {
return typeof node;
}
return typeof node;
const { type } = node as Record<string, unknown>;
if (!type) {
throw new Error('Objects must have a type property');
}
return type as string;
}

View file

@ -9,7 +9,7 @@
import type { SerializableRecord } from '@kbn/utility-types';
import { map, pick, zipObject } from 'lodash';
import { ExpressionTypeDefinition } from '../types';
import { ExpressionTypeDefinition, ExpressionValueBoxed } from '../types';
import { PointSeries, PointSeriesColumn } from './pointseries';
import { ExpressionValueRender } from './render';
import { SerializedFieldFormat } from '../../types';
@ -21,7 +21,7 @@ const name = 'datatable';
* @param datatable
*/
export const isDatatable = (datatable: unknown): datatable is Datatable =>
!!datatable && typeof datatable === 'object' && (datatable as any).type === 'datatable';
(datatable as ExpressionValueBoxed | undefined)?.type === 'datatable';
/**
* This type represents the `type` of any `DatatableColumn` in a `Datatable`.
@ -48,6 +48,7 @@ export type DatatableColumnType =
/**
* This type represents a row in a `Datatable`.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type DatatableRow = Record<string, any>;
/**
@ -112,7 +113,7 @@ interface RenderedDatatable {
export const datatable: ExpressionTypeDefinition<typeof name, Datatable, SerializedDatatable> = {
name,
validate: (table) => {
validate: (table: Record<string, unknown>) => {
// TODO: Check columns types. Only string, boolean, number, date, allowed for now.
if (!table.columns) {
throw new Error('datatable must have a columns array, even if it is empty');

View file

@ -22,7 +22,7 @@ export type ExpressionValueError = ExpressionValueBoxed<
}
>;
export const isExpressionValueError = (value: any): value is ExpressionValueError =>
export const isExpressionValueError = (value: unknown): value is ExpressionValueError =>
getType(value) === 'error';
/**

View file

@ -7,7 +7,7 @@
*/
import { ExpressionTypeDefinition, ExpressionValueBoxed } from '../types';
import { Datatable } from './datatable';
import { Datatable, DatatableRow } from './datatable';
import { ExpressionValueRender } from './render';
const name = 'pointseries';
@ -31,7 +31,7 @@ export interface PointSeriesColumn {
*/
export type PointSeriesColumns = Record<PointSeriesColumnName, PointSeriesColumn> | {};
export type PointSeriesRow = Record<string, any>;
export type PointSeriesRow = DatatableRow;
/**
* A `PointSeries` is a unique structure that represents dots on a chart.

View file

@ -11,7 +11,7 @@ import { ExpressionValueRender } from './render';
const name = 'shape';
export const shape: ExpressionTypeDefinition<typeof name, ExpressionValueRender<any>> = {
export const shape: ExpressionTypeDefinition<typeof name, ExpressionValueRender<unknown>> = {
name: 'shape',
to: {
render: (input) => {

View file

@ -6,6 +6,9 @@
* Side Public License, v 1.
*/
import type { ExpressionType } from './expression_type';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ExpressionValueUnboxed = any;
export type ExpressionValueBoxed<Type extends string = string, Value extends object = object> = {
@ -16,7 +19,7 @@ export type ExpressionValue = ExpressionValueUnboxed | ExpressionValueBoxed;
export type ExpressionValueConverter<I extends ExpressionValue, O extends ExpressionValue> = (
input: I,
availableTypes: Record<string, any>
availableTypes: Record<string, ExpressionType>
) => O;
/**
@ -29,18 +32,19 @@ export interface ExpressionTypeDefinition<
SerializedType = undefined
> {
name: Name;
validate?: (type: any) => void | Error;
serialize?: (type: Value) => SerializedType;
deserialize?: (type: SerializedType) => Value;
validate?(type: unknown): void | Error;
serialize?(type: Value): SerializedType;
deserialize?(type: SerializedType): Value;
// TODO: Update typings for the `availableTypes` parameter once interfaces for this
// have been added elsewhere in the interpreter.
from?: {
[type: string]: ExpressionValueConverter<any, Value>;
[type: string]: ExpressionValueConverter<ExpressionValue, Value>;
};
to?: {
[type: string]: ExpressionValueConverter<Value, any>;
[type: string]: ExpressionValueConverter<Value, ExpressionValue>;
};
help?: string;
}
export type AnyExpressionTypeDefinition = ExpressionTypeDefinition<any, any, any>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyExpressionTypeDefinition = ExpressionTypeDefinition<string, any, any>;

View file

@ -11,7 +11,7 @@ import { ExecutionContext } from './execution/types';
export const createMockExecutionContext = <ExtraContext extends object = object>(
extraContext: ExtraContext = {} as ExtraContext
): ExecutionContext & ExtraContext => {
const executionContext: ExecutionContext = {
const executionContext = {
getSearchContext: jest.fn(),
getSearchSessionId: jest.fn(),
getExecutionContext: jest.fn(),
@ -25,10 +25,10 @@ export const createMockExecutionContext = <ExtraContext extends object = object>
removeEventListener: jest.fn(),
},
inspectorAdapters: {
requests: {} as any,
data: {} as any,
requests: {},
data: {},
},
};
} as unknown as ExecutionContext;
return {
...executionContext,

View file

@ -125,7 +125,7 @@ export interface ExpressionsServiceSetup {
export interface ExpressionExecutionParams {
searchContext?: SerializableRecord;
variables?: Record<string, any>;
variables?: Record<string, unknown>;
/**
* Whether to execute expression in *debug mode*. In *debug mode* inputs and
@ -148,6 +148,8 @@ export interface ExpressionExecutionParams {
inspectorAdapters?: Adapters;
executionContext?: KibanaExecutionContext;
extraContext?: object;
}
/**
@ -375,7 +377,7 @@ export class ExpressionsService
*/
public readonly telemetry = (
state: ExpressionAstExpression,
telemetryData: Record<string, any> = {}
telemetryData: Record<string, unknown> = {}
) => {
return this.executor.telemetry(state, telemetryData);
};

View file

@ -8,7 +8,7 @@
import { ExpressionFunctionDefinition } from '../../expression_functions';
export const access: ExpressionFunctionDefinition<'access', any, { key: string }, any> = {
export const access: ExpressionFunctionDefinition<'access', unknown, { key: string }, unknown> = {
name: 'access',
help: 'Access key on input object or return the input, if it is not an object',
args: {
@ -19,6 +19,10 @@ export const access: ExpressionFunctionDefinition<'access', any, { key: string }
},
},
fn: (input, { key }, context) => {
return !input ? input : typeof input === 'object' ? input[key] : input;
return !input
? input
: typeof input === 'object'
? (input as Record<string, unknown>)[key]
: input;
},
};

View file

@ -26,11 +26,11 @@ export const add: ExpressionFunctionDefinition<
types: ['null', 'number', 'string'],
},
},
fn: ({ value: value1 }, { val: input2 }, context) => {
fn: ({ value: value1 }, { val: input2 }) => {
const value2 = !input2
? 0
: typeof input2 === 'object'
? (input2 as any).value
? (input2 as ExpressionValueNum).value
: Number(input2);
return {

View file

@ -10,9 +10,9 @@ import { ExpressionFunctionDefinition } from '../../expression_functions';
export const introspectContext: ExpressionFunctionDefinition<
'introspectContext',
any,
unknown,
{ key: string },
any
unknown
> = {
name: 'introspectContext',
args: {
@ -25,7 +25,7 @@ export const introspectContext: ExpressionFunctionDefinition<
fn: (input, args, context) => {
return {
type: 'any',
result: (context as any)[args.key],
result: context[args.key as keyof typeof context],
};
},
};

View file

@ -8,7 +8,7 @@
import { ExpressionFunctionDefinition } from '../../expression_functions';
export const sleep: ExpressionFunctionDefinition<'sleep', any, { time: number }, any> = {
export const sleep: ExpressionFunctionDefinition<'sleep', unknown, { time: number }, unknown> = {
name: 'sleep',
args: {
time: {

View file

@ -37,7 +37,7 @@ export type KnownTypeToString<T> =
* `someArgument: Promise<boolean | string>` results in `types: ['boolean', 'string']`
*/
export type TypeString<T> = KnownTypeToString<
T extends ObservableLike<any> ? UnwrapObservable<T> : UnwrapPromiseOrReturn<T>
T extends ObservableLike<unknown> ? UnwrapObservable<T> : UnwrapPromiseOrReturn<T>
>;
/**
@ -52,6 +52,7 @@ export type UnmappedTypeStrings = 'date' | 'filter';
* Is used to carry information about how to format data in
* a data table as part of the column definition.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface SerializedFieldFormat<TParams = Record<string, any>> {
id?: string;
params?: TParams;

View file

@ -7,16 +7,17 @@
*/
import { EventEmitter } from 'events';
import { ExpressionAstNode } from '..';
export class ExpressionsInspectorAdapter extends EventEmitter {
private _ast: any = {};
private _ast = {} as ExpressionAstNode;
public logAST(ast: any): void {
logAST(ast: ExpressionAstNode): void {
this._ast = ast;
this.emit('change', this._ast);
}
public get ast() {
public get ast(): ExpressionAstNode {
return this._ast;
}
}

View file

@ -14,7 +14,7 @@ export const createMockContext = () => {
getSearchSessionId: () => undefined,
types: {},
variables: {},
abortSignal: {} as any,
inspectorAdapters: {} as any,
abortSignal: {},
inspectorAdapters: {},
} as ExecutionContext;
};

View file

@ -16,12 +16,14 @@ import {
IInterpreterRenderHandlers,
RenderMode,
AnyExpressionFunctionDefinition,
ExpressionsService,
ExecutionContract,
} from '../common';
// eslint-disable-next-line
const { __getLastExecution, __getLastRenderMode } = require('./services');
const element: HTMLElement = null as any;
const element = null as unknown as HTMLElement;
let testScheduler: TestScheduler;
@ -36,8 +38,9 @@ jest.mock('./services', () => {
},
};
// eslint-disable-next-line
const service = new (require('../common/service/expressions_services').ExpressionsService as any)();
const service: ExpressionsService =
// eslint-disable-next-line @typescript-eslint/no-var-requires
new (require('../common/service/expressions_services').ExpressionsService)();
const testFn: AnyExpressionFunctionDefinition = {
fn: () => ({ type: 'render', as: 'test' }),
@ -54,9 +57,9 @@ jest.mock('./services', () => {
service.start();
let execution: ExecutionContract;
const moduleMock = {
__execution: undefined,
__getLastExecution: () => moduleMock.__execution,
__getLastExecution: () => execution,
__getLastRenderMode: () => renderMode,
getRenderersRegistry: () => ({
get: (id: string) => renderers[id],
@ -72,13 +75,14 @@ jest.mock('./services', () => {
};
const execute = service.execute;
service.execute = (...args: any) => {
const execution = execute(...args);
jest.spyOn(service, 'execute').mockImplementation((...args) => {
execution = execute(...args);
jest.spyOn(execution, 'getData');
jest.spyOn(execution, 'cancel');
moduleMock.__execution = execution;
return execution;
};
});
return moduleMock;
});

View file

@ -9,7 +9,7 @@
import { BehaviorSubject, Observable, Subject, Subscription, asyncScheduler, identity } from 'rxjs';
import { filter, map, delay, throttleTime } from 'rxjs/operators';
import { defaults } from 'lodash';
import { UnwrapObservable } from '@kbn/utility-types';
import { SerializableRecord, UnwrapObservable } from '@kbn/utility-types';
import { Adapters } from '../../inspector/public';
import { IExpressionLoaderParams } from './types';
import { ExpressionAstExpression } from '../common';
@ -18,7 +18,7 @@ import { ExecutionContract } from '../common/execution/execution_contract';
import { ExpressionRenderHandler } from './render';
import { getExpressionsService } from './services';
type Data = any;
type Data = unknown;
export class ExpressionLoader {
data$: ReturnType<ExecutionContract['getData']>;
@ -156,7 +156,7 @@ export class ExpressionLoader {
};
private render(data: Data): void {
this.renderHandler.render(data, this.params.uiState);
this.renderHandler.render(data as SerializableRecord, this.params.uiState);
}
private setParams(params?: IExpressionLoaderParams) {
@ -169,7 +169,7 @@ export class ExpressionLoader {
{},
params.searchContext,
this.params.searchContext || {}
) as any;
);
}
if (params.uiState && this.params) {
this.params.uiState = params.uiState;

View file

@ -14,6 +14,7 @@ import { ReactExpressionRenderer } from './react_expression_renderer';
import { ExpressionLoader } from './loader';
import { mount } from 'enzyme';
import { EuiProgress } from '@elastic/eui';
import { IInterpreterRenderHandlers } from '../common';
import { RenderErrorHandlerFnType } from './types';
import { ExpressionRendererEvent } from './render';
@ -234,7 +235,7 @@ describe('ExpressionRenderer', () => {
done: () => {
renderSubject.next(1);
},
} as any);
} as IInterpreterRenderHandlers);
});
instance.update();

View file

@ -8,6 +8,7 @@
import { ExpressionRenderHandler, render } from './render';
import { Observable } from 'rxjs';
import { SerializableRecord } from '@kbn/utility-types';
import { ExpressionRenderError } from './types';
import { getRenderersRegistry } from './services';
import { first, take, toArray } from 'rxjs/operators';
@ -79,11 +80,11 @@ describe('ExpressionRenderHandler', () => {
it('in case of error render$ should emit when error renderer is finished', async () => {
const expressionRenderHandler = new ExpressionRenderHandler(element);
expressionRenderHandler.render(false);
expressionRenderHandler.render(false as unknown as SerializableRecord);
const promise1 = expressionRenderHandler.render$.pipe(first()).toPromise();
await expect(promise1).resolves.toEqual(1);
expressionRenderHandler.render(false);
expressionRenderHandler.render(false as unknown as SerializableRecord);
const promise2 = expressionRenderHandler.render$.pipe(first()).toPromise();
await expect(promise2).resolves.toEqual(2);
});
@ -92,7 +93,7 @@ describe('ExpressionRenderHandler', () => {
const expressionRenderHandler = new ExpressionRenderHandler(element, {
onRenderError: mockMockErrorRenderFunction,
});
await expressionRenderHandler.render(false);
await expressionRenderHandler.render(false as unknown as SerializableRecord);
expect(getHandledError()!.message).toEqual(
`invalid data provided to the expression renderer`
);
@ -122,7 +123,8 @@ describe('ExpressionRenderHandler', () => {
get: () => ({
render: (domNode: HTMLElement, config: unknown, handlers: IInterpreterRenderHandlers) => {
handlers.hasCompatibleActions!({
foo: 'bar',
name: 'something',
data: 'bar',
});
},
}),
@ -136,7 +138,8 @@ describe('ExpressionRenderHandler', () => {
await expressionRenderHandler.render({ type: 'render', as: 'something' });
expect(hasCompatibleActions).toHaveBeenCalledTimes(1);
expect(hasCompatibleActions.mock.calls[0][0]).toEqual({
foo: 'bar',
name: 'something',
data: 'bar',
});
});
@ -156,7 +159,7 @@ describe('ExpressionRenderHandler', () => {
it('default renderer should use notification service', async () => {
const expressionRenderHandler = new ExpressionRenderHandler(element);
const promise1 = expressionRenderHandler.render$.pipe(first()).toPromise();
expressionRenderHandler.render(false);
expressionRenderHandler.render(false as unknown as SerializableRecord);
await expect(promise1).resolves.toEqual(1);
expect(mockNotificationService.toasts.addError).toBeCalledWith(
expect.objectContaining({
@ -175,7 +178,7 @@ describe('ExpressionRenderHandler', () => {
const expressionRenderHandler1 = new ExpressionRenderHandler(element, {
onRenderError: mockMockErrorRenderFunction,
});
expressionRenderHandler1.render(false);
expressionRenderHandler1.render(false as unknown as SerializableRecord);
const renderPromiseAfterRender = expressionRenderHandler1.render$.pipe(first()).toPromise();
await expect(renderPromiseAfterRender).resolves.toEqual(1);
expect(getHandledError()!.message).toEqual(
@ -188,7 +191,7 @@ describe('ExpressionRenderHandler', () => {
onRenderError: mockMockErrorRenderFunction,
});
const renderPromiseBeforeRender = expressionRenderHandler2.render$.pipe(first()).toPromise();
expressionRenderHandler2.render(false);
expressionRenderHandler2.render(false as unknown as SerializableRecord);
await expect(renderPromiseBeforeRender).resolves.toEqual(1);
expect(getHandledError()!.message).toEqual(
'invalid data provided to the expression renderer'
@ -199,9 +202,9 @@ describe('ExpressionRenderHandler', () => {
// that observables will emit previous result if subscription happens after render
it('should emit previous render and error results', async () => {
const expressionRenderHandler = new ExpressionRenderHandler(element);
expressionRenderHandler.render(false);
expressionRenderHandler.render(false as unknown as SerializableRecord);
const renderPromise = expressionRenderHandler.render$.pipe(take(2), toArray()).toPromise();
expressionRenderHandler.render(false);
expressionRenderHandler.render(false as unknown as SerializableRecord);
await expect(renderPromise).resolves.toEqual([1, 2]);
});
});

View file

@ -9,13 +9,20 @@
import * as Rx from 'rxjs';
import { Observable } from 'rxjs';
import { filter } from 'rxjs/operators';
import { isNumber } from 'lodash';
import { SerializableRecord } from '@kbn/utility-types';
import { ExpressionRenderError, RenderErrorHandlerFnType, IExpressionLoaderParams } from './types';
import { renderErrorHandler as defaultRenderErrorHandler } from './render_error_handler';
import { IInterpreterRenderHandlers, ExpressionAstExpression, RenderMode } from '../common';
import {
IInterpreterRenderHandlers,
IInterpreterRenderEvent,
IInterpreterRenderUpdateParams,
RenderMode,
} from '../common';
import { getRenderersRegistry } from './services';
export type IExpressionRendererExtraHandlers = Record<string, any>;
export type IExpressionRendererExtraHandlers = Record<string, unknown>;
export interface ExpressionRenderHandlerParams {
onRenderError?: RenderErrorHandlerFnType;
@ -25,15 +32,10 @@ export interface ExpressionRenderHandlerParams {
hasCompatibleActions?: (event: ExpressionRendererEvent) => Promise<boolean>;
}
export interface ExpressionRendererEvent {
name: string;
data: any;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ExpressionRendererEvent = IInterpreterRenderEvent<any>;
interface UpdateValue {
newExpression?: string | ExpressionAstExpression;
newParams: IExpressionLoaderParams;
}
type UpdateValue = IInterpreterRenderUpdateParams<IExpressionLoaderParams>;
export class ExpressionRenderHandler {
render$: Observable<number>;
@ -41,7 +43,7 @@ export class ExpressionRenderHandler {
events$: Observable<ExpressionRendererEvent>;
private element: HTMLElement;
private destroyFn?: any;
private destroyFn?: Function;
private renderCount: number = 0;
private renderSubject: Rx.BehaviorSubject<number | null>;
private eventsSubject: Rx.Subject<unknown>;
@ -66,16 +68,14 @@ export class ExpressionRenderHandler {
this.onRenderError = onRenderError || defaultRenderErrorHandler;
this.renderSubject = new Rx.BehaviorSubject(null as any | null);
this.render$ = this.renderSubject
.asObservable()
.pipe(filter((_) => _ !== null)) as Observable<any>;
this.renderSubject = new Rx.BehaviorSubject<number | null>(null);
this.render$ = this.renderSubject.asObservable().pipe(filter(isNumber));
this.updateSubject = new Rx.Subject();
this.update$ = this.updateSubject.asObservable();
this.handlers = {
onDestroy: (fn: any) => {
onDestroy: (fn: Function) => {
this.destroyFn = fn;
},
done: () => {
@ -104,14 +104,14 @@ export class ExpressionRenderHandler {
};
}
render = async (value: any, uiState?: any) => {
render = async (value: SerializableRecord, uiState?: unknown) => {
if (!value || typeof value !== 'object') {
return this.handleRenderError(new Error('invalid data provided to the expression renderer'));
}
if (value.type !== 'render' || !value.as) {
if (value.type === 'error') {
return this.handleRenderError(value.error);
return this.handleRenderError(value.error as unknown as ExpressionRenderError);
} else {
return this.handleRenderError(
new Error('invalid data provided to the expression renderer')
@ -119,20 +119,20 @@ export class ExpressionRenderHandler {
}
}
if (!getRenderersRegistry().get(value.as)) {
if (!getRenderersRegistry().get(value.as as string)) {
return this.handleRenderError(new Error(`invalid renderer id '${value.as}'`));
}
try {
// Rendering is asynchronous, completed by handlers.done()
await getRenderersRegistry()
.get(value.as)!
.get(value.as as string)!
.render(this.element, value.value, {
...this.handlers,
uiState,
} as any);
});
} catch (e) {
return this.handleRenderError(e);
return this.handleRenderError(e as ExpressionRenderError);
}
};
@ -156,10 +156,10 @@ export class ExpressionRenderHandler {
export function render(
element: HTMLElement,
data: any,
data: unknown,
options?: ExpressionRenderHandlerParams
): ExpressionRenderHandler {
const handler = new ExpressionRenderHandler(element, options);
handler.render(data);
handler.render(data as SerializableRecord);
return handler;
}

View file

@ -36,7 +36,7 @@ export interface ExpressionInterpreter {
export interface IExpressionLoaderParams {
searchContext?: SerializableRecord;
context?: ExpressionValue;
variables?: Record<string, any>;
variables?: Record<string, unknown>;
// Enables debug tracking on each expression in the AST
debug?: boolean;
disableCaching?: boolean;

View file

@ -8,6 +8,7 @@
import { functionWrapper } from '../../expressions/common/expression_functions/specs/tests/utils';
import { createMarkdownVisFn } from './markdown_fn';
import { Arguments } from './types';
describe('interpreter/functions#markdown', () => {
const fn = functionWrapper(createMarkdownVisFn());
@ -15,7 +16,7 @@ describe('interpreter/functions#markdown', () => {
font: { spec: { fontSize: 12 } },
openLinksInNewTab: true,
markdown: '## hello _markdown_',
};
} as unknown as Arguments;
it('returns an object with the correct structure', async () => {
const actual = await fn(null, args, undefined);

View file

@ -10,13 +10,15 @@ import { createMetricVisFn } from './metric_vis_fn';
import { functionWrapper } from '../../../expressions/common/expression_functions/specs/tests/utils';
import { Datatable } from '../../../expressions/common/expression_types/specs';
type Arguments = Parameters<ReturnType<typeof createMetricVisFn>['fn']>[1];
describe('interpreter/functions#metric', () => {
const fn = functionWrapper(createMetricVisFn());
const context = {
type: 'datatable',
rows: [{ 'col-0-1': 0 }],
columns: [{ id: 'col-0-1', name: 'Count' }],
};
} as unknown as Datatable;
const args = {
percentageMode: false,
useRanges: false,
@ -50,7 +52,7 @@ describe('interpreter/functions#metric', () => {
aggType: 'count',
},
],
};
} as unknown as Arguments;
it('returns an object with the correct structure', () => {
const actual = fn(context, args, undefined);

View file

@ -8,6 +8,7 @@
import { functionWrapper } from '../../../expressions/common/expression_functions/specs/tests/utils';
import { createPieVisFn } from './pie_fn';
import { PieVisConfig } from './types';
import { Datatable } from '../../../expressions/common/expression_types/specs';
describe('interpreter/functions#pie', () => {
@ -16,7 +17,7 @@ describe('interpreter/functions#pie', () => {
type: 'datatable',
rows: [{ 'col-0-1': 0 }],
columns: [{ id: 'col-0-1', name: 'Count' }],
};
} as unknown as Datatable;
const visConfig = {
addTooltip: true,
addLegend: true,
@ -43,7 +44,7 @@ describe('interpreter/functions#pie', () => {
params: {},
aggType: 'count',
},
};
} as unknown as PieVisConfig;
beforeEach(() => {
jest.clearAllMocks();

View file

@ -8,6 +8,7 @@
import { createTableVisFn } from './table_vis_fn';
import { tableVisResponseHandler } from './utils';
import { TableVisConfig } from './types';
import { functionWrapper } from '../../../expressions/common/expression_functions/specs/tests/utils';
import { Datatable } from '../../../expressions/common/expression_types/specs';
@ -24,7 +25,7 @@ describe('interpreter/functions#table', () => {
type: 'datatable',
rows: [{ 'col-0-1': 0 }],
columns: [{ id: 'col-0-1', name: 'Count' }],
};
} as unknown as Datatable;
const visConfig = {
title: 'My Chart title',
perPage: 10,
@ -52,7 +53,7 @@ describe('interpreter/functions#table', () => {
},
],
buckets: [],
};
} as unknown as TableVisConfig;
beforeEach(() => {
jest.clearAllMocks();

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
import type { Datatable } from 'src/plugins/expressions';
import { functionWrapper } from '../../../expressions/common/expression_functions/specs/tests/utils';
import { createPieVisFn } from './pie_fn';
// @ts-ignore
@ -34,7 +35,7 @@ describe('interpreter/functions#pie', () => {
type: 'datatable',
rows: [{ 'col-0-1': 0 }],
columns: [{ id: 'col-0-1', name: 'Count' }],
};
} as unknown as Datatable;
const visConfig = {
type: 'pie',
addTooltip: true,

View file

@ -5,7 +5,12 @@
* 2.0.
*/
import { Datatable, ExpressionFunctionDefinition, getType } from '../../../types';
import {
Datatable,
DatatableColumnType,
ExpressionFunctionDefinition,
getType,
} from '../../../types';
import { getFunctionHelp } from '../../../i18n';
interface Arguments {
@ -30,14 +35,14 @@ export function asFn(): ExpressionFunctionDefinition<'as', Input, Arguments, Dat
default: 'value',
},
},
fn: (input, args) => {
fn: (input, args): Datatable => {
return {
type: 'datatable',
columns: [
{
id: args.name,
name: args.name,
meta: { type: getType(input) },
meta: { type: getType(input) as DatatableColumnType },
},
],
rows: [

View file

@ -156,7 +156,7 @@ ${examplesBlock}
*Returns:* ${output ? wrapInBackTicks(output) : 'Depends on your input and arguments'}\n\n`;
};
const getArgsTable = (args: { [key: string]: ExpressionFunctionParameter }) => {
const getArgsTable = (args: { [key: string]: ExpressionFunctionParameter<any> }) => {
if (!args || Object.keys(args).length === 0) {
return 'None';
}

View file

@ -7,11 +7,10 @@
import { Datatable, DatatableColumn } from 'src/plugins/expressions/public';
import { functionWrapper } from 'src/plugins/expressions/common/expression_functions/specs/tests/utils';
import { FormatColumnArgs, formatColumn } from './index';
import { formatColumn } from './index';
describe('format_column', () => {
const fn: (input: Datatable, args: FormatColumnArgs) => Promise<Datatable> =
functionWrapper(formatColumn);
const fn = functionWrapper(formatColumn);
let datatable: Datatable;