[KQL] Add support for toKqlExpression (#161601)

## Summary

Resolves https://github.com/elastic/kibana/issues/77971.

Adds a `toKqlExpression` method to the `@kbn/es-query` that allows
generating a KQL expression from an AST node.

Example:

```ts
const node = fromKueryExpression('extension: "jpg"');
const kql = toKqlExpression(node); // 'extension: "jpg"'
```

Note that the generated KQL expression may not exactly match the
original text (whitespace is not preserved, parentheses may be added,
etc.).

### Checklist

Delete any items that are not applicable to this PR.

- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
This commit is contained in:
Lukas Olson 2023-07-14 13:44:50 -07:00 committed by GitHub
parent e9a0ad188b
commit 12e748486c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 750 additions and 141 deletions

View file

@ -40,13 +40,7 @@ export type {
CombinedFilter,
} from './src/filters';
export type {
DslQuery,
FunctionTypeBuildNode,
KueryNode,
KueryParseOptions,
KueryQueryOptions,
} from './src/kuery';
export type { DslQuery, KueryNode, KueryParseOptions, KueryQueryOptions } from './src/kuery';
export {
buildEsQuery,
@ -117,6 +111,7 @@ export {
export {
KQLSyntaxError,
fromKueryExpression,
toKqlExpression,
nodeBuilder,
nodeTypes,
toElasticsearchQuery,

View file

@ -6,7 +6,12 @@
* Side Public License, v 1.
*/
import { fromKueryExpression, fromLiteralExpression, toElasticsearchQuery } from './ast';
import {
fromKueryExpression,
fromLiteralExpression,
toElasticsearchQuery,
toKqlExpression,
} from './ast';
import { nodeTypes } from '../node_types';
import { DataViewBase } from '../../..';
import { KueryNode } from '../types';
@ -387,4 +392,44 @@ describe('kuery AST API', () => {
expect(result).toEqual(expected);
});
});
describe('toKqlExpression', () => {
test('function node', () => {
const node = nodeTypes.function.buildNode('exists', 'response');
const result = toKqlExpression(node);
expect(result).toEqual('response: *');
});
test('literal node', () => {
const node = nodeTypes.literal.buildNode('foo');
const result = toKqlExpression(node);
expect(result).toEqual('foo');
});
test('wildcard node', () => {
const node = nodeTypes.wildcard.buildNode('foo*bar');
const result = toKqlExpression(node);
expect(result).toEqual('foo*bar');
});
test('should throw an error with invalid node type', () => {
const noTypeNode = nodeTypes.function.buildNode('exists', 'foo');
// @ts-expect-error
delete noTypeNode.type;
expect(() => toKqlExpression(noTypeNode)).toThrowErrorMatchingInlineSnapshot(
`"Unknown KQL node type: \\"undefined\\""`
);
});
test('fromKueryExpression toKqlExpression', () => {
const node = fromKueryExpression(
'field: (value AND value2 OR "value3") OR nested: { field2: value4 }'
);
const result = toKqlExpression(node);
expect(result).toMatchInlineSnapshot(
`"(((field: value AND field: value2) OR field: \\"value3\\") OR nested: { field2: value4 })"`
);
});
});
});

View file

