[KQL] Clean up wildcard type (#134359)

* Clean up literal type

* [KQL] Clean up wildcard type

* [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix'

* Fix bugs

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Lukas Olson 2022-06-28 09:19:42 -07:00 committed by GitHub
parent c477a90eca
commit 8e1773ea0a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 78 additions and 61 deletions

View file

@ -12,7 +12,7 @@
const buildFunctionNode = nodeTypes.function.buildNodeWithArgumentNodes;
const buildLiteralNode = nodeTypes.literal.buildNode;
const buildWildcardNode = nodeTypes.wildcard.buildNode;
const { wildcardSymbol } = nodeTypes.wildcard;
const { KQL_WILDCARD_SYMBOL } = nodeTypes.wildcard;
}
start
@ -189,7 +189,7 @@ Value "value"
/ value:UnquotedLiteral {
if (value.type === 'cursor') return value;
if (!allowLeadingWildcards && value.type === 'wildcard' && nodeTypes.wildcard.hasLeadingWildcard(value)) {
if (!allowLeadingWildcards && nodeTypes.wildcard.isNode(value) && nodeTypes.wildcard.hasLeadingWildcard(value)) {
error('Leading wildcards are disabled. See query:allowLeadingWildcards in Advanced Settings.');
}
@ -248,7 +248,7 @@ UnquotedLiteral
if (sequence === 'null') return buildLiteralNode(null);
if (sequence === 'true') return buildLiteralNode(true);
if (sequence === 'false') return buildLiteralNode(false);
if (chars.includes(wildcardSymbol)) return buildWildcardNode(sequence);
if (chars.includes(KQL_WILDCARD_SYMBOL)) return buildWildcardNode(sequence);
return buildLiteralNode(sequence);
}
@ -261,7 +261,7 @@ UnquotedCharacter
/ !SpecialCharacter !Keyword !Cursor char:. { return char; }
Wildcard
= '*' { return wildcardSymbol; }
= '*' { return KQL_WILDCARD_SYMBOL; }
OptionalSpace
= &{ return parseCursor; } prefix:Space* cursor:Cursor suffix:Space* {

View file

@ -9,6 +9,7 @@
import { nodeTypes } from '../node_types';
import { fields } from '../../filters/stubs';
import { DataViewBase } from '../..';
import { KQL_NODE_TYPE_LITERAL } from '../node_types/literal';
jest.mock('../grammar');
@ -39,7 +40,7 @@ describe('kuery functions', () => {
arguments: [arg],
} = exists.buildNodeParams('response');
expect(arg).toHaveProperty('type', 'literal');
expect(arg).toHaveProperty('type', KQL_NODE_TYPE_LITERAL);
expect(arg).toHaveProperty('value', 'response');
});
});

View file

