From 6fed6da7a0686f46d8ec7dca560c23610892112f Mon Sep 17 00:00:00 2001 From: ppisljar Date: Tue, 9 Mar 2021 02:24:13 -0800 Subject: [PATCH 1/9] adding kibanaFilter, rangeFilter, existsFilter and phraseFilter expression functions --- .../common/es_query/filters/build_filters.ts | 6 +- .../search/expressions/exists_filter.test.ts | 45 ++++++++++ .../search/expressions/exists_filter.ts | 65 ++++++++++++++ .../common/search/expressions/field.test.ts | 42 +++++++++ .../data/common/search/expressions/field.ts | 67 ++++++++++++++ .../search/expressions/filters_to_ast.test.ts | 45 ++++++++++ .../search/expressions/filters_to_ast.ts | 21 +++++ .../data/common/search/expressions/index.ts | 6 ++ .../search/expressions/kibana_context.ts | 10 +-- .../search/expressions/kibana_context_type.ts | 3 + .../search/expressions/kibana_filter.test.ts | 44 +++++++++ .../search/expressions/kibana_filter.ts | 61 +++++++++++++ .../search/expressions/phrase_filter.test.ts | 70 +++++++++++++++ .../search/expressions/phrase_filter.ts | 89 +++++++++++++++++++ .../common/search/expressions/range.test.ts | 40 +++++++++ .../data/common/search/expressions/range.ts | 76 ++++++++++++++++ .../search/expressions/range_filter.test.ts | 53 +++++++++++ .../common/search/expressions/range_filter.ts | 74 +++++++++++++++ .../data/public/search/search_service.ts | 10 +++ .../data/server/search/search_service.ts | 10 +++ .../public/embeddable/to_ast.ts | 9 +- 21 files changed, 836 insertions(+), 10 deletions(-) create mode 100644 src/plugins/data/common/search/expressions/exists_filter.test.ts create mode 100644 src/plugins/data/common/search/expressions/exists_filter.ts create mode 100644 src/plugins/data/common/search/expressions/field.test.ts create mode 100644 src/plugins/data/common/search/expressions/field.ts create mode 100644 src/plugins/data/common/search/expressions/filters_to_ast.test.ts create mode 100644 src/plugins/data/common/search/expressions/filters_to_ast.ts create mode 100644 src/plugins/data/common/search/expressions/kibana_filter.test.ts create mode 100644 src/plugins/data/common/search/expressions/kibana_filter.ts create mode 100644 src/plugins/data/common/search/expressions/phrase_filter.test.ts create mode 100644 src/plugins/data/common/search/expressions/phrase_filter.ts create mode 100644 src/plugins/data/common/search/expressions/range.test.ts create mode 100644 src/plugins/data/common/search/expressions/range.ts create mode 100644 src/plugins/data/common/search/expressions/range_filter.test.ts create mode 100644 src/plugins/data/common/search/expressions/range_filter.ts diff --git a/src/plugins/data/common/es_query/filters/build_filters.ts b/src/plugins/data/common/es_query/filters/build_filters.ts index 42a4d66359346..ba1bd0a615493 100644 --- a/src/plugins/data/common/es_query/filters/build_filters.ts +++ b/src/plugins/data/common/es_query/filters/build_filters.ts @@ -26,13 +26,15 @@ export function buildFilter( disabled: boolean, params: any, alias: string | null, - store: FilterStateStore + store?: FilterStateStore ): Filter { const filter = buildBaseFilter(indexPattern, field, type, params); filter.meta.alias = alias; filter.meta.negate = negate; filter.meta.disabled = disabled; - filter.$state = { store }; + if (store) { + filter.$state = { store }; + } return filter; } diff --git a/src/plugins/data/common/search/expressions/exists_filter.test.ts b/src/plugins/data/common/search/expressions/exists_filter.test.ts new file mode 100644 index 0000000000000..5b986e2d0a0a2 --- /dev/null +++ b/src/plugins/data/common/search/expressions/exists_filter.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ExecutionContext } from 'src/plugins/expressions/common'; +import { functionWrapper } from './utils'; +import { existsFilterFunction } from './exists_filter'; + +describe('interpreter/functions#existsFilter', () => { + const fn = functionWrapper(existsFilterFunction); + let context: ExecutionContext; + + beforeEach(() => { + context = { + getSearchContext: () => ({}), + getSearchSessionId: () => undefined, + types: {}, + variables: {}, + abortSignal: {} as any, + inspectorAdapters: {} as any, + }; + }); + + it('returns an object with the correct structure', () => { + const actual = fn(null, { field: { spec: { name: 'test' } } }, context); + expect(actual).toMatchInlineSnapshot(` + Object { + "exists": Object { + "field": "test", + }, + "meta": Object { + "alias": null, + "disabled": false, + "index": undefined, + "negate": false, + }, + "type": "kibana_filter", + } + `); + }); +}); diff --git a/src/plugins/data/common/search/expressions/exists_filter.ts b/src/plugins/data/common/search/expressions/exists_filter.ts new file mode 100644 index 0000000000000..d20d467a20680 --- /dev/null +++ b/src/plugins/data/common/search/expressions/exists_filter.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { KibanaField, KibanaFilter } from './kibana_context_type'; +import { buildFilter, FILTERS } from '../../es_query/filters'; +import { IndexPattern } from '../../index_patterns/index_patterns'; + +interface Arguments { + field: KibanaField; + negate?: boolean; +} + +export type ExpressionFunctionExistsFilter = ExpressionFunctionDefinition< + 'existsFilter', + null, + Arguments, + KibanaFilter +>; + +export const existsFilterFunction: ExpressionFunctionExistsFilter = { + name: 'existsFilter', + type: 'kibana_filter', + inputTypes: ['null'], + help: i18n.translate('data.search.functions.existsFilter.help', { + defaultMessage: 'Create kibana exists filter', + }), + args: { + field: { + types: ['kibana_field'], + required: true, + help: i18n.translate('data.search.functions.existsFilter.field.help', { + defaultMessage: 'Specify the field you want to filter on. Use `field` function.', + }), + }, + negate: { + types: ['boolean'], + default: false, + help: i18n.translate('data.search.functions.existsFilter.negate.help', { + defaultMessage: 'Should the filter be negated', + }), + }, + }, + + fn(input, args) { + return { + type: 'kibana_filter', + ...buildFilter( + ({} as any) as IndexPattern, + args.field.spec, + FILTERS.EXISTS, + args.negate || false, + false, + {}, + null + ), + }; + }, +}; diff --git a/src/plugins/data/common/search/expressions/field.test.ts b/src/plugins/data/common/search/expressions/field.test.ts new file mode 100644 index 0000000000000..2ad139e2bd0a7 --- /dev/null +++ b/src/plugins/data/common/search/expressions/field.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ExecutionContext } from 'src/plugins/expressions/common'; +import { functionWrapper } from './utils'; +import { fieldFunction } from './field'; + +describe('interpreter/functions#field', () => { + const fn = functionWrapper(fieldFunction); + let context: ExecutionContext; + + beforeEach(() => { + context = { + getSearchContext: () => ({}), + getSearchSessionId: () => undefined, + types: {}, + variables: {}, + abortSignal: {} as any, + inspectorAdapters: {} as any, + }; + }); + + it('returns an object with the correct structure', () => { + const actual = fn(null, { name: 'test', type: 'number' }, context); + expect(actual).toMatchInlineSnapshot(` + Object { + "spec": Object { + "name": "test", + "script": undefined, + "scripted": false, + "type": "number", + }, + "type": "kibana_field", + } + `); + }); +}); diff --git a/src/plugins/data/common/search/expressions/field.ts b/src/plugins/data/common/search/expressions/field.ts new file mode 100644 index 0000000000000..71f7c789c4873 --- /dev/null +++ b/src/plugins/data/common/search/expressions/field.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { KibanaField } from './kibana_context_type'; + +interface Arguments { + name: string; + type: string; + script?: string; +} + +export type ExpressionFunctionField = ExpressionFunctionDefinition< + 'field', + null, + Arguments, + KibanaField +>; + +export const fieldFunction: ExpressionFunctionField = { + name: 'field', + type: 'kibana_field', + inputTypes: ['null'], + help: i18n.translate('data.search.functions.field.help', { + defaultMessage: 'Create kibana_field', + }), + args: { + name: { + types: ['string'], + required: true, + help: i18n.translate('data.search.functions.field.name.help', { + defaultMessage: 'Name of the field', + }), + }, + type: { + types: ['string'], + required: true, + help: i18n.translate('data.search.functions.field.type.help', { + defaultMessage: 'Type of the field', + }), + }, + script: { + types: ['string'], + help: i18n.translate('data.search.functions.field.script.help', { + defaultMessage: 'script in case the field is scripted', + }), + }, + }, + + fn(input, args) { + return { + type: 'kibana_field', + spec: { + name: args.name, + type: args.type, + scripted: args.script ? true : false, + script: args.script, + }, + } as KibanaField; + }, +}; diff --git a/src/plugins/data/common/search/expressions/filters_to_ast.test.ts b/src/plugins/data/common/search/expressions/filters_to_ast.test.ts new file mode 100644 index 0000000000000..aa84f4665954e --- /dev/null +++ b/src/plugins/data/common/search/expressions/filters_to_ast.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { filtersToAst } from './filters_to_ast'; + +describe('interpreter/functions#filtersToAst', () => { + const normalFilter = { + meta: { negate: false, alias: '', disabled: false }, + query: { test: 'something' }, + }; + const negatedFilter = { + meta: { negate: true, alias: '', disabled: false }, + query: { test: 'something' }, + }; + + it('returns an object with the correct structure', () => { + const actual = filtersToAst([normalFilter, negatedFilter]); + expect(actual).toHaveLength(2); + expect(actual[0].functions[0]).toHaveProperty('name', 'kibanaFilter'); + expect(actual[0].functions[0].arguments).toMatchInlineSnapshot(` + Object { + "query": Array [ + Object { + "test": "something", + }, + ], + } + `); + expect(actual[1].functions[0]).toHaveProperty('name', 'kibanaFilter'); + expect(actual[1].functions[0].arguments).toMatchInlineSnapshot(` + Object { + "query": Array [ + Object { + "test": "something", + }, + ], + } + `); + }); +}); diff --git a/src/plugins/data/common/search/expressions/filters_to_ast.ts b/src/plugins/data/common/search/expressions/filters_to_ast.ts new file mode 100644 index 0000000000000..b64e6ac23da64 --- /dev/null +++ b/src/plugins/data/common/search/expressions/filters_to_ast.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { buildExpression, buildExpressionFunction } from '../../../../expressions/common'; +import { Filter } from '../../es_query/filters'; +import { ExpressionFunctionKibanaFilter } from './kibana_filter'; + +export const filtersToAst = (filters: Filter[] | Filter) => { + return (Array.isArray(filters) ? filters : [filters]).map((filter) => { + return buildExpression([ + buildExpressionFunction('kibanaFilter', { + query: filter.query, + }), + ]); + }); +}; diff --git a/src/plugins/data/common/search/expressions/index.ts b/src/plugins/data/common/search/expressions/index.ts index b38dce247261c..afd0446e981fa 100644 --- a/src/plugins/data/common/search/expressions/index.ts +++ b/src/plugins/data/common/search/expressions/index.ts @@ -15,4 +15,10 @@ export * from './timerange_to_ast'; export * from './kibana_context_type'; export * from './esaggs'; export * from './utils'; +export * from './range'; +export * from './field'; +export * from './phrase_filter'; +export * from './exists_filter'; +export * from './range_filter'; +export * from './filters_to_ast'; export * from './timerange'; diff --git a/src/plugins/data/common/search/expressions/kibana_context.ts b/src/plugins/data/common/search/expressions/kibana_context.ts index 5c2e2f418e69c..e4991e03e1211 100644 --- a/src/plugins/data/common/search/expressions/kibana_context.ts +++ b/src/plugins/data/common/search/expressions/kibana_context.ts @@ -11,13 +11,13 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition, ExecutionContext } from 'src/plugins/expressions/common'; import { Adapters } from 'src/plugins/inspector/common'; import { Query, uniqFilters } from '../../query'; -import { ExecutionContextSearch, KibanaContext } from './kibana_context_type'; +import { ExecutionContextSearch, KibanaContext, KibanaFilter } from './kibana_context_type'; import { KibanaQueryOutput } from './kibana_context_type'; import { KibanaTimerangeOutput } from './timerange'; interface Arguments { q?: KibanaQueryOutput | null; - filters?: string | null; + filters?: KibanaFilter[] | null; timeRange?: KibanaTimerangeOutput | null; savedSearchId?: string | null; } @@ -56,8 +56,8 @@ export const kibanaContextFunction: ExpressionFunctionKibanaContext = { }), }, filters: { - types: ['string', 'null'], - default: '"[]"', + types: ['kibana_filter', 'null'], + multi: true, help: i18n.translate('data.search.functions.kibana_context.filters.help', { defaultMessage: 'Specify Kibana generic filters', }), @@ -81,7 +81,7 @@ export const kibanaContextFunction: ExpressionFunctionKibanaContext = { async fn(input, args, { getSavedObject }) { const timeRange = args.timeRange || input?.timeRange; let queries = mergeQueries(input?.query, args?.q || []); - let filters = [...(input?.filters || []), ...getParsedValue(args?.filters, [])]; + let filters = [...(input?.filters || []), ...(args?.filters || [])]; if (args.savedSearchId) { if (typeof getSavedObject !== 'function') { diff --git a/src/plugins/data/common/search/expressions/kibana_context_type.ts b/src/plugins/data/common/search/expressions/kibana_context_type.ts index 090f09f7004ca..0a7c365bb2914 100644 --- a/src/plugins/data/common/search/expressions/kibana_context_type.ts +++ b/src/plugins/data/common/search/expressions/kibana_context_type.ts @@ -9,6 +9,7 @@ import { ExpressionValueBoxed } from 'src/plugins/expressions/common'; import { Filter } from '../../es_query'; import { Query, TimeRange } from '../../query'; +import { IndexPatternField } from '../../index_patterns/fields'; // eslint-disable-next-line @typescript-eslint/consistent-type-definitions export type ExecutionContextSearch = { @@ -23,6 +24,8 @@ export type ExpressionValueSearchContext = ExpressionValueBoxed< >; export type KibanaQueryOutput = ExpressionValueBoxed<'kibana_query', Query>; +export type KibanaFilter = ExpressionValueBoxed<'kibana_filter', Filter>; +export type KibanaField = ExpressionValueBoxed<'kibana_field', IndexPatternField>; // TODO: These two are exported for legacy reasons - remove them eventually. export type KIBANA_CONTEXT_NAME = 'kibana_context'; diff --git a/src/plugins/data/common/search/expressions/kibana_filter.test.ts b/src/plugins/data/common/search/expressions/kibana_filter.test.ts new file mode 100644 index 0000000000000..2968d2d2ad4ad --- /dev/null +++ b/src/plugins/data/common/search/expressions/kibana_filter.test.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ExecutionContext } from 'src/plugins/expressions/common'; +import { functionWrapper } from './utils'; +import { kibanaFilterFunction } from './kibana_filter'; + +describe('interpreter/functions#kibanaFilter', () => { + const fn = functionWrapper(kibanaFilterFunction); + let context: ExecutionContext; + + beforeEach(() => { + context = { + getSearchContext: () => ({}), + getSearchSessionId: () => undefined, + types: {}, + variables: {}, + abortSignal: {} as any, + inspectorAdapters: {} as any, + }; + }); + + it('returns an object with the correct structure', () => { + const actual = fn(null, { query: { name: 'test' } }, context); + expect(actual).toMatchInlineSnapshot(` + Object { + "meta": Object { + "alias": "", + "disabled": false, + "negate": false, + }, + "query": Object { + "name": "test", + }, + "type": "kibana_filter", + } + `); + }); +}); diff --git a/src/plugins/data/common/search/expressions/kibana_filter.ts b/src/plugins/data/common/search/expressions/kibana_filter.ts new file mode 100644 index 0000000000000..bf4731f1c3f61 --- /dev/null +++ b/src/plugins/data/common/search/expressions/kibana_filter.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { KibanaFilter } from './kibana_context_type'; + +interface Arguments { + query: string; + negate?: boolean; +} + +export type ExpressionFunctionKibanaFilter = ExpressionFunctionDefinition< + 'kibanaFilter', + null, + Arguments, + KibanaFilter +>; + +export const kibanaFilterFunction: ExpressionFunctionKibanaFilter = { + name: 'kibanaFilter', + type: 'kibana_filter', + inputTypes: ['null'], + help: i18n.translate('data.search.functions.kibanaFilter.help', { + defaultMessage: 'Create kibana filter', + }), + args: { + query: { + types: ['string'], + aliases: ['q', '_'], + required: true, + help: i18n.translate('data.search.functions.kibanaFilter.field.help', { + defaultMessage: 'Specify free form esdsl query', + }), + }, + negate: { + types: ['boolean'], + default: false, + help: i18n.translate('data.search.functions.kibanaFilter.negate.help', { + defaultMessage: 'Should the filter be negated', + }), + }, + }, + + fn(input, args) { + return { + type: 'kibana_filter', + meta: { + negate: args.negate || false, + alias: '', + disabled: false, + }, + query: args.query, + }; + }, +}; diff --git a/src/plugins/data/common/search/expressions/phrase_filter.test.ts b/src/plugins/data/common/search/expressions/phrase_filter.test.ts new file mode 100644 index 0000000000000..b95949f67da77 --- /dev/null +++ b/src/plugins/data/common/search/expressions/phrase_filter.test.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ExecutionContext } from 'src/plugins/expressions/common'; +import { functionWrapper } from './utils'; +import { phraseFilterFunction } from './phrase_filter'; + +describe('interpreter/functions#phraseFilter', () => { + const fn = functionWrapper(phraseFilterFunction); + let context: ExecutionContext; + + beforeEach(() => { + context = { + getSearchContext: () => ({}), + getSearchSessionId: () => undefined, + types: {}, + variables: {}, + abortSignal: {} as any, + inspectorAdapters: {} as any, + }; + }); + + it('returns an object with the correct structure', () => { + const actual = fn( + null, + { field: { spec: { name: 'test' } }, phrase: ['test', 'something'] }, + context + ); + expect(actual).toMatchInlineSnapshot(` + Object { + "meta": Object { + "alias": null, + "disabled": false, + "index": undefined, + "key": "test", + "negate": false, + "params": Array [ + "test", + "something", + ], + "type": "phrases", + "value": "test, something", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "test": "test", + }, + }, + Object { + "match_phrase": Object { + "test": "something", + }, + }, + ], + }, + }, + "type": "kibana_filter", + } + `); + }); +}); diff --git a/src/plugins/data/common/search/expressions/phrase_filter.ts b/src/plugins/data/common/search/expressions/phrase_filter.ts new file mode 100644 index 0000000000000..0b19e8a1e416d --- /dev/null +++ b/src/plugins/data/common/search/expressions/phrase_filter.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { KibanaField, KibanaFilter } from './kibana_context_type'; +import { buildFilter, FILTERS } from '../../es_query/filters'; +import { IndexPattern } from '../../index_patterns/index_patterns'; + +interface Arguments { + field: KibanaField; + phrase: string[]; + negate?: boolean; +} + +export type ExpressionFunctionPhraseFilter = ExpressionFunctionDefinition< + 'rangeFilter', + null, + Arguments, + KibanaFilter +>; + +export const phraseFilterFunction: ExpressionFunctionPhraseFilter = { + name: 'rangeFilter', + type: 'kibana_filter', + inputTypes: ['null'], + help: i18n.translate('data.search.functions.phraseFilter.help', { + defaultMessage: 'Create kibana phrase or phrases filter', + }), + args: { + field: { + types: ['kibana_field'], + required: true, + help: i18n.translate('data.search.functions.phraseFilter.field.help', { + defaultMessage: 'Specify the field you want to filter on. Use `field` function.', + }), + }, + phrase: { + types: ['string'], + multi: true, + required: true, + help: i18n.translate('data.search.functions.phraseFilter.phrase.help', { + defaultMessage: 'Specify the phrases', + }), + }, + negate: { + types: ['boolean'], + default: false, + help: i18n.translate('data.search.functions.phraseFilter.negate.help', { + defaultMessage: 'Should the filter be negated', + }), + }, + }, + + fn(input, args) { + if (args.phrase.length === 1) { + return { + type: 'kibana_filter', + ...buildFilter( + ({} as any) as IndexPattern, + args.field.spec, + FILTERS.PHRASE, + args.negate || false, + false, + args.phrase[0], + null + ), + }; + } + + return { + type: 'kibana_filter', + ...buildFilter( + ({} as any) as IndexPattern, + args.field.spec, + FILTERS.PHRASES, + args.negate || false, + false, + args.phrase, + null + ), + }; + }, +}; diff --git a/src/plugins/data/common/search/expressions/range.test.ts b/src/plugins/data/common/search/expressions/range.test.ts new file mode 100644 index 0000000000000..fbb4781c33a04 --- /dev/null +++ b/src/plugins/data/common/search/expressions/range.test.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ExecutionContext } from 'src/plugins/expressions/common'; +import { functionWrapper } from './utils'; +import { rangeFunction } from './range'; + +describe('interpreter/functions#range', () => { + const fn = functionWrapper(rangeFunction); + let context: ExecutionContext; + + beforeEach(() => { + context = { + getSearchContext: () => ({}), + getSearchSessionId: () => undefined, + types: {}, + variables: {}, + abortSignal: {} as any, + inspectorAdapters: {} as any, + }; + }); + + it('returns an object with the correct structure', () => { + const actual = fn(null, { lt: 20, gt: 10 }, context); + expect(actual).toMatchInlineSnapshot(` + Object { + "gt": 10, + "gte": undefined, + "lt": 20, + "lte": undefined, + "type": "kibana_range", + } + `); + }); +}); diff --git a/src/plugins/data/common/search/expressions/range.ts b/src/plugins/data/common/search/expressions/range.ts new file mode 100644 index 0000000000000..a72e0ca1038bb --- /dev/null +++ b/src/plugins/data/common/search/expressions/range.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition, ExpressionValueBoxed } from 'src/plugins/expressions/common'; + +interface Arguments { + gt?: number | string; + lt?: number | string; + gte?: number | string; + lte?: number | string; +} + +export type KibanaRange = ExpressionValueBoxed<'kibana_range', Arguments>; + +export type ExpressionFunctionRange = ExpressionFunctionDefinition< + 'range', + null, + Arguments, + KibanaRange +>; + +export const rangeFunction: ExpressionFunctionRange = { + name: 'range', + type: 'kibana_range', + inputTypes: ['null'], + help: i18n.translate('data.search.functions.field.help', { + defaultMessage: 'Create kibana range filter', + }), + args: { + gt: { + types: ['string', 'number'], + help: i18n.translate('data.search.functions.field.name.help', { + defaultMessage: 'Specify Lucene free form text query', + }), + }, + lt: { + types: ['string', 'number'], + help: i18n.translate('data.search.functions.field.name.help', { + defaultMessage: 'Specify Lucene free form text query', + }), + }, + gte: { + types: ['string', 'number'], + help: i18n.translate('data.search.functions.field.name.help', { + defaultMessage: 'Specify Lucene free form text query', + }), + }, + lte: { + types: ['string', 'number'], + help: i18n.translate('data.search.functions.field.name.help', { + defaultMessage: 'Specify Lucene free form text query', + }), + }, + }, + + fn(input, args) { + if (!args.lt && !args.lte) { + throw new Error('lt or lte must be provided'); + } + + if (!args.gt && !args.gte) { + throw new Error('lt or lte must be provided'); + } + + return { + type: 'kibana_range', + ...args, + }; + }, +}; diff --git a/src/plugins/data/common/search/expressions/range_filter.test.ts b/src/plugins/data/common/search/expressions/range_filter.test.ts new file mode 100644 index 0000000000000..3cf989511cf74 --- /dev/null +++ b/src/plugins/data/common/search/expressions/range_filter.test.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ExecutionContext } from 'src/plugins/expressions/common'; +import { functionWrapper } from './utils'; +import { rangeFilterFunction } from './range_filter'; + +describe('interpreter/functions#rangeFilter', () => { + const fn = functionWrapper(rangeFilterFunction); + let context: ExecutionContext; + + beforeEach(() => { + context = { + getSearchContext: () => ({}), + getSearchSessionId: () => undefined, + types: {}, + variables: {}, + abortSignal: {} as any, + inspectorAdapters: {} as any, + }; + }); + + it('returns an object with the correct structure', () => { + const actual = fn( + null, + { field: { spec: { name: 'test' } }, range: { gt: 10, lt: 20 } }, + context + ); + expect(actual).toMatchInlineSnapshot(` + Object { + "meta": Object { + "alias": null, + "disabled": false, + "index": undefined, + "negate": false, + "params": Object {}, + }, + "range": Object { + "test": Object { + "gte": 10, + "lt": 20, + }, + }, + "type": "kibana_filter", + } + `); + }); +}); diff --git a/src/plugins/data/common/search/expressions/range_filter.ts b/src/plugins/data/common/search/expressions/range_filter.ts new file mode 100644 index 0000000000000..ed71f5362fe85 --- /dev/null +++ b/src/plugins/data/common/search/expressions/range_filter.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { KibanaField, KibanaFilter } from './kibana_context_type'; +import { buildFilter, FILTERS } from '../../es_query/filters'; +import { IndexPattern } from '../../index_patterns/index_patterns'; +import { KibanaRange } from './range'; + +interface Arguments { + field: KibanaField; + range: KibanaRange; + negate?: boolean; +} + +export type ExpressionFunctionRangeFilter = ExpressionFunctionDefinition< + 'rangeFilter', + null, + Arguments, + KibanaFilter +>; + +export const rangeFilterFunction: ExpressionFunctionRangeFilter = { + name: 'rangeFilter', + type: 'kibana_filter', + inputTypes: ['null'], + help: i18n.translate('data.search.functions.rangeFilter.help', { + defaultMessage: 'Create kibana range filter', + }), + args: { + field: { + types: ['kibana_field'], + required: true, + help: i18n.translate('data.search.functions.rangeFilter.field.help', { + defaultMessage: 'Specify the field you want to filter on. Use `field` function.', + }), + }, + range: { + types: ['kibana_range'], + required: true, + help: i18n.translate('data.search.functions.rangeFilter.range.help', { + defaultMessage: 'Specify the range, use `range` function.', + }), + }, + negate: { + types: ['boolean'], + default: false, + help: i18n.translate('data.search.functions.rangeFilter.negate.help', { + defaultMessage: 'Should the filter be negated', + }), + }, + }, + + fn(input, args) { + return { + type: 'kibana_filter', + ...buildFilter( + ({} as any) as IndexPattern, + args.field.spec, + FILTERS.RANGE, + args.negate || false, + false, + { from: args.range.gt || args.range.gte, to: args.range.lt || args.range.lte }, + null + ), + }; + }, +}; diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 8eb73ba62244f..9cf3a35e0ab7b 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -28,6 +28,11 @@ import { kibanaTimerangeFunction, luceneFunction, kqlFunction, + fieldFunction, + rangeFunction, + existsFilterFunction, + rangeFilterFunction, + kibanaFilterFunction, } from '../../common/search'; import { getCallMsearch } from './legacy'; import { AggsService, AggsStartDependencies } from './aggs'; @@ -108,6 +113,11 @@ export class SearchService implements Plugin { expressions.registerFunction(luceneFunction); expressions.registerFunction(kqlFunction); expressions.registerFunction(kibanaTimerangeFunction); + expressions.registerFunction(fieldFunction); + expressions.registerFunction(rangeFunction); + expressions.registerFunction(kibanaFilterFunction); + expressions.registerFunction(existsFilterFunction); + expressions.registerFunction(rangeFilterFunction); expressions.registerType(kibanaContext); expressions.registerFunction(esdsl); diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index ab9fc84d51187..ef27be4c8c7fe 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -43,6 +43,8 @@ import { registerUsageCollector } from './collectors/register'; import { usageProvider } from './collectors/usage'; import { searchTelemetry } from '../saved_objects'; import { + existsFilterFunction, + fieldFunction, IEsSearchRequest, IEsSearchResponse, IKibanaSearchRequest, @@ -52,8 +54,11 @@ import { kibanaContext, kibanaContextFunction, kibanaTimerangeFunction, + kibanaFilterFunction, kqlFunction, luceneFunction, + rangeFilterFunction, + rangeFunction, SearchSourceDependencies, searchSourceRequiredUiSettings, SearchSourceService, @@ -149,6 +154,11 @@ export class SearchService implements Plugin { expressions.registerFunction(kqlFunction); expressions.registerFunction(kibanaTimerangeFunction); expressions.registerFunction(kibanaContextFunction); + expressions.registerFunction(fieldFunction); + expressions.registerFunction(rangeFunction); + expressions.registerFunction(kibanaFilterFunction); + expressions.registerFunction(existsFilterFunction); + expressions.registerFunction(rangeFilterFunction); expressions.registerType(kibanaContext); const aggs = this.aggsService.setup({ registerFunction: expressions.registerFunction }); diff --git a/src/plugins/visualizations/public/embeddable/to_ast.ts b/src/plugins/visualizations/public/embeddable/to_ast.ts index 7ccff9394943a..95b64fe4e6e17 100644 --- a/src/plugins/visualizations/public/embeddable/to_ast.ts +++ b/src/plugins/visualizations/public/embeddable/to_ast.ts @@ -10,7 +10,7 @@ import { ExpressionFunctionKibana, ExpressionFunctionKibanaContext } from '../.. import { buildExpression, buildExpressionFunction } from '../../../expressions/public'; import { VisToExpressionAst } from '../types'; -import { queryToAst } from '../../../data/common'; +import { queryToAst, filtersToAst } from '../../../data/common'; /** * Creates an ast expression for a visualization based on kibana context (query, filters, timerange) @@ -22,12 +22,15 @@ import { queryToAst } from '../../../data/common'; export const toExpressionAst: VisToExpressionAst = async (vis, params) => { const { savedSearchId, searchSource } = vis.data; const query = searchSource?.getField('query'); - const filters = searchSource?.getField('filter'); + let filters = searchSource?.getField('filter'); + if (typeof filters === 'function') { + filters = filters(); + } const kibana = buildExpressionFunction('kibana', {}); const kibanaContext = buildExpressionFunction('kibana_context', { q: query && queryToAst(query), - filters: filters && JSON.stringify(filters), + filters: filters && filtersToAst(filters), savedSearchId, }); From e7e002058d25546fa71d4f0659f6a3dbd3efc0ed Mon Sep 17 00:00:00 2001 From: ppisljar Date: Wed, 17 Mar 2021 17:38:00 +0100 Subject: [PATCH 2/9] fixes --- .../search/expressions/filters_to_ast.test.ts | 8 ++------ .../search/expressions/filters_to_ast.ts | 2 +- .../data/common/search/expressions/index.ts | 1 + .../common/search/expressions/kibana_filter.ts | 2 +- .../data/common/search/expressions/range.ts | 18 +++++++++--------- .../data/public/search/search_service.ts | 2 ++ .../data/server/search/search_service.ts | 2 ++ 7 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/plugins/data/common/search/expressions/filters_to_ast.test.ts b/src/plugins/data/common/search/expressions/filters_to_ast.test.ts index aa84f4665954e..fe51259543937 100644 --- a/src/plugins/data/common/search/expressions/filters_to_ast.test.ts +++ b/src/plugins/data/common/search/expressions/filters_to_ast.test.ts @@ -25,9 +25,7 @@ describe('interpreter/functions#filtersToAst', () => { expect(actual[0].functions[0].arguments).toMatchInlineSnapshot(` Object { "query": Array [ - Object { - "test": "something", - }, + "{\\"test\\":\\"something\\"}", ], } `); @@ -35,9 +33,7 @@ describe('interpreter/functions#filtersToAst', () => { expect(actual[1].functions[0].arguments).toMatchInlineSnapshot(` Object { "query": Array [ - Object { - "test": "something", - }, + "{\\"test\\":\\"something\\"}", ], } `); diff --git a/src/plugins/data/common/search/expressions/filters_to_ast.ts b/src/plugins/data/common/search/expressions/filters_to_ast.ts index b64e6ac23da64..ec5b51a057888 100644 --- a/src/plugins/data/common/search/expressions/filters_to_ast.ts +++ b/src/plugins/data/common/search/expressions/filters_to_ast.ts @@ -14,7 +14,7 @@ export const filtersToAst = (filters: Filter[] | Filter) => { return (Array.isArray(filters) ? filters : [filters]).map((filter) => { return buildExpression([ buildExpressionFunction('kibanaFilter', { - query: filter.query, + query: JSON.stringify(filter.query), }), ]); }); diff --git a/src/plugins/data/common/search/expressions/index.ts b/src/plugins/data/common/search/expressions/index.ts index afd0446e981fa..b80cbad778a11 100644 --- a/src/plugins/data/common/search/expressions/index.ts +++ b/src/plugins/data/common/search/expressions/index.ts @@ -20,5 +20,6 @@ export * from './field'; export * from './phrase_filter'; export * from './exists_filter'; export * from './range_filter'; +export * from './kibana_filter'; export * from './filters_to_ast'; export * from './timerange'; diff --git a/src/plugins/data/common/search/expressions/kibana_filter.ts b/src/plugins/data/common/search/expressions/kibana_filter.ts index bf4731f1c3f61..14c34d16763ef 100644 --- a/src/plugins/data/common/search/expressions/kibana_filter.ts +++ b/src/plugins/data/common/search/expressions/kibana_filter.ts @@ -55,7 +55,7 @@ export const kibanaFilterFunction: ExpressionFunctionKibanaFilter = { alias: '', disabled: false, }, - query: args.query, + query: JSON.parse(args.query), }; }, }; diff --git a/src/plugins/data/common/search/expressions/range.ts b/src/plugins/data/common/search/expressions/range.ts index a72e0ca1038bb..123dca3c485b0 100644 --- a/src/plugins/data/common/search/expressions/range.ts +++ b/src/plugins/data/common/search/expressions/range.ts @@ -29,32 +29,32 @@ export const rangeFunction: ExpressionFunctionRange = { name: 'range', type: 'kibana_range', inputTypes: ['null'], - help: i18n.translate('data.search.functions.field.help', { + help: i18n.translate('data.search.functions.range.help', { defaultMessage: 'Create kibana range filter', }), args: { gt: { types: ['string', 'number'], - help: i18n.translate('data.search.functions.field.name.help', { - defaultMessage: 'Specify Lucene free form text query', + help: i18n.translate('data.search.functions.range.gt.help', { + defaultMessage: 'Greater than', }), }, lt: { types: ['string', 'number'], - help: i18n.translate('data.search.functions.field.name.help', { - defaultMessage: 'Specify Lucene free form text query', + help: i18n.translate('data.search.functions.range.lt.help', { + defaultMessage: 'Less than', }), }, gte: { types: ['string', 'number'], - help: i18n.translate('data.search.functions.field.name.help', { - defaultMessage: 'Specify Lucene free form text query', + help: i18n.translate('data.search.functions.range.gte.help', { + defaultMessage: 'Greater or equal than', }), }, lte: { types: ['string', 'number'], - help: i18n.translate('data.search.functions.field.name.help', { - defaultMessage: 'Specify Lucene free form text query', + help: i18n.translate('data.search.functions.range.lte.help', { + defaultMessage: 'Less or equal than', }), }, }, diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 9cf3a35e0ab7b..94fa5b7230f69 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -33,6 +33,7 @@ import { existsFilterFunction, rangeFilterFunction, kibanaFilterFunction, + phraseFilterFunction, } from '../../common/search'; import { getCallMsearch } from './legacy'; import { AggsService, AggsStartDependencies } from './aggs'; @@ -118,6 +119,7 @@ export class SearchService implements Plugin { expressions.registerFunction(kibanaFilterFunction); expressions.registerFunction(existsFilterFunction); expressions.registerFunction(rangeFilterFunction); + expressions.registerFunction(phraseFilterFunction); expressions.registerType(kibanaContext); expressions.registerFunction(esdsl); diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index ef27be4c8c7fe..69710e82b73b4 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -62,6 +62,7 @@ import { SearchSourceDependencies, searchSourceRequiredUiSettings, SearchSourceService, + phraseFilterFunction, } from '../../common/search'; import { getEsaggs } from './expressions'; import { @@ -159,6 +160,7 @@ export class SearchService implements Plugin { expressions.registerFunction(kibanaFilterFunction); expressions.registerFunction(existsFilterFunction); expressions.registerFunction(rangeFilterFunction); + expressions.registerFunction(phraseFilterFunction); expressions.registerType(kibanaContext); const aggs = this.aggsService.setup({ registerFunction: expressions.registerFunction }); From 3daaa59ca7ac0a51b3efe1cae266eef7ec113dcd Mon Sep 17 00:00:00 2001 From: ppisljar Date: Mon, 22 Mar 2021 15:17:18 +0100 Subject: [PATCH 3/9] fixes --- src/plugins/data/common/search/expressions/filters_to_ast.ts | 1 + .../data/common/search/expressions/kibana_filter.test.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/data/common/search/expressions/filters_to_ast.ts b/src/plugins/data/common/search/expressions/filters_to_ast.ts index ec5b51a057888..f537a0a51def1 100644 --- a/src/plugins/data/common/search/expressions/filters_to_ast.ts +++ b/src/plugins/data/common/search/expressions/filters_to_ast.ts @@ -15,6 +15,7 @@ export const filtersToAst = (filters: Filter[] | Filter) => { return buildExpression([ buildExpressionFunction('kibanaFilter', { query: JSON.stringify(filter.query), + negate: filter.meta.negate, }), ]); }); diff --git a/src/plugins/data/common/search/expressions/kibana_filter.test.ts b/src/plugins/data/common/search/expressions/kibana_filter.test.ts index 2968d2d2ad4ad..927b27c2e2954 100644 --- a/src/plugins/data/common/search/expressions/kibana_filter.test.ts +++ b/src/plugins/data/common/search/expressions/kibana_filter.test.ts @@ -26,7 +26,7 @@ describe('interpreter/functions#kibanaFilter', () => { }); it('returns an object with the correct structure', () => { - const actual = fn(null, { query: { name: 'test' } }, context); + const actual = fn(null, { query: '{ "name": "test" }' }, context); expect(actual).toMatchInlineSnapshot(` Object { "meta": Object { From 0fbb0ff6e60a9e8d80cb50e271f2816e1e15302a Mon Sep 17 00:00:00 2001 From: ppisljar Date: Mon, 22 Mar 2021 16:17:06 +0100 Subject: [PATCH 4/9] fixes --- src/plugins/data/common/search/expressions/filters_to_ast.ts | 3 ++- src/plugins/data/common/search/expressions/kibana_filter.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/plugins/data/common/search/expressions/filters_to_ast.ts b/src/plugins/data/common/search/expressions/filters_to_ast.ts index f537a0a51def1..a4dd959caecf6 100644 --- a/src/plugins/data/common/search/expressions/filters_to_ast.ts +++ b/src/plugins/data/common/search/expressions/filters_to_ast.ts @@ -12,9 +12,10 @@ import { ExpressionFunctionKibanaFilter } from './kibana_filter'; export const filtersToAst = (filters: Filter[] | Filter) => { return (Array.isArray(filters) ? filters : [filters]).map((filter) => { + const { meta, $state, ...restOfFilter } = filter; return buildExpression([ buildExpressionFunction('kibanaFilter', { - query: JSON.stringify(filter.query), + query: JSON.stringify(restOfFilter), negate: filter.meta.negate, }), ]); diff --git a/src/plugins/data/common/search/expressions/kibana_filter.ts b/src/plugins/data/common/search/expressions/kibana_filter.ts index 14c34d16763ef..6d6f70fa8d1d6 100644 --- a/src/plugins/data/common/search/expressions/kibana_filter.ts +++ b/src/plugins/data/common/search/expressions/kibana_filter.ts @@ -55,7 +55,7 @@ export const kibanaFilterFunction: ExpressionFunctionKibanaFilter = { alias: '', disabled: false, }, - query: JSON.parse(args.query), + ...JSON.parse(args.query), }; }, }; From 7ec093356df3139c7f8679b53dbaaa7ba73170c8 Mon Sep 17 00:00:00 2001 From: ppisljar Date: Mon, 22 Mar 2021 18:09:51 +0100 Subject: [PATCH 5/9] fixes --- .../data/common/search/expressions/kibana_context.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/plugins/data/common/search/expressions/kibana_context.ts b/src/plugins/data/common/search/expressions/kibana_context.ts index e4991e03e1211..f2ba2a6e3dd9d 100644 --- a/src/plugins/data/common/search/expressions/kibana_context.ts +++ b/src/plugins/data/common/search/expressions/kibana_context.ts @@ -81,7 +81,14 @@ export const kibanaContextFunction: ExpressionFunctionKibanaContext = { async fn(input, args, { getSavedObject }) { const timeRange = args.timeRange || input?.timeRange; let queries = mergeQueries(input?.query, args?.q || []); - let filters = [...(input?.filters || []), ...(args?.filters || [])]; + let filters = [ + ...(input?.filters || []), + ...(args?.filters?.map((f) => { + // @ts-ignore + delete f.type; + return f; + }) || []), + ]; if (args.savedSearchId) { if (typeof getSavedObject !== 'function') { From 7bbbe6080fdf28914a4c4b557a780731c4e96b25 Mon Sep 17 00:00:00 2001 From: Michael Dokolin Date: Mon, 22 Mar 2021 18:40:14 +0100 Subject: [PATCH 6/9] Refactor filters unboxing --- .../common/search/expressions/kibana_context.ts | 15 ++++++--------- .../common/expression_types/index.ts | 1 + .../unbox_expression_value.test.ts | 17 +++++++++++++++++ .../expression_types/unbox_expression_value.ts | 16 ++++++++++++++++ 4 files changed, 40 insertions(+), 9 deletions(-) create mode 100644 src/plugins/expressions/common/expression_types/unbox_expression_value.test.ts create mode 100644 src/plugins/expressions/common/expression_types/unbox_expression_value.ts diff --git a/src/plugins/data/common/search/expressions/kibana_context.ts b/src/plugins/data/common/search/expressions/kibana_context.ts index f2ba2a6e3dd9d..f530d4b0b4663 100644 --- a/src/plugins/data/common/search/expressions/kibana_context.ts +++ b/src/plugins/data/common/search/expressions/kibana_context.ts @@ -8,7 +8,11 @@ import { uniqBy } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition, ExecutionContext } from 'src/plugins/expressions/common'; +import { + ExpressionFunctionDefinition, + ExecutionContext, + unboxExpressionValue, +} from 'src/plugins/expressions/common'; import { Adapters } from 'src/plugins/inspector/common'; import { Query, uniqFilters } from '../../query'; import { ExecutionContextSearch, KibanaContext, KibanaFilter } from './kibana_context_type'; @@ -81,14 +85,7 @@ export const kibanaContextFunction: ExpressionFunctionKibanaContext = { async fn(input, args, { getSavedObject }) { const timeRange = args.timeRange || input?.timeRange; let queries = mergeQueries(input?.query, args?.q || []); - let filters = [ - ...(input?.filters || []), - ...(args?.filters?.map((f) => { - // @ts-ignore - delete f.type; - return f; - }) || []), - ]; + let filters = [...(input?.filters || []), ...(args?.filters?.map(unboxExpressionValue) || [])]; if (args.savedSearchId) { if (typeof getSavedObject !== 'function') { diff --git a/src/plugins/expressions/common/expression_types/index.ts b/src/plugins/expressions/common/expression_types/index.ts index 169dd434cf430..c7f0340a52fad 100644 --- a/src/plugins/expressions/common/expression_types/index.ts +++ b/src/plugins/expressions/common/expression_types/index.ts @@ -11,3 +11,4 @@ export * from './get_type'; export * from './serialize_provider'; export * from './expression_type'; export * from './specs'; +export * from './unbox_expression_value'; diff --git a/src/plugins/expressions/common/expression_types/unbox_expression_value.test.ts b/src/plugins/expressions/common/expression_types/unbox_expression_value.test.ts new file mode 100644 index 0000000000000..ace99d4925e9f --- /dev/null +++ b/src/plugins/expressions/common/expression_types/unbox_expression_value.test.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { unboxExpressionValue } from './unbox_expression_value'; + +describe('unboxExpressionValue()', () => { + it('should remove type property from a boxed value', () => { + const expressionValue = { type: 'something', value: 'something' }; + + expect(unboxExpressionValue(expressionValue)).toEqual({ value: 'something' }); + }); +}); diff --git a/src/plugins/expressions/common/expression_types/unbox_expression_value.ts b/src/plugins/expressions/common/expression_types/unbox_expression_value.ts new file mode 100644 index 0000000000000..bb41efdf15609 --- /dev/null +++ b/src/plugins/expressions/common/expression_types/unbox_expression_value.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ExpressionValueBoxed } from './types'; + +export function unboxExpressionValue({ + type, + ...value +}: ExpressionValueBoxed): T { + return value as T; +} From 57b3d719bcc5265db12769601b636ca0a49b83b6 Mon Sep 17 00:00:00 2001 From: ppisljar Date: Tue, 23 Mar 2021 06:32:32 +0100 Subject: [PATCH 7/9] fixes --- .../common/search/expressions/filters_to_ast.test.ts | 10 ++++++++-- .../data/common/search/expressions/kibana_context.ts | 7 ++----- .../common/search/expressions/kibana_filter.test.ts | 4 +--- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/plugins/data/common/search/expressions/filters_to_ast.test.ts b/src/plugins/data/common/search/expressions/filters_to_ast.test.ts index fe51259543937..6d7e656446b50 100644 --- a/src/plugins/data/common/search/expressions/filters_to_ast.test.ts +++ b/src/plugins/data/common/search/expressions/filters_to_ast.test.ts @@ -24,16 +24,22 @@ describe('interpreter/functions#filtersToAst', () => { expect(actual[0].functions[0]).toHaveProperty('name', 'kibanaFilter'); expect(actual[0].functions[0].arguments).toMatchInlineSnapshot(` Object { + "negate": Array [ + false, + ], "query": Array [ - "{\\"test\\":\\"something\\"}", + "{\\"query\\":{\\"test\\":\\"something\\"}}", ], } `); expect(actual[1].functions[0]).toHaveProperty('name', 'kibanaFilter'); expect(actual[1].functions[0].arguments).toMatchInlineSnapshot(` Object { + "negate": Array [ + true, + ], "query": Array [ - "{\\"test\\":\\"something\\"}", + "{\\"query\\":{\\"test\\":\\"something\\"}}", ], } `); diff --git a/src/plugins/data/common/search/expressions/kibana_context.ts b/src/plugins/data/common/search/expressions/kibana_context.ts index f530d4b0b4663..98d7a2c45b4fc 100644 --- a/src/plugins/data/common/search/expressions/kibana_context.ts +++ b/src/plugins/data/common/search/expressions/kibana_context.ts @@ -8,12 +8,9 @@ import { uniqBy } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { - ExpressionFunctionDefinition, - ExecutionContext, - unboxExpressionValue, -} from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition, ExecutionContext } from 'src/plugins/expressions/common'; import { Adapters } from 'src/plugins/inspector/common'; +import { unboxExpressionValue } from '../../../../expressions/common'; import { Query, uniqFilters } from '../../query'; import { ExecutionContextSearch, KibanaContext, KibanaFilter } from './kibana_context_type'; import { KibanaQueryOutput } from './kibana_context_type'; diff --git a/src/plugins/data/common/search/expressions/kibana_filter.test.ts b/src/plugins/data/common/search/expressions/kibana_filter.test.ts index 927b27c2e2954..1446fe7646030 100644 --- a/src/plugins/data/common/search/expressions/kibana_filter.test.ts +++ b/src/plugins/data/common/search/expressions/kibana_filter.test.ts @@ -34,9 +34,7 @@ describe('interpreter/functions#kibanaFilter', () => { "disabled": false, "negate": false, }, - "query": Object { - "name": "test", - }, + "name": "test", "type": "kibana_filter", } `); From 9722fb9207abb93c1f6d716d2fb02154398c1d90 Mon Sep 17 00:00:00 2001 From: Peter Pisljar Date: Tue, 23 Mar 2021 14:44:41 +0100 Subject: [PATCH 8/9] Apply suggestions from code review Co-authored-by: Vadim Dalecky --- src/plugins/data/common/search/expressions/exists_filter.ts | 2 +- src/plugins/data/common/search/expressions/field.ts | 4 ++-- .../data/common/search/expressions/filters_to_ast.test.ts | 2 +- src/plugins/data/common/search/expressions/range.ts | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/plugins/data/common/search/expressions/exists_filter.ts b/src/plugins/data/common/search/expressions/exists_filter.ts index d20d467a20680..0979328860b4c 100644 --- a/src/plugins/data/common/search/expressions/exists_filter.ts +++ b/src/plugins/data/common/search/expressions/exists_filter.ts @@ -43,7 +43,7 @@ export const existsFilterFunction: ExpressionFunctionExistsFilter = { types: ['boolean'], default: false, help: i18n.translate('data.search.functions.existsFilter.negate.help', { - defaultMessage: 'Should the filter be negated', + defaultMessage: 'Should the filter be negated.', }), }, }, diff --git a/src/plugins/data/common/search/expressions/field.ts b/src/plugins/data/common/search/expressions/field.ts index 71f7c789c4873..8c13069f50ad5 100644 --- a/src/plugins/data/common/search/expressions/field.ts +++ b/src/plugins/data/common/search/expressions/field.ts @@ -28,7 +28,7 @@ export const fieldFunction: ExpressionFunctionField = { type: 'kibana_field', inputTypes: ['null'], help: i18n.translate('data.search.functions.field.help', { - defaultMessage: 'Create kibana_field', + defaultMessage: 'Create a Kibana field.', }), args: { name: { @@ -48,7 +48,7 @@ export const fieldFunction: ExpressionFunctionField = { script: { types: ['string'], help: i18n.translate('data.search.functions.field.script.help', { - defaultMessage: 'script in case the field is scripted', + defaultMessage: 'A field script, in case the field is scripted.', }), }, }, diff --git a/src/plugins/data/common/search/expressions/filters_to_ast.test.ts b/src/plugins/data/common/search/expressions/filters_to_ast.test.ts index 6d7e656446b50..108b48f9ea77e 100644 --- a/src/plugins/data/common/search/expressions/filters_to_ast.test.ts +++ b/src/plugins/data/common/search/expressions/filters_to_ast.test.ts @@ -18,7 +18,7 @@ describe('interpreter/functions#filtersToAst', () => { query: { test: 'something' }, }; - it('returns an object with the correct structure', () => { + it('converts a list of filters to an expression AST node', () => { const actual = filtersToAst([normalFilter, negatedFilter]); expect(actual).toHaveLength(2); expect(actual[0].functions[0]).toHaveProperty('name', 'kibanaFilter'); diff --git a/src/plugins/data/common/search/expressions/range.ts b/src/plugins/data/common/search/expressions/range.ts index 123dca3c485b0..c7649a6e0669c 100644 --- a/src/plugins/data/common/search/expressions/range.ts +++ b/src/plugins/data/common/search/expressions/range.ts @@ -60,12 +60,12 @@ export const rangeFunction: ExpressionFunctionRange = { }, fn(input, args) { - if (!args.lt && !args.lte) { + if (args.lt === undefined && args.lte === undefined) { throw new Error('lt or lte must be provided'); } - if (!args.gt && !args.gte) { - throw new Error('lt or lte must be provided'); + if (args.gt === undefined && args.gte === undefined) { + throw new Error('gt or gte must be provided'); } return { From bdbc1d19051a5ac60d6a108d75a9d85e2590035e Mon Sep 17 00:00:00 2001 From: ppisljar Date: Tue, 23 Mar 2021 14:54:37 +0100 Subject: [PATCH 9/9] improving tests --- .../search/expressions/exists_filter.test.ts | 16 ++------------- .../search/expressions/kibana_filter.test.ts | 16 ++------------- .../search/expressions/phrase_filter.test.ts | 16 ++------------- .../search/expressions/range_filter.test.ts | 16 ++------------- src/plugins/expressions/common/util/index.ts | 1 + .../expressions/common/util/test_utils.ts | 20 +++++++++++++++++++ 6 files changed, 29 insertions(+), 56 deletions(-) create mode 100644 src/plugins/expressions/common/util/test_utils.ts diff --git a/src/plugins/data/common/search/expressions/exists_filter.test.ts b/src/plugins/data/common/search/expressions/exists_filter.test.ts index 5b986e2d0a0a2..e3b53b2281398 100644 --- a/src/plugins/data/common/search/expressions/exists_filter.test.ts +++ b/src/plugins/data/common/search/expressions/exists_filter.test.ts @@ -6,27 +6,15 @@ * Side Public License, v 1. */ -import { ExecutionContext } from 'src/plugins/expressions/common'; +import { createMockContext } from '../../../../expressions/common'; import { functionWrapper } from './utils'; import { existsFilterFunction } from './exists_filter'; describe('interpreter/functions#existsFilter', () => { const fn = functionWrapper(existsFilterFunction); - let context: ExecutionContext; - - beforeEach(() => { - context = { - getSearchContext: () => ({}), - getSearchSessionId: () => undefined, - types: {}, - variables: {}, - abortSignal: {} as any, - inspectorAdapters: {} as any, - }; - }); it('returns an object with the correct structure', () => { - const actual = fn(null, { field: { spec: { name: 'test' } } }, context); + const actual = fn(null, { field: { spec: { name: 'test' } } }, createMockContext()); expect(actual).toMatchInlineSnapshot(` Object { "exists": Object { diff --git a/src/plugins/data/common/search/expressions/kibana_filter.test.ts b/src/plugins/data/common/search/expressions/kibana_filter.test.ts index 1446fe7646030..ac8ae55492cc0 100644 --- a/src/plugins/data/common/search/expressions/kibana_filter.test.ts +++ b/src/plugins/data/common/search/expressions/kibana_filter.test.ts @@ -6,27 +6,15 @@ * Side Public License, v 1. */ -import { ExecutionContext } from 'src/plugins/expressions/common'; +import { createMockContext } from '../../../../expressions/common'; import { functionWrapper } from './utils'; import { kibanaFilterFunction } from './kibana_filter'; describe('interpreter/functions#kibanaFilter', () => { const fn = functionWrapper(kibanaFilterFunction); - let context: ExecutionContext; - - beforeEach(() => { - context = { - getSearchContext: () => ({}), - getSearchSessionId: () => undefined, - types: {}, - variables: {}, - abortSignal: {} as any, - inspectorAdapters: {} as any, - }; - }); it('returns an object with the correct structure', () => { - const actual = fn(null, { query: '{ "name": "test" }' }, context); + const actual = fn(null, { query: '{ "name": "test" }' }, createMockContext()); expect(actual).toMatchInlineSnapshot(` Object { "meta": Object { diff --git a/src/plugins/data/common/search/expressions/phrase_filter.test.ts b/src/plugins/data/common/search/expressions/phrase_filter.test.ts index b95949f67da77..39bd907513a0d 100644 --- a/src/plugins/data/common/search/expressions/phrase_filter.test.ts +++ b/src/plugins/data/common/search/expressions/phrase_filter.test.ts @@ -6,30 +6,18 @@ * Side Public License, v 1. */ -import { ExecutionContext } from 'src/plugins/expressions/common'; +import { createMockContext } from '../../../../expressions/common'; import { functionWrapper } from './utils'; import { phraseFilterFunction } from './phrase_filter'; describe('interpreter/functions#phraseFilter', () => { const fn = functionWrapper(phraseFilterFunction); - let context: ExecutionContext; - - beforeEach(() => { - context = { - getSearchContext: () => ({}), - getSearchSessionId: () => undefined, - types: {}, - variables: {}, - abortSignal: {} as any, - inspectorAdapters: {} as any, - }; - }); it('returns an object with the correct structure', () => { const actual = fn( null, { field: { spec: { name: 'test' } }, phrase: ['test', 'something'] }, - context + createMockContext() ); expect(actual).toMatchInlineSnapshot(` Object { diff --git a/src/plugins/data/common/search/expressions/range_filter.test.ts b/src/plugins/data/common/search/expressions/range_filter.test.ts index 3cf989511cf74..92670f8a044ba 100644 --- a/src/plugins/data/common/search/expressions/range_filter.test.ts +++ b/src/plugins/data/common/search/expressions/range_filter.test.ts @@ -6,30 +6,18 @@ * Side Public License, v 1. */ -import { ExecutionContext } from 'src/plugins/expressions/common'; +import { createMockContext } from '../../../../expressions/common'; import { functionWrapper } from './utils'; import { rangeFilterFunction } from './range_filter'; describe('interpreter/functions#rangeFilter', () => { const fn = functionWrapper(rangeFilterFunction); - let context: ExecutionContext; - - beforeEach(() => { - context = { - getSearchContext: () => ({}), - getSearchSessionId: () => undefined, - types: {}, - variables: {}, - abortSignal: {} as any, - inspectorAdapters: {} as any, - }; - }); it('returns an object with the correct structure', () => { const actual = fn( null, { field: { spec: { name: 'test' } }, range: { gt: 10, lt: 20 } }, - context + createMockContext() ); expect(actual).toMatchInlineSnapshot(` Object { diff --git a/src/plugins/expressions/common/util/index.ts b/src/plugins/expressions/common/util/index.ts index 5f83d962d5aea..470dfc3c2d436 100644 --- a/src/plugins/expressions/common/util/index.ts +++ b/src/plugins/expressions/common/util/index.ts @@ -10,3 +10,4 @@ export * from './create_error'; export * from './get_by_alias'; export * from './tables_adapter'; export * from './expressions_inspector_adapter'; +export * from './test_utils'; diff --git a/src/plugins/expressions/common/util/test_utils.ts b/src/plugins/expressions/common/util/test_utils.ts new file mode 100644 index 0000000000000..59bd0a4235d9b --- /dev/null +++ b/src/plugins/expressions/common/util/test_utils.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ExecutionContext } from '../execution'; + +export const createMockContext = () => { + return { + getSearchContext: () => ({}), + getSearchSessionId: () => undefined, + types: {}, + variables: {}, + abortSignal: {} as any, + inspectorAdapters: {} as any, + } as ExecutionContext; +};