@ -56,6 +56,18 @@ export const fromKueryExpression = (
}
};
/**
* Given a KQL AST node, generate the corresponding KQL expression.
* @public
* @param node
*/
export function toKqlExpression(node: KueryNode): string {
if (nodeTypes.function.isNode(node)) return nodeTypes.function.toKqlExpression(node);
if (nodeTypes.literal.isNode(node)) return nodeTypes.literal.toKqlExpression(node);
if (nodeTypes.wildcard.isNode(node)) return nodeTypes.wildcard.toKqlExpression(node);
throw new Error(`Unknown KQL node type: "${node.type}"`);
}
/**
* @params {String} indexPattern
* @params {Object} config - contains the dateFormatTZ

View file

@ -11,6 +11,7 @@ import { fields } from '../../filters/stubs';
import * as ast from '../ast';
import * as and from './and';
import { DataViewBase } from '../../es_query';
import { KqlAndFunctionNode } from './and';
jest.mock('../grammar');
@ -42,15 +43,18 @@ describe('kuery functions', () => {
describe('toElasticsearchQuery', () => {
test("should wrap subqueries in an ES bool query's filter clause", () => {
const node = nodeTypes.function.buildNode('and', [childNode1, childNode2]);
const node = nodeTypes.function.buildNode('and', [
childNode1,
childNode2,
]) as KqlAndFunctionNode;
const result = and.toElasticsearchQuery(node, indexPattern);
expect(result).toHaveProperty('bool');
expect(Object.keys(result).length).toBe(1);
expect(result.bool).toHaveProperty('filter');
expect(Object.keys(result.bool).length).toBe(1);
expect(Object.keys(result.bool!).length).toBe(1);
expect(result.bool.filter).toEqual(
expect(result.bool!.filter).toEqual(
[childNode1, childNode2].map((childNode) =>
ast.toElasticsearchQuery(childNode, indexPattern)
)
@ -58,7 +62,10 @@ describe('kuery functions', () => {
});
test("should wrap subqueries in an ES bool query's must clause for scoring if enabled", () => {
const node = nodeTypes.function.buildNode('and', [childNode1, childNode2]);
const node = nodeTypes.function.buildNode('and', [
childNode1,
childNode2,
]) as KqlAndFunctionNode;
const result = and.toElasticsearchQuery(node, indexPattern, {
filtersInMustClause: true,
});
@ -66,14 +73,31 @@ describe('kuery functions', () => {
expect(result).toHaveProperty('bool');
expect(Object.keys(result).length).toBe(1);
expect(result.bool).toHaveProperty('must');
expect(Object.keys(result.bool).length).toBe(1);
expect(Object.keys(result.bool!).length).toBe(1);
expect(result.bool.must).toEqual(
expect(result.bool!.must).toEqual(
[childNode1, childNode2].map((childNode) =>
ast.toElasticsearchQuery(childNode, indexPattern)
)
);
});
});
describe('toKqlExpression', () => {
test('with one sub-expression', () => {
const node = nodeTypes.function.buildNode('and', [childNode1]) as KqlAndFunctionNode;
const result = and.toKqlExpression(node);
expect(result).toBe('(machine.os: osx)');
});
test('with two sub-expressions', () => {
const node = nodeTypes.function.buildNode('and', [
childNode1,
childNode2,
]) as KqlAndFunctionNode;
const result = and.toKqlExpression(node);
expect(result).toBe('(machine.os: osx AND extension: jpg)');
});
});
});
});

View file

@ -6,10 +6,23 @@
* Side Public License, v 1.
*/
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import * as ast from '../ast';
import type { DataViewBase, KueryNode, KueryQueryOptions } from '../../..';
import type { KqlFunctionNode } from '../node_types';
import type { KqlContext } from '../types';
export const KQL_FUNCTION_AND = 'and';
export interface KqlAndFunctionNode extends KqlFunctionNode {
function: typeof KQL_FUNCTION_AND;
arguments: KqlFunctionNode[];
}
export function isNode(node: KqlFunctionNode): node is KqlAndFunctionNode {
return node.function === KQL_FUNCTION_AND;
}
export function buildNodeParams(children: KueryNode[]) {
return {
arguments: children,
@ -17,11 +30,11 @@ export function buildNodeParams(children: KueryNode[]) {
}
export function toElasticsearchQuery(
node: KueryNode,
node: KqlAndFunctionNode,
indexPattern?: DataViewBase,
config: KueryQueryOptions = {},
context: KqlContext = {}
) {
): QueryDslQueryContainer {
const { filtersInMustClause } = config;
const children = node.arguments || [];
const key = filtersInMustClause ? 'must' : 'filter';
@ -34,3 +47,7 @@ export function toElasticsearchQuery(
},
};
}
export function toKqlExpression(node: KqlAndFunctionNode): string {
return `(${node.arguments.map(ast.toKqlExpression).join(' AND ')})`;
}

View file

@ -10,12 +10,11 @@ import { nodeTypes } from '../node_types';
import { fields } from '../../filters/stubs';
import { DataViewBase } from '../../..';
import { KQL_NODE_TYPE_LITERAL } from '../node_types/literal';
import * as exists from './exists';
import type { KqlExistsFunctionNode } from './exists';
jest.mock('../grammar');
// @ts-ignore
import * as exists from './exists';
describe('kuery functions', () => {
describe('exists', () => {
let indexPattern: DataViewBase;
@ -50,7 +49,10 @@ describe('kuery functions', () => {
const expected = {
exists: { field: 'response' },
};
const existsNode = nodeTypes.function.buildNode('exists', 'response');
const existsNode = nodeTypes.function.buildNode(
'exists',
'response'
) as KqlExistsFunctionNode;
const result = exists.toElasticsearchQuery(existsNode, indexPattern);
expect(expected).toEqual(result);
@ -60,14 +62,20 @@ describe('kuery functions', () => {
const expected = {
exists: { field: 'response' },
};
const existsNode = nodeTypes.function.buildNode('exists', 'response');
const existsNode = nodeTypes.function.buildNode(
'exists',
'response'
) as KqlExistsFunctionNode;
const result = exists.toElasticsearchQuery(existsNode);
expect(expected).toEqual(result);
});
test('should throw an error for scripted fields', () => {
const existsNode = nodeTypes.function.buildNode('exists', 'script string');
const existsNode = nodeTypes.function.buildNode(
'exists',
'script string'
) as KqlExistsFunctionNode;
expect(() => exists.toElasticsearchQuery(existsNode, indexPattern)).toThrowError(
/Exists query does not support scripted fields/
);
@ -77,7 +85,10 @@ describe('kuery functions', () => {
const expected = {
exists: { field: 'nestedField.response' },
};
const existsNode = nodeTypes.function.buildNode('exists', 'response');
const existsNode = nodeTypes.function.buildNode(
'exists',
'response'
) as KqlExistsFunctionNode;
const result = exists.toElasticsearchQuery(
existsNode,
indexPattern,
@ -88,5 +99,16 @@ describe('kuery functions', () => {
expect(expected).toEqual(result);
});
});
describe('toKqlExpression', () => {
test('should return a KQL expression', () => {
const existsNode = nodeTypes.function.buildNode(
'exists',
'response'
) as KqlExistsFunctionNode;
const result = exists.toKqlExpression(existsNode);
expect(result).toBe('response: *');
});
});
});
});

View file

@ -6,23 +6,39 @@
* Side Public License, v 1.
*/
import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { DataViewFieldBase, DataViewBase, KueryNode, KueryQueryOptions } from '../../..';
import * as literal from '../node_types/literal';
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { DataViewFieldBase, DataViewBase, KueryQueryOptions } from '../../..';
import type { KqlFunctionNode, KqlLiteralNode } from '../node_types';
import type { KqlContext } from '../types';
import {
buildNode as buildLiteralNode,
toElasticsearchQuery as literalToElasticsearchQuery,
toKqlExpression as literalToKqlExpression,
} from '../node_types/literal';
export const KQL_FUNCTION_EXISTS = 'exists';
export interface KqlExistsFunctionNode extends KqlFunctionNode {
function: typeof KQL_FUNCTION_EXISTS;
arguments: [KqlLiteralNode];
}
export function isNode(node: KqlFunctionNode): node is KqlExistsFunctionNode {
return node.function === KQL_FUNCTION_EXISTS;
}
export function buildNodeParams(fieldName: string) {
return {
arguments: [literal.buildNode(fieldName)],
arguments: [buildLiteralNode(fieldName)],
};
}
export function toElasticsearchQuery(
node: KueryNode,
node: KqlExistsFunctionNode,
indexPattern?: DataViewBase,
config: KueryQueryOptions = {},
context: KqlContext = {}
): estypes.QueryDslQueryContainer {
): QueryDslQueryContainer {
const {
arguments: [fieldNameArg],
} = node;
@ -30,7 +46,7 @@ export function toElasticsearchQuery(
...fieldNameArg,
value: context?.nested ? `${context.nested.path}.${fieldNameArg.value}` : fieldNameArg.value,
};
const fieldName = literal.toElasticsearchQuery(fullFieldNameArg) as string;
const fieldName = literalToElasticsearchQuery(fullFieldNameArg) as string;
const field = indexPattern?.fields?.find((fld: DataViewFieldBase) => fld.name === fieldName);
if (field?.scripted) {
@ -40,3 +56,8 @@ export function toElasticsearchQuery(
exists: { field: fieldName },
};
}
export function toKqlExpression(node: KqlExistsFunctionNode): string {
const [field] = node.arguments;
return `${literalToKqlExpression(field)}: *`;
}

View file

@ -14,6 +14,14 @@ import * as range from './range';
import * as exists from './exists';
import * as nested from './nested';
export { KQL_FUNCTION_AND } from './and';
export { KQL_FUNCTION_EXISTS } from './exists';
export { KQL_FUNCTION_IS } from './is';
export { KQL_FUNCTION_NESTED } from './nested';
export { KQL_FUNCTION_NOT } from './not';
export { KQL_FUNCTION_OR } from './or';
export { KQL_FUNCTION_RANGE } from './range';
export const functions = {
is,
and,

View file

@ -14,6 +14,7 @@ import { DataViewBase } from '../../..';
import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { KQL_NODE_TYPE_WILDCARD } from '../node_types/wildcard';
import { KQL_NODE_TYPE_LITERAL } from '../node_types/literal';
import { KqlIsFunctionNode } from './is';
jest.mock('../grammar');
@ -69,7 +70,7 @@ describe('kuery functions', () => {
const expected = {
match_all: {},
};
const node = nodeTypes.function.buildNode('is', '*', '*');
const node = nodeTypes.function.buildNode('is', '*', '*') as KqlIsFunctionNode;
const result = is.toElasticsearchQuery(node, indexPattern);
expect(result).toEqual(expected);
@ -79,7 +80,7 @@ describe('kuery functions', () => {
const expected = {
match_all: {},
};
const node = nodeTypes.function.buildNode('is', 'n*', '*');
const node = nodeTypes.function.buildNode('is', 'n*', '*') as KqlIsFunctionNode;
const result = is.toElasticsearchQuery(node, {
...indexPattern,
fields: indexPattern.fields.filter((field) => field.name.startsWith('n')),
@ -92,7 +93,7 @@ describe('kuery functions', () => {
const expected = {
match_all: {},
};
const node = nodeTypes.function.buildNode('is', '*', '*');
const node = nodeTypes.function.buildNode('is', '*', '*') as KqlIsFunctionNode;
const result = is.toElasticsearchQuery(node);
expect(result).toEqual(expected);
@ -106,7 +107,7 @@ describe('kuery functions', () => {
lenient: true,
},
};
const node = nodeTypes.function.buildNode('is', null, 200);
const node = nodeTypes.function.buildNode('is', null, 200) as KqlIsFunctionNode;
const result = is.toElasticsearchQuery(node, indexPattern);
expect(result).toEqual(expected);
@ -118,14 +119,14 @@ describe('kuery functions', () => {
query: 'jpg*',
},
};
const node = nodeTypes.function.buildNode('is', null, 'jpg*');
const node = nodeTypes.function.buildNode('is', null, 'jpg*') as KqlIsFunctionNode;
const result = is.toElasticsearchQuery(node, indexPattern);
expect(result).toEqual(expected);
});
test('should return an ES bool query with a sub-query for each field when fieldName is "*"', () => {
const node = nodeTypes.function.buildNode('is', '*', 200);
const node = nodeTypes.function.buildNode('is', '*', 200) as KqlIsFunctionNode;
const result = is.toElasticsearchQuery(node, indexPattern);
expect(result).toHaveProperty('bool');
@ -141,7 +142,7 @@ describe('kuery functions', () => {
minimum_should_match: 1,
},
};
const node = nodeTypes.function.buildNode('is', 'extension', '*');
const node = nodeTypes.function.buildNode('is', 'extension', '*') as KqlIsFunctionNode;
const result = is.toElasticsearchQuery(node, indexPattern);
expect(result).toEqual(expected);
@ -154,7 +155,7 @@ describe('kuery functions', () => {
minimum_should_match: 1,
},
};
const node = nodeTypes.function.buildNode('is', 'extension', 'jpg');
const node = nodeTypes.function.buildNode('is', 'extension', 'jpg') as KqlIsFunctionNode;
const result = is.toElasticsearchQuery(node, indexPattern);
expect(result).toEqual(expected);
@ -167,7 +168,7 @@ describe('kuery functions', () => {
minimum_should_match: 1,
},
};
const node = nodeTypes.function.buildNode('is', 'extension', 'jpg');
const node = nodeTypes.function.buildNode('is', 'extension', 'jpg') as KqlIsFunctionNode;
const result = is.toElasticsearchQuery(node);
expect(result).toEqual(expected);
@ -180,7 +181,7 @@ describe('kuery functions', () => {
minimum_should_match: 1,
},
};
const node = nodeTypes.function.buildNode('is', 'extension', '"jpg"');
const node = nodeTypes.function.buildNode('is', 'extension', '"jpg"') as KqlIsFunctionNode;
const result = is.toElasticsearchQuery(node, indexPattern);
expect(result).toEqual(expected);
@ -200,7 +201,7 @@ describe('kuery functions', () => {
minimum_should_match: 1,
},
};
const node = nodeTypes.function.buildNode('is', 'extension', 'jpg*');
const node = nodeTypes.function.buildNode('is', 'extension', 'jpg*') as KqlIsFunctionNode;
const result = is.toElasticsearchQuery(node, indexPattern);
expect(result).toEqual(expected);
@ -219,7 +220,11 @@ describe('kuery functions', () => {
minimum_should_match: 1,
},
};
const node = nodeTypes.function.buildNode('is', 'machine.os.keyword', 'win*');
const node = nodeTypes.function.buildNode(
'is',
'machine.os.keyword',
'win*'
) as KqlIsFunctionNode;
const result = is.toElasticsearchQuery(node, indexPattern);
expect(result).toEqual(expected);
@ -238,14 +243,22 @@ describe('kuery functions', () => {
minimum_should_match: 1,
},
};
const node = nodeTypes.function.buildNode('is', 'machine.os.keyword', 'win*');
const node = nodeTypes.function.buildNode(
'is',
'machine.os.keyword',
'win*'
) as KqlIsFunctionNode;
const result = is.toElasticsearchQuery(node, indexPattern, { caseInsensitive: true });
expect(result).toEqual(expected);
});
test('should support scripted fields', () => {
const node = nodeTypes.function.buildNode('is', 'script string', 'foo');
const node = nodeTypes.function.buildNode(
'is',
'script string',
'foo'
) as KqlIsFunctionNode;
const result = is.toElasticsearchQuery(node, indexPattern);
expect((result.bool!.should as estypes.QueryDslQueryContainer[])[0]).toHaveProperty(
@ -269,7 +282,11 @@ describe('kuery functions', () => {
minimum_should_match: 1,
},
};
const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"');
const node = nodeTypes.function.buildNode(
'is',
'@timestamp',
'"2018-04-03T19:04:17"'
) as KqlIsFunctionNode;
const result = is.toElasticsearchQuery(node, indexPattern);
expect(result).toEqual(expected);
@ -293,7 +310,11 @@ describe('kuery functions', () => {
minimum_should_match: 1,
},
};
const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"');
const node = nodeTypes.function.buildNode(
'is',
'@timestamp',
'"2018-04-03T19:04:17"'
) as KqlIsFunctionNode;
const result = is.toElasticsearchQuery(node, indexPattern, config);
expect(result).toEqual(expected);
@ -306,7 +327,7 @@ describe('kuery functions', () => {
minimum_should_match: 1,
},
};
const node = nodeTypes.function.buildNode('is', 'extension', 'jpg');
const node = nodeTypes.function.buildNode('is', 'extension', 'jpg') as KqlIsFunctionNode;
const result = is.toElasticsearchQuery(
node,
indexPattern,
@ -324,7 +345,7 @@ describe('kuery functions', () => {
minimum_should_match: 1,
},
};
const node = nodeTypes.function.buildNode('is', 'ext*', 'jpg');
const node = nodeTypes.function.buildNode('is', 'ext*', 'jpg') as KqlIsFunctionNode;
const result = is.toElasticsearchQuery(node, indexPattern);
expect(result).toEqual(expected);
@ -349,7 +370,11 @@ describe('kuery functions', () => {
minimum_should_match: 1,
},
};
const node = nodeTypes.function.buildNode('is', '*doublyNested*', 'foo');
const node = nodeTypes.function.buildNode(
'is',
'*doublyNested*',
'foo'
) as KqlIsFunctionNode;
const result = is.toElasticsearchQuery(node, indexPattern);
expect(result).toEqual(expected);
@ -375,14 +400,22 @@ describe('kuery functions', () => {
minimum_should_match: 1,
},
};
const node = nodeTypes.function.buildNode('is', '*doublyNested*', 'foo');
const node = nodeTypes.function.buildNode(
'is',
'*doublyNested*',
'foo'
) as KqlIsFunctionNode;
const result = is.toElasticsearchQuery(node, indexPattern, { nestedIgnoreUnmapped: true });
expect(result).toEqual(expected);
});
test('should use a term query for keyword fields', () => {
const node = nodeTypes.function.buildNode('is', 'machine.os.keyword', 'Win 7');
const node = nodeTypes.function.buildNode(
'is',
'machine.os.keyword',
'Win 7'
) as KqlIsFunctionNode;
const result = is.toElasticsearchQuery(node, indexPattern);
expect(result).toEqual({
bool: {
@ -399,7 +432,11 @@ describe('kuery functions', () => {
});
test('should use a case-insensitive term query for keyword fields', () => {
const node = nodeTypes.function.buildNode('is', 'machine.os.keyword', 'Win 7');
const node = nodeTypes.function.buildNode(
'is',
'machine.os.keyword',
'Win 7'
) as KqlIsFunctionNode;
const result = is.toElasticsearchQuery(node, indexPattern, { caseInsensitive: true });
expect(result).toEqual({
bool: {
@ -415,5 +452,71 @@ describe('kuery functions', () => {
});
});
});
describe('toKqlExpression', () => {
test('match all fields and all values', () => {
const node = nodeTypes.function.buildNode('is', '*', '*') as KqlIsFunctionNode;
const result = is.toKqlExpression(node);
expect(result).toMatchInlineSnapshot(`"*: *"`);
});
test('no field with literal value', () => {
const node = nodeTypes.function.buildNode('is', null, 200) as KqlIsFunctionNode;
const result = is.toKqlExpression(node);
expect(result).toMatchInlineSnapshot(`"200"`);
});
test('no field with wildcard value', () => {
const node = nodeTypes.function.buildNode('is', null, 'jpg*') as KqlIsFunctionNode;
const result = is.toKqlExpression(node);
expect(result).toMatchInlineSnapshot(`"jpg*"`);
});
test('match all fields with value', () => {
const node = nodeTypes.function.buildNode('is', '*', 200) as KqlIsFunctionNode;
const result = is.toKqlExpression(node);
expect(result).toMatchInlineSnapshot(`"*: 200"`);
});
test('field with match all value"', () => {
const node = nodeTypes.function.buildNode('is', 'extension', '*') as KqlIsFunctionNode;
const result = is.toKqlExpression(node);
expect(result).toMatchInlineSnapshot(`"extension: *"`);
});
test('field with value', () => {
const node = nodeTypes.function.buildNode('is', 'extension', 'jpg') as KqlIsFunctionNode;
const result = is.toKqlExpression(node);
expect(result).toMatchInlineSnapshot(`"extension: jpg"`);
});
test('field with phrase value', () => {
const node = nodeTypes.function.buildNode('is', 'extension', '"jpg"') as KqlIsFunctionNode;
const result = is.toKqlExpression(node);
expect(result).toMatchInlineSnapshot(`"extension: \\"jpg\\""`);
});
test('phrase field with phrase value', () => {
const node = nodeTypes.function.buildNode(
'is',
'"extension"',
'"jpg"'
) as KqlIsFunctionNode;
const result = is.toKqlExpression(node);
expect(result).toMatchInlineSnapshot(`"\\"extension\\": \\"jpg\\""`);
});
test('field with wildcard value', () => {
const node = nodeTypes.function.buildNode('is', 'extension', 'jpg*') as KqlIsFunctionNode;
const result = is.toKqlExpression(node);
expect(result).toMatchInlineSnapshot(`"extension: jpg*"`);
});
test('wildcard field with value', () => {
const node = nodeTypes.function.buildNode('is', 'ext*', 'jpg') as KqlIsFunctionNode;
const result = is.toKqlExpression(node);
expect(result).toMatchInlineSnapshot(`"ext*: jpg"`);
});
});
});
});

View file

@ -7,18 +7,30 @@
*/
import { isUndefined } from 'lodash';
import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { getPhraseScript } from '../../filters';
import { getFields } from './utils/get_fields';
import { getTimeZoneFromSettings, getDataViewFieldSubtypeNested } from '../../utils';
import { getFullFieldNameNode } from './utils/get_full_field_name_node';
import type { DataViewBase, KueryNode, DataViewFieldBase, KueryQueryOptions } from '../../..';
import type { DataViewBase, DataViewFieldBase, KueryQueryOptions } from '../../..';
import type { KqlFunctionNode, KqlLiteralNode, KqlWildcardNode } from '../node_types';
import type { KqlContext } from '../types';
import * as ast from '../ast';
import * as literal from '../node_types/literal';
import * as wildcard from '../node_types/wildcard';
export const KQL_FUNCTION_IS = 'is';
export interface KqlIsFunctionNode extends KqlFunctionNode {
function: typeof KQL_FUNCTION_IS;
arguments: [KqlLiteralNode | KqlWildcardNode, KqlLiteralNode | KqlWildcardNode];
}
export function isNode(node: KqlFunctionNode): node is KqlIsFunctionNode {
return node.function === KQL_FUNCTION_IS;
}
export function buildNodeParams(fieldName: string, value: any) {
if (isUndefined(fieldName)) {
throw new Error('fieldName is a required argument');
@ -38,11 +50,11 @@ export function buildNodeParams(fieldName: string, value: any) {
}
export function toElasticsearchQuery(
node: KueryNode,
node: KqlIsFunctionNode,
indexPattern?: DataViewBase,
config: KueryQueryOptions = {},
context: KqlContext = {}
): estypes.QueryDslQueryContainer {
): QueryDslQueryContainer {
const {
arguments: [fieldNameArg, valueArg],
} = node;
@ -216,3 +228,9 @@ export function toElasticsearchQuery(
},
};
}
export function toKqlExpression(node: KqlIsFunctionNode): string {
const [field, value] = node.arguments;
if (field.value === null) return `${ast.toKqlExpression(value)}`;
return `${ast.toKqlExpression(field)}: ${ast.toKqlExpression(value)}`;
}

View file

@ -9,10 +9,9 @@
import { nodeTypes } from '../node_types';
import { fields } from '../../filters/stubs';
import { DataViewBase } from '../../..';
import * as ast from '../ast';
import * as nested from './nested';
import type { KqlNestedFunctionNode } from './nested';
jest.mock('../grammar');
@ -43,7 +42,11 @@ describe('kuery functions', () => {
describe('toElasticsearchQuery', () => {
test('should wrap subqueries in an ES nested query', () => {
const node = nodeTypes.function.buildNode('nested', 'nestedField', childNode);
const node = nodeTypes.function.buildNode(
'nested',
'nestedField',
childNode
) as KqlNestedFunctionNode;
const result = nested.toElasticsearchQuery(node, indexPattern);
expect(result).toHaveProperty('nested');
@ -54,7 +57,11 @@ describe('kuery functions', () => {
});
test('should pass the nested path to subqueries so the full field name can be used', () => {
const node = nodeTypes.function.buildNode('nested', 'nestedField', childNode);
const node = nodeTypes.function.buildNode(
'nested',
'nestedField',
childNode
) as KqlNestedFunctionNode;
const result = nested.toElasticsearchQuery(node, indexPattern);
const expectedSubQuery = ast.toElasticsearchQuery(
nodeTypes.function.buildNode('is', 'nestedField.child', 'foo')
@ -63,5 +70,41 @@ describe('kuery functions', () => {
expect(result.nested!.query).toEqual(expectedSubQuery);
});
});
describe('toKqlExpression', () => {
test('single nested query', () => {
const node = nodeTypes.function.buildNode(
'nested',
'nestedField',
childNode
) as KqlNestedFunctionNode;
const result = nested.toKqlExpression(node);
expect(result).toMatchInlineSnapshot(`"nestedField: { child: foo }"`);
});
test('multiple nested queries', () => {
const andNode = nodeTypes.function.buildNode('and', [childNode, childNode]);
const node = nodeTypes.function.buildNode(
'nested',
'nestedField',
andNode
) as KqlNestedFunctionNode;
const result = nested.toKqlExpression(node);
expect(result).toMatchInlineSnapshot(`"nestedField: { (child: foo AND child: foo) }"`);
});
test('doubly nested query', () => {
const subNode = nodeTypes.function.buildNode('nested', 'anotherNestedField', childNode);
const node = nodeTypes.function.buildNode(
'nested',
'nestedField',
subNode
) as KqlNestedFunctionNode;
const result = nested.toKqlExpression(node);
expect(result).toMatchInlineSnapshot(
`"nestedField: { anotherNestedField: { child: foo } }"`
);
});
});
});
});

View file

@ -6,12 +6,24 @@
* Side Public License, v 1.
*/
import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import * as ast from '../ast';
import * as literal from '../node_types/literal';
import type { DataViewBase, KueryNode, KueryQueryOptions } from '../../..';
import type { DataViewBase, KueryQueryOptions } from '../../..';
import type { KqlFunctionNode, KqlLiteralNode } from '../node_types';
import type { KqlContext } from '../types';
export const KQL_FUNCTION_NESTED = 'nested';
export interface KqlNestedFunctionNode extends KqlFunctionNode {
function: typeof KQL_FUNCTION_NESTED;
arguments: [KqlLiteralNode, KqlFunctionNode];
}
export function isNode(node: KqlFunctionNode): node is KqlNestedFunctionNode {
return node.function === KQL_FUNCTION_NESTED;
}
export function buildNodeParams(path: any, child: any) {
const pathNode =
typeof path === 'string' ? ast.fromLiteralExpression(path) : literal.buildNode(path);
@ -21,11 +33,11 @@ export function buildNodeParams(path: any, child: any) {
}
export function toElasticsearchQuery(
node: KueryNode,
node: KqlNestedFunctionNode,
indexPattern?: DataViewBase,
config: KueryQueryOptions = {},
context: KqlContext = {}
): estypes.QueryDslQueryContainer {
): QueryDslQueryContainer {
const [path, child] = node.arguments;
const stringPath = ast.toElasticsearchQuery(path) as unknown as string;
const fullPath = context?.nested?.path ? `${context.nested.path}.${stringPath}` : stringPath;
@ -36,7 +48,7 @@ export function toElasticsearchQuery(
query: ast.toElasticsearchQuery(child, indexPattern, config, {
...context,
nested: { path: fullPath },
}) as estypes.QueryDslQueryContainer,
}),
score_mode: 'none',
...(typeof config.nestedIgnoreUnmapped === 'boolean' && {
ignore_unmapped: config.nestedIgnoreUnmapped,
@ -44,3 +56,8 @@ export function toElasticsearchQuery(
},
};
}
export function toKqlExpression(node: KqlNestedFunctionNode): string {
const [path, child] = node.arguments;
return `${literal.toKqlExpression(path)}: { ${ast.toKqlExpression(child)} }`;
}

View file

@ -12,6 +12,7 @@ import { DataViewBase } from '../../..';
import * as ast from '../ast';
import * as not from './not';
import { KqlNotFunctionNode } from './not';
jest.mock('../grammar');
@ -40,7 +41,7 @@ describe('kuery functions', () => {
describe('toElasticsearchQuery', () => {
test("should wrap a subquery in an ES bool query's must_not clause", () => {
const node = nodeTypes.function.buildNode('not', childNode);
const node = nodeTypes.function.buildNode('not', childNode) as KqlNotFunctionNode;
const result = not.toElasticsearchQuery(node, indexPattern);
expect(result).toHaveProperty('bool');
@ -52,5 +53,13 @@ describe('kuery functions', () => {
expect(result.bool!.must_not).toEqual(ast.toElasticsearchQuery(childNode, indexPattern));
});
});
describe('toKqlExpression', () => {
test('with one sub-expression', () => {
const node = nodeTypes.function.buildNode('not', childNode) as KqlNotFunctionNode;
const result = not.toKqlExpression(node);
expect(result).toBe('NOT extension: jpg');
});
});
});
});

View file

@ -6,11 +6,23 @@
* Side Public License, v 1.
*/
import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import * as ast from '../ast';
import type { DataViewBase, KueryNode, KueryQueryOptions } from '../../..';
import type { KqlFunctionNode } from '../node_types';
import type { KqlContext } from '../types';
export const KQL_FUNCTION_NOT = 'not';
export interface KqlNotFunctionNode extends KqlFunctionNode {
function: typeof KQL_FUNCTION_NOT;
arguments: [KqlFunctionNode];
}
export function isNode(node: KqlFunctionNode): node is KqlNotFunctionNode {
return node.function === KQL_FUNCTION_NOT;
}
export function buildNodeParams(child: KueryNode) {
return {
arguments: [child],
@ -18,21 +30,21 @@ export function buildNodeParams(child: KueryNode) {
}
export function toElasticsearchQuery(
node: KueryNode,
node: KqlNotFunctionNode,
indexPattern?: DataViewBase,
config: KueryQueryOptions = {},
context: KqlContext = {}
): estypes.QueryDslQueryContainer {
): QueryDslQueryContainer {
const [argument] = node.arguments;
return {
bool: {
must_not: ast.toElasticsearchQuery(
argument,
indexPattern,
config,
context
) as estypes.QueryDslQueryContainer,
must_not: ast.toElasticsearchQuery(argument, indexPattern, config, context),
},
};
}
export function toKqlExpression(node: KqlNotFunctionNode): string {
const [child] = node.arguments;
return `NOT ${ast.toKqlExpression(child)}`;
}

View file

@ -13,6 +13,7 @@ import { DataViewBase } from '../../..';
import * as ast from '../ast';
import * as or from './or';
import { KqlOrFunctionNode } from './or';
jest.mock('../grammar');
const childNode1 = nodeTypes.function.buildNode('is', 'machine.os', 'osx');
@ -43,7 +44,10 @@ describe('kuery functions', () => {
describe('toElasticsearchQuery', () => {
test("should wrap subqueries in an ES bool query's should clause", () => {
const node = nodeTypes.function.buildNode('or', [childNode1, childNode2]);
const node = nodeTypes.function.buildNode('or', [
childNode1,
childNode2,
]) as KqlOrFunctionNode;
const result = or.toElasticsearchQuery(node, indexPattern);
expect(result).toHaveProperty('bool');
@ -57,11 +61,31 @@ describe('kuery functions', () => {
});
test('should require one of the clauses to match', () => {
const node = nodeTypes.function.buildNode('or', [childNode1, childNode2]);
const node = nodeTypes.function.buildNode('or', [
childNode1,
childNode2,
]) as KqlOrFunctionNode;
const result = or.toElasticsearchQuery(node, indexPattern);
expect(result.bool).toHaveProperty('minimum_should_match', 1);
});
});
describe('toKqlExpression', () => {
test('with one sub-expression', () => {
const node = nodeTypes.function.buildNode('or', [childNode1]) as KqlOrFunctionNode;
const result = or.toKqlExpression(node);
expect(result).toBe('(machine.os: osx)');
});
test('with two sub-expressions', () => {
const node = nodeTypes.function.buildNode('or', [
childNode1,
childNode2,
]) as KqlOrFunctionNode;
const result = or.toKqlExpression(node);
expect(result).toBe('(machine.os: osx OR extension: jpg)');
});
});
});
});

View file

@ -6,11 +6,23 @@
* Side Public License, v 1.
*/
import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import * as ast from '../ast';
import type { DataViewBase, KueryNode, KueryQueryOptions } from '../../..';
import type { KqlFunctionNode } from '../node_types';
import type { KqlContext } from '../types';
export const KQL_FUNCTION_OR = 'or';
export interface KqlOrFunctionNode extends KqlFunctionNode {
function: typeof KQL_FUNCTION_OR;
arguments: KqlFunctionNode[];
}
export function isNode(node: KqlFunctionNode): node is KqlOrFunctionNode {
return node.function === KQL_FUNCTION_OR;
}
export function buildNodeParams(children: KueryNode[]) {
return {
arguments: children,
@ -18,11 +30,11 @@ export function buildNodeParams(children: KueryNode[]) {
}
export function toElasticsearchQuery(
node: KueryNode,
node: KqlOrFunctionNode,
indexPattern?: DataViewBase,
config: KueryQueryOptions = {},
context: KqlContext = {}
): estypes.QueryDslQueryContainer {
): QueryDslQueryContainer {
const children = node.arguments || [];
return {
@ -34,3 +46,7 @@ export function toElasticsearchQuery(
},
};
}
export function toKqlExpression(node: KqlOrFunctionNode): string {
return `(${node.arguments.map(ast.toKqlExpression).join(' OR ')})`;
}

View file

@ -13,6 +13,7 @@ import { DataViewBase } from '../../..';
import * as range from './range';
import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { KQL_NODE_TYPE_LITERAL } from '../node_types/literal';
import { KqlRangeFunctionNode } from './range';
jest.mock('../grammar');
@ -65,7 +66,12 @@ describe('kuery functions', () => {
minimum_should_match: 1,
},
};
const node = nodeTypes.function.buildNode('range', 'bytes', 'gt', 1000);
const node = nodeTypes.function.buildNode(
'range',
'bytes',
'gt',
1000
) as KqlRangeFunctionNode;
const result = range.toElasticsearchQuery(node, indexPattern);
expect(result).toEqual(expected);
@ -87,7 +93,12 @@ describe('kuery functions', () => {
},
};
const node = nodeTypes.function.buildNode('range', 'bytes', 'gt', 1000);
const node = nodeTypes.function.buildNode(
'range',
'bytes',
'gt',
1000
) as KqlRangeFunctionNode;
const result = range.toElasticsearchQuery(node);
expect(result).toEqual(expected);
@ -109,14 +120,24 @@ describe('kuery functions', () => {
},
};
const node = nodeTypes.function.buildNode('range', 'byt*', 'gt', 1000);
const node = nodeTypes.function.buildNode(
'range',
'byt*',
'gt',
1000
) as KqlRangeFunctionNode;
const result = range.toElasticsearchQuery(node, indexPattern);
expect(result).toEqual(expected);
});
test('should support scripted fields', () => {
const node = nodeTypes.function.buildNode('range', 'script number', 'gt', 1000);
const node = nodeTypes.function.buildNode(
'range',
'script number',
'gt',
1000
) as KqlRangeFunctionNode;
const result = range.toElasticsearchQuery(node, indexPattern);
expect((result.bool!.should as estypes.QueryDslQueryContainer[])[0]).toHaveProperty(
@ -144,7 +165,7 @@ describe('kuery functions', () => {
'@timestamp',
'gt',
'2018-01-03T19:04:17'
);
) as KqlRangeFunctionNode;
const result = range.toElasticsearchQuery(node, indexPattern);
expect(result).toEqual(expected);
@ -172,7 +193,7 @@ describe('kuery functions', () => {
'@timestamp',
'gt',
'2018-01-03T19:04:17'
);
) as KqlRangeFunctionNode;
const result = range.toElasticsearchQuery(node, indexPattern, config);
expect(result).toEqual(expected);
@ -193,7 +214,12 @@ describe('kuery functions', () => {
minimum_should_match: 1,
},
};
const node = nodeTypes.function.buildNode('range', 'bytes', 'gt', 1000);
const node = nodeTypes.function.buildNode(
'range',
'bytes',
'gt',
1000
) as KqlRangeFunctionNode;
const result = range.toElasticsearchQuery(
node,
indexPattern,
@ -225,7 +251,12 @@ describe('kuery functions', () => {
minimum_should_match: 1,
},
};
const node = nodeTypes.function.buildNode('range', '*doublyNested*', 'lt', 8000);
const node = nodeTypes.function.buildNode(
'range',
'*doublyNested*',
'lt',
8000
) as KqlRangeFunctionNode;
const result = range.toElasticsearchQuery(node, indexPattern);
expect(result).toEqual(expected);
@ -253,7 +284,12 @@ describe('kuery functions', () => {
minimum_should_match: 1,
},
};
const node = nodeTypes.function.buildNode('range', '*doublyNested*', 'lt', 8000);
const node = nodeTypes.function.buildNode(
'range',
'*doublyNested*',
'lt',
8000
) as KqlRangeFunctionNode;
const result = range.toElasticsearchQuery(node, indexPattern, {
nestedIgnoreUnmapped: true,
});
@ -261,5 +297,75 @@ describe('kuery functions', () => {
expect(result).toEqual(expected);
});
});
describe('toKqlExpression', () => {
describe('operators', () => {
test('gt', () => {
const node = nodeTypes.function.buildNode(
'range',
'bytes',
'gt',
1000
) as KqlRangeFunctionNode;
const result = range.toKqlExpression(node);
expect(result).toMatchInlineSnapshot(`"bytes > 1000"`);
});
test('gte', () => {
const node = nodeTypes.function.buildNode(
'range',
'bytes',
'gte',
1000
) as KqlRangeFunctionNode;
const result = range.toKqlExpression(node);
expect(result).toMatchInlineSnapshot(`"bytes >= 1000"`);
});
test('lt', () => {
const node = nodeTypes.function.buildNode(
'range',
'bytes',
'lt',
1000
) as KqlRangeFunctionNode;
const result = range.toKqlExpression(node);
expect(result).toMatchInlineSnapshot(`"bytes < 1000"`);
});
test('lte', () => {
const node = nodeTypes.function.buildNode(
'range',
'bytes',
'lte',
1000
) as KqlRangeFunctionNode;
const result = range.toKqlExpression(node);
expect(result).toMatchInlineSnapshot(`"bytes <= 1000"`);
});
});
test('with wildcard field & literal value', () => {
const node = nodeTypes.function.buildNode(
'range',
'byt*',
'gt',
1000
) as KqlRangeFunctionNode;
const result = range.toKqlExpression(node);
expect(result).toMatchInlineSnapshot(`"byt* > 1000"`);
});
test('with wildcard field & wildcard value', () => {
const node = nodeTypes.function.buildNode(
'range',
'byt*',
'gt',
'100*'
) as KqlRangeFunctionNode;
const result = range.toKqlExpression(node);
expect(result).toMatchInlineSnapshot(`"byt* > 100*"`);
});
});
});
});

View file

@ -6,16 +6,38 @@
* Side Public License, v 1.
*/
import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { nodeTypes } from '../node_types';
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { buildNode as buildLiteralNode } from '../node_types/literal';
import { type KqlFunctionNode, type KqlLiteralNode, nodeTypes } from '../node_types';
import * as ast from '../ast';
import { getRangeScript, RangeFilterParams } from '../../filters';
import { getFields } from './utils/get_fields';
import { getDataViewFieldSubtypeNested, getTimeZoneFromSettings } from '../../utils';
import { getFullFieldNameNode } from './utils/get_full_field_name_node';
import type { DataViewBase, KueryNode, KueryQueryOptions } from '../../..';
import type { DataViewBase, KueryQueryOptions } from '../../..';
import type { KqlContext } from '../types';
export const KQL_FUNCTION_RANGE = 'range';
export const KQL_RANGE_OPERATOR_MAP = {
gt: '>',
gte: '>=',
lt: '<',
lte: '<=',
};
export interface KqlRangeFunctionNode extends KqlFunctionNode {
function: typeof KQL_FUNCTION_RANGE;
arguments: [
KqlLiteralNode,
keyof Pick<RangeFilterParams, 'gt' | 'gte' | 'lt' | 'lte'>,
KqlLiteralNode
];
}
export function isNode(node: KqlFunctionNode): node is KqlRangeFunctionNode {
return node.function === KQL_FUNCTION_RANGE;
}
export function buildNodeParams(
fieldName: string,
operator: keyof Pick<RangeFilterParams, 'gt' | 'gte' | 'lt' | 'lte'>,
@ -23,16 +45,16 @@ export function buildNodeParams(
) {
// Run through the parser instead treating it as a literal because it may contain wildcards
const fieldNameArg = ast.fromLiteralExpression(fieldName);
const valueArg = nodeTypes.literal.buildNode(value);
const valueArg = buildLiteralNode(value);
return { arguments: [fieldNameArg, operator, valueArg] };
}
export function toElasticsearchQuery(
node: KueryNode,
node: KqlRangeFunctionNode,
indexPattern?: DataViewBase,
config: KueryQueryOptions = {},
context: KqlContext = {}
): estypes.QueryDslQueryContainer {
): QueryDslQueryContainer {
const [fieldNameArg, operatorArg, valueArg] = node.arguments;
const fullFieldNameArg = getFullFieldNameNode(
fieldNameArg,
@ -114,3 +136,10 @@ export function toElasticsearchQuery(
},
};
}
export function toKqlExpression(node: KqlRangeFunctionNode): string {
const [field, operator, value] = node.arguments;
return `${ast.toKqlExpression(field)} ${KQL_RANGE_OPERATOR_MAP[operator]} ${ast.toKqlExpression(
value
)}`;
}

View file

@ -22,7 +22,6 @@ export const toElasticsearchQuery = (...params: Parameters<typeof astToElasticse
export { KQLSyntaxError } from './kuery_syntax_error';
export { nodeTypes, nodeBuilder } from './node_types';
export { fromKueryExpression } from './ast';
export { fromKueryExpression, toKqlExpression } from './ast';
export { escapeKuery } from './utils';
export type { FunctionTypeBuildNode } from './node_types';
export type { DslQuery, KueryNode, KueryQueryOptions, KueryParseOptions } from './types';

View file

@ -8,8 +8,16 @@
import { nodeTypes } from '.';
import { buildNode, buildNodeWithArgumentNodes, toElasticsearchQuery } from './function';
import { toElasticsearchQuery as isFunctionToElasticsearchQuery } from '../functions/is';
import {
buildNode,
buildNodeWithArgumentNodes,
toElasticsearchQuery,
toKqlExpression,
} from './function';
import {
KqlIsFunctionNode,
toElasticsearchQuery as isFunctionToElasticsearchQuery,
} from '../functions/is';
import { DataViewBase } from '../../es_query';
import { fields } from '../../filters/stubs/fields.mocks';
@ -53,12 +61,20 @@ describe('kuery node types', () => {
describe('toElasticsearchQuery', () => {
test("should return the given function type's ES query representation", () => {
const node = buildNode('is', 'extension', 'jpg');
const node = buildNode('is', 'extension', 'jpg') as KqlIsFunctionNode;
const expected = isFunctionToElasticsearchQuery(node, indexPattern);
const result = toElasticsearchQuery(node, indexPattern);
expect(expected).toEqual(result);
});
});
describe('toKqlExpression', () => {
test("should return the given function type's KQL representation", () => {
const node = buildNode('is', 'extension', 'jpg') as KqlIsFunctionNode;
const result = toKqlExpression(node);
expect(result).toEqual('extension: jpg');
});
});
});
});

View file

@ -8,19 +8,48 @@
import _ from 'lodash';
import { functions } from '../functions';
import {
functions,
KQL_FUNCTION_AND,
KQL_FUNCTION_EXISTS,
KQL_FUNCTION_NESTED,
KQL_FUNCTION_IS,
KQL_FUNCTION_NOT,
KQL_FUNCTION_OR,
KQL_FUNCTION_RANGE,
} from '../functions';
import type { DataViewBase, KueryNode, KueryQueryOptions } from '../../..';
import type { FunctionName, FunctionTypeBuildNode } from './types';
import type { KqlContext } from '../types';
export function buildNode(functionName: FunctionName, ...args: any[]) {
export const KQL_NODE_TYPE_FUNCTION = 'function';
export type KqlFunctionName =
| typeof KQL_FUNCTION_AND
| typeof KQL_FUNCTION_EXISTS
| typeof KQL_FUNCTION_IS
| typeof KQL_FUNCTION_NESTED
| typeof KQL_FUNCTION_NOT
| typeof KQL_FUNCTION_OR
| typeof KQL_FUNCTION_RANGE;
export interface KqlFunctionNode extends KueryNode {
arguments: unknown[];
function: KqlFunctionName;
type: typeof KQL_NODE_TYPE_FUNCTION;
}
export function isNode(node: KueryNode): node is KqlFunctionNode {
return node.type === KQL_NODE_TYPE_FUNCTION;
}
export function buildNode(functionName: KqlFunctionName, ...args: any[]): KqlFunctionNode {
const kueryFunction = functions[functionName];
if (_.isUndefined(kueryFunction)) {
throw new Error(`Unknown function "${functionName}"`);
}
return {
type: 'function' as 'function',
type: KQL_NODE_TYPE_FUNCTION,
function: functionName,
// This requires better typing of the different typings and their return types.
// @ts-ignore
@ -30,26 +59,50 @@ export function buildNode(functionName: FunctionName, ...args: any[]) {
// Mainly only useful in the grammar where we'll already have real argument nodes in hand
export function buildNodeWithArgumentNodes(
functionName: FunctionName,
functionName: KqlFunctionName,
args: any[]
): FunctionTypeBuildNode {
): KqlFunctionNode {
if (_.isUndefined(functions[functionName])) {
throw new Error(`Unknown function "${functionName}"`);
}
return {
type: 'function',
type: KQL_NODE_TYPE_FUNCTION,
function: functionName,
arguments: args,
};
}
export function toElasticsearchQuery(
node: KueryNode,
node: KqlFunctionNode,
indexPattern?: DataViewBase,
config?: KueryQueryOptions,
context?: KqlContext
) {
const kueryFunction = functions[node.function as FunctionName];
return kueryFunction.toElasticsearchQuery(node, indexPattern, config, context);
if (functions.and.isNode(node))
return functions.and.toElasticsearchQuery(node, indexPattern, config, context);
if (functions.exists.isNode(node))
return functions.exists.toElasticsearchQuery(node), indexPattern, config, context;
if (functions.is.isNode(node))
return functions.is.toElasticsearchQuery(node, indexPattern, config, context);
if (functions.nested.isNode(node))
return functions.nested.toElasticsearchQuery(node, indexPattern, config, context);
if (functions.not.isNode(node))
return functions.not.toElasticsearchQuery(node, indexPattern, config, context);
if (functions.or.isNode(node))
return functions.or.toElasticsearchQuery(node, indexPattern, config, context);
if (functions.range.isNode(node))
return functions.range.toElasticsearchQuery(node, indexPattern, config, context);
throw new Error(`Unknown KQL function: "${node.function}"`);
}
export function toKqlExpression(node: KqlFunctionNode): string {
if (functions.and.isNode(node)) return functions.and.toKqlExpression(node);
if (functions.exists.isNode(node)) return functions.exists.toKqlExpression(node);
if (functions.is.isNode(node)) return functions.is.toKqlExpression(node);
if (functions.nested.isNode(node)) return functions.nested.toKqlExpression(node);
if (functions.not.isNode(node)) return functions.not.toKqlExpression(node);
if (functions.or.isNode(node)) return functions.or.toKqlExpression(node);
if (functions.range.isNode(node)) return functions.range.toKqlExpression(node);
throw new Error(`Unknown KQL function: "${node.function}"`);
}

View file

@ -9,11 +9,13 @@
import * as functionType from './function';
import * as literal from './literal';
import * as wildcard from './wildcard';
import { FunctionTypeBuildNode } from './types';
export type { FunctionTypeBuildNode };
export { nodeBuilder } from './node_builder';
export { type KqlFunctionNode, KQL_NODE_TYPE_FUNCTION } from './function';
export { type KqlLiteralNode, KQL_NODE_TYPE_LITERAL } from './literal';
export { type KqlWildcardNode, KQL_NODE_TYPE_WILDCARD } from './wildcard';
/**
* @public
*/

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { buildNode, KQL_NODE_TYPE_LITERAL, toElasticsearchQuery } from './literal';
import { buildNode, KQL_NODE_TYPE_LITERAL, toElasticsearchQuery, toKqlExpression } from './literal';
jest.mock('../grammar');
@ -29,5 +29,19 @@ describe('kuery node types', () => {
expect(result).toBe('foo');
});
});
describe('toKqlExpression', () => {
test('quoted', () => {
const node = buildNode('foo');
const result = toKqlExpression(node);
expect(result).toBe('foo');
});
test('unquoted', () => {
const node = buildNode('foo', true);
const result = toKqlExpression(node);
expect(result).toBe('"foo"');
});
});
});
});

View file

@ -33,3 +33,7 @@ export function buildNode(value: KqlLiteralType, isQuoted: boolean = false): Kql
export function toElasticsearchQuery(node: KqlLiteralNode) {
return node.value;
}
export function toKqlExpression(node: KqlLiteralNode): string {
return node.isQuoted ? `"${node.value}"` : `${node.value}`;
}

View file

@ -1,20 +0,0 @@
/*
* 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.
*/
/**
* WARNING: these typings are incomplete
*/
export type FunctionName = 'is' | 'and' | 'or' | 'not' | 'range' | 'exists' | 'nested';
export interface FunctionTypeBuildNode {
type: 'function';
function: FunctionName;
// TODO -> Need to define a better type for DSL query
arguments: any[];
}

View file

@ -7,14 +7,15 @@
*/
import {
buildNode,
KQL_WILDCARD_SYMBOL,
hasLeadingWildcard,
toElasticsearchQuery,
test as testNode,
toQueryStringQuery,
type KqlWildcardNode,
KQL_NODE_TYPE_WILDCARD,
// @ts-ignore
KQL_WILDCARD_SYMBOL,
buildNode,
hasLeadingWildcard,
test as testNode,
toElasticsearchQuery,
toKqlExpression,
toQueryStringQuery,
} from './wildcard';
jest.mock('../grammar');
@ -98,5 +99,16 @@ describe('kuery node types', () => {
expect(hasLeadingWildcard(leadingWildcardNode)).toBe(false);
});
});
describe('toKqlExpression', () => {
test('should return the string representation of the wildcard literal', () => {
const node: KqlWildcardNode = {
type: 'wildcard',
value: 'foo*bar@kuery-wildcard@baz',
};
const result = toKqlExpression(node);
expect(result).toBe('foo\\*bar*baz');
});
});
});
});

View file

@ -71,3 +71,7 @@ export function isLoneWildcard({ value }: KqlWildcardNode) {
export function hasLeadingWildcard(node: KqlWildcardNode) {
return !isLoneWildcard(node) && node.value.startsWith(KQL_WILDCARD_SYMBOL);
}
export function toKqlExpression(node: KqlWildcardNode): string {
return toQueryStringQuery(node);
}

View file

@ -8,11 +8,15 @@
import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { SerializableRecord } from '@kbn/utility-types';
import { KQL_NODE_TYPE_FUNCTION } from './node_types/function';
import { KQL_NODE_TYPE_LITERAL } from './node_types/literal';
import { KQL_NODE_TYPE_WILDCARD } from './node_types/wildcard';
/** @public */
export type KqlNodeType = typeof KQL_NODE_TYPE_LITERAL | 'function' | typeof KQL_NODE_TYPE_WILDCARD;
export type KqlNodeType =
| typeof KQL_NODE_TYPE_LITERAL
| typeof KQL_NODE_TYPE_FUNCTION
| typeof KQL_NODE_TYPE_WILDCARD;
/** @public */
export interface KueryNode {