@ -12,6 +12,8 @@ import { fields } from '../../filters/stubs';
import * as is from './is';
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';
jest.mock('../grammar');
@ -32,9 +34,9 @@ describe('kuery functions', () => {
arguments: [fieldName, value],
} = is.buildNodeParams('response', 200);
expect(fieldName).toHaveProperty('type', 'literal');
expect(fieldName).toHaveProperty('type', KQL_NODE_TYPE_LITERAL);
expect(fieldName).toHaveProperty('value', 'response');
expect(value).toHaveProperty('type', 'literal');
expect(value).toHaveProperty('type', KQL_NODE_TYPE_LITERAL);
expect(value).toHaveProperty('value', 200);
});
@ -43,8 +45,8 @@ describe('kuery functions', () => {
arguments: [fieldName, value],
} = is.buildNodeParams('machine*', 'win*');
expect(fieldName).toHaveProperty('type', 'wildcard');
expect(value).toHaveProperty('type', 'wildcard');
expect(fieldName).toHaveProperty('type', KQL_NODE_TYPE_WILDCARD);
expect(value).toHaveProperty('type', KQL_NODE_TYPE_WILDCARD);
});
test('should default to a non-phrase query', () => {

View file

@ -48,9 +48,8 @@ export function toElasticsearchQuery(
arguments: [fieldNameArg, valueArg, isPhraseArg],
} = node;
const isExistsQuery = valueArg.type === 'wildcard' && valueArg.value === wildcard.wildcardSymbol;
const isAllFieldsQuery =
fieldNameArg.type === 'wildcard' && fieldNameArg.value === wildcard.wildcardSymbol;
const isExistsQuery = wildcard.isNode(valueArg) && wildcard.isLoneWildcard(valueArg);
const isAllFieldsQuery = wildcard.isNode(fieldNameArg) && wildcard.isLoneWildcard(fieldNameArg);
const isMatchAllQuery = isExistsQuery && isAllFieldsQuery;
if (isMatchAllQuery) {
@ -65,7 +64,7 @@ export function toElasticsearchQuery(
const value = !isUndefined(valueArg) ? ast.toElasticsearchQuery(valueArg) : valueArg;
const type = isPhraseArg.value ? 'phrase' : 'best_fields';
if (fullFieldNameArg.value === null) {
if (valueArg.type === 'wildcard') {
if (wildcard.isNode(valueArg)) {
return {
query_string: {
query: wildcard.toQueryStringQuery(valueArg),
@ -106,7 +105,7 @@ export function toElasticsearchQuery(
// Wildcards can easily include nested and non-nested fields. There isn't a good way to let
// users handle this themselves so we automatically add nested queries in this scenario.
const subTypeNested = getDataViewFieldSubtypeNested(field);
if (!(fullFieldNameArg.type === 'wildcard') || !subTypeNested?.nested || context?.nested) {
if (!wildcard.isNode(fullFieldNameArg) || !subTypeNested?.nested || context?.nested) {
return query;
} else {
return {
@ -143,7 +142,7 @@ export function toElasticsearchQuery(
},
}),
];
} else if (valueArg.type === 'wildcard') {
} else if (wildcard.isNode(valueArg)) {
return [
...accumulator,
wrapWithNestedQuery({

View file

@ -12,6 +12,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';
jest.mock('../grammar');
@ -33,7 +34,7 @@ describe('kuery functions', () => {
arguments: [fieldName],
} = result;
expect(fieldName).toHaveProperty('type', 'literal');
expect(fieldName).toHaveProperty('type', KQL_NODE_TYPE_LITERAL);
expect(fieldName).toHaveProperty('value', 'bytes');
});
@ -43,7 +44,7 @@ describe('kuery functions', () => {
arguments: [, , valueArg],
} = result;
expect(valueArg).toHaveProperty('type', 'literal');
expect(valueArg).toHaveProperty('type', KQL_NODE_TYPE_LITERAL);
expect(valueArg).toHaveProperty('value', 1000);
});
});

View file

@ -59,7 +59,11 @@ export function toElasticsearchQuery(
// Wildcards can easily include nested and non-nested fields. There isn't a good way to let
// users handle this themselves so we automatically add nested queries in this scenario.
const subTypeNested = getDataViewFieldSubtypeNested(field);
if (!(fullFieldNameArg.type === 'wildcard') || !subTypeNested?.nested || context!.nested) {
if (
!nodeTypes.wildcard.isNode(fullFieldNameArg) ||
!subTypeNested?.nested ||
context!.nested
) {
return query;
} else {
return {

View file

@ -19,7 +19,7 @@ export function getFields(node: KueryNode, indexPattern?: DataViewBase) {
return [];
}
return [field];
} else if (node.type === 'wildcard') {
} else if (wildcard.isNode(node)) {
const fields = indexPattern.fields.filter((fld) => wildcard.test(node, fld.name));
return fields;
}

View file

@ -9,6 +9,7 @@
import { getFields } from './get_fields';
import { DataViewBase, DataViewFieldBase, KueryNode } from '../../..';
import { getDataViewFieldSubtypeNested } from '../../../utils';
import { isNode as isWildcardNode } from '../../node_types/wildcard';
export function getFullFieldNameNode(
rootNameNode: any,
@ -23,7 +24,7 @@ export function getFullFieldNameNode(
// Wildcards can easily include nested and non-nested fields. There isn't a good way to let
// users handle this themselves so we automatically add nested queries in this scenario and skip the
// error checking below.
if (!indexPattern || (fullFieldNameNode.type === 'wildcard' && !nestedPath)) {
if (!indexPattern || (isWildcardNode(fullFieldNameNode) && !nestedPath)) {
return fullFieldNameNode;
}
const fields = getFields(fullFieldNameNode, indexPattern);

View file

@ -373,7 +373,7 @@ function peg$parse(input, options) {
var peg$f16 = function(value) {
if (value.type === 'cursor') return value;
if (!allowLeadingWildcards && value.type === 'wildcard' && nodeTypes.wildcard.hasLeadingWildcard(value)) {
if (!allowLeadingWildcards && nodeTypes.wildcard.isNode(value) && nodeTypes.wildcard.hasLeadingWildcard(value)) {
error('Leading wildcards are disabled. See query:allowLeadingWildcards in Advanced Settings.');
}
@ -401,10 +401,10 @@ function peg$parse(input, options) {
if (sequence === 'null') return buildLiteralNode(null);
if (sequence === 'true') return buildLiteralNode(true);
if (sequence === 'false') return buildLiteralNode(false);
if (chars.includes(wildcardSymbol)) return buildWildcardNode(sequence);
if (chars.includes(KQL_WILDCARD_SYMBOL)) return buildWildcardNode(sequence);
return buildLiteralNode(sequence);
};
var peg$f22 = function() { return wildcardSymbol; };
var peg$f22 = function() { return KQL_WILDCARD_SYMBOL; };
var peg$f23 = function() { return '\t'; };
var peg$f24 = function() { return '\r'; };
var peg$f25 = function() { return '\n'; };
@ -2189,7 +2189,7 @@ function peg$parse(input, options) {
const buildFunctionNode = nodeTypes.function.buildNodeWithArgumentNodes;
const buildLiteralNode = nodeTypes.literal.buildNode;
const buildWildcardNode = nodeTypes.wildcard.buildNode;
const { wildcardSymbol } = nodeTypes.wildcard;
const { KQL_WILDCARD_SYMBOL } = nodeTypes.wildcard;
peg$result = peg$startRuleFunction();

View file

@ -6,8 +6,7 @@
* Side Public License, v 1.
*/
// @ts-ignore
import { buildNode, toElasticsearchQuery } from './literal';
import { buildNode, KQL_NODE_TYPE_LITERAL, toElasticsearchQuery } from './literal';
jest.mock('../grammar');
@ -17,7 +16,7 @@ describe('kuery node types', () => {
test('should return a node representing the given value', () => {
const result = buildNode('foo');
expect(result).toHaveProperty('type', 'literal');
expect(result).toHaveProperty('type', KQL_NODE_TYPE_LITERAL);
expect(result).toHaveProperty('value', 'foo');
});
});

View file

@ -23,7 +23,7 @@ export function isNode(node: KueryNode): node is KqlLiteralNode {
export function buildNode(value: KqlLiteralType): KqlLiteralNode {
return {
type: 'literal',
type: KQL_NODE_TYPE_LITERAL,
value,
};
}

View file

@ -8,6 +8,7 @@
import { nodeBuilder } from './node_builder';
import { toElasticsearchQuery } from '..';
import { buildNode } from './literal';
jest.mock('../grammar');
@ -33,10 +34,7 @@ describe('nodeBuilder', () => {
});
test('KueryNode value', () => {
const literalValue = {
type: 'literal' as 'literal',
value: 'bar',
};
const literalValue = buildNode('bar');
const nodes = nodeBuilder.is('foo', literalValue);
const query = toElasticsearchQuery(nodes);
expect(query).toMatchInlineSnapshot(`

View file

@ -18,8 +18,3 @@ export interface FunctionTypeBuildNode {
// TODO -> Need to define a better type for DSL query
arguments: any[];
}
export interface WildcardTypeBuildNode {
type: 'wildcard';
value: string;
}

View file

@ -8,11 +8,12 @@
import {
buildNode,
wildcardSymbol,
KQL_WILDCARD_SYMBOL,
hasLeadingWildcard,
toElasticsearchQuery,
test as testNode,
toQueryStringQuery,
KQL_NODE_TYPE_WILDCARD,
// @ts-ignore
} from './wildcard';
@ -22,18 +23,18 @@ describe('kuery node types', () => {
describe('wildcard', () => {
describe('buildNode', () => {
test('should accept a string argument representing a wildcard string', () => {
const wildcardValue = `foo${wildcardSymbol}bar`;
const wildcardValue = `foo${KQL_WILDCARD_SYMBOL}bar`;
const result = buildNode(wildcardValue);
expect(result).toHaveProperty('type', 'wildcard');
expect(result).toHaveProperty('type', KQL_NODE_TYPE_WILDCARD);
expect(result).toHaveProperty('value', wildcardValue);
});
test('should accept and parse a wildcard string', () => {
const result = buildNode('foo*bar');
expect(result).toHaveProperty('type', 'wildcard');
expect(result.value).toBe(`foo${wildcardSymbol}bar`);
expect(result).toHaveProperty('type', KQL_NODE_TYPE_WILDCARD);
expect(result.value).toBe(`foo${KQL_WILDCARD_SYMBOL}bar`);
});
});

View file

@ -6,11 +6,15 @@
* Side Public License, v 1.
*/
import { fromLiteralExpression } from '../ast/ast';
import { WildcardTypeBuildNode } from './types';
import { KueryNode } from '..';
export const wildcardSymbol = '@kuery-wildcard@';
export const KQL_WILDCARD_SYMBOL = '@kuery-wildcard@';
export const KQL_NODE_TYPE_WILDCARD = 'wildcard';
export interface KqlWildcardNode extends KueryNode {
type: typeof KQL_NODE_TYPE_WILDCARD;
value: string;
}
// Copied from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
function escapeRegExp(str: string) {
@ -22,37 +26,48 @@ function escapeQueryString(str: string) {
return str.replace(/[+-=&|><!(){}[\]^"~*?:\\/]/g, '\\$&'); // $& means the whole matched string
}
export function buildNode(value: string): WildcardTypeBuildNode | KueryNode {
if (!value.includes(wildcardSymbol)) {
return fromLiteralExpression(value);
export function isNode(node: KueryNode): node is KqlWildcardNode {
return node.type === KQL_NODE_TYPE_WILDCARD;
}
export function isMatchAll(node: KqlWildcardNode) {
return node.value === KQL_WILDCARD_SYMBOL;
}
export function buildNode(value: string): KqlWildcardNode {
// When called from the parser, all wildcard characters are replaced with a special flag (since escaped wildcards are
// handled as normal strings). However, when invoking programmatically, callers shouldn't need to do this replacement.
if (!value.includes(KQL_NODE_TYPE_WILDCARD) && value.includes('*')) {
return buildNode(value.replaceAll('*', KQL_WILDCARD_SYMBOL));
}
return {
type: 'wildcard',
type: KQL_NODE_TYPE_WILDCARD,
value,
};
}
export function test(node: any, str: string): boolean {
export function test(node: KqlWildcardNode, str: string) {
const { value } = node;
const regex = value.split(wildcardSymbol).map(escapeRegExp).join('[\\s\\S]*');
const regex = value.split(KQL_WILDCARD_SYMBOL).map(escapeRegExp).join('[\\s\\S]*');
const regexp = new RegExp(`^${regex}$`);
return regexp.test(str);
}
export function toElasticsearchQuery(node: any): string {
export function toElasticsearchQuery(node: KqlWildcardNode) {
const { value } = node;
return value.split(wildcardSymbol).join('*');
return value.split(KQL_WILDCARD_SYMBOL).join('*');
}
export function toQueryStringQuery(node: any): string {
export function toQueryStringQuery(node: KqlWildcardNode) {
const { value } = node;
return value.split(wildcardSymbol).map(escapeQueryString).join('*');
return value.split(KQL_WILDCARD_SYMBOL).map(escapeQueryString).join('*');
}
export function hasLeadingWildcard(node: any): boolean {
const { value } = node;
// A lone wildcard turns into an `exists` query, so we're only concerned with
// leading wildcards followed by additional characters.
return value.startsWith(wildcardSymbol) && value.replace(wildcardSymbol, '').length > 0;
export function isLoneWildcard({ value }: KqlWildcardNode) {
return value.includes(KQL_WILDCARD_SYMBOL) && value.replace(KQL_WILDCARD_SYMBOL, '').length === 0;
}
export function hasLeadingWildcard(node: KqlWildcardNode) {
return !isLoneWildcard(node) && node.value.startsWith(KQL_WILDCARD_SYMBOL);
}

View file

@ -9,9 +9,10 @@
import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { SerializableRecord } from '@kbn/utility-types';
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' | 'wildcard';
export type KqlNodeType = typeof KQL_NODE_TYPE_LITERAL | 'function' | typeof KQL_NODE_TYPE_WILDCARD;
/** @public */
export interface KueryNode {