From 95f93e5f81897319f1f0010991ff8ba485520ec1 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Mon, 8 Apr 2019 14:18:09 -0700 Subject: [PATCH] Enables optional use of the timezone set in Advanced Settings in queries (#34602) * Adds time_zone to query * Adds dateFormatTZ to kuery query Removes comments * Adds defaults for the dateFormatTZ to the function signatures * Adds tests for date match in kuery Modifies test * Adds a test for get_es_query_config * Adds test for get timezone from settings utility method * Adds tests for modified range method Adds config param test to node_types/functions code clean up * resolves initial PR comments * Refactors build_es_query test * Refactors get_time_zone_from_settings test * Uses spys to test that the config is passed down to children in ast toElasticsearchQuery * removes default config nulls * Deletes sinon.spy tests in kuery * removes moment.setDefault from __tests__/get_timezone_from_settings.js --- packages/kbn-es-query/package.json | 3 +- .../src/es_query/__tests__/build_es_query.js | 30 +++++++++ .../src/es_query/__tests__/decorate_query.js | 5 ++ .../src/es_query/__tests__/from_kuery.js | 25 +++++++ .../src/es_query/__tests__/from_lucene.js | 18 +++++ .../es_query/__tests__/get_es_query_config.js | 66 +++++++++++++++++++ .../src/es_query/build_es_query.js | 7 +- .../src/es_query/decorate_query.js | 8 ++- .../kbn-es-query/src/es_query/from_kuery.js | 8 +-- .../kbn-es-query/src/es_query/from_lucene.js | 4 +- .../src/es_query/get_es_query_config.js | 3 +- .../src/kuery/ast/__tests__/ast.js | 9 +++ packages/kbn-es-query/src/kuery/ast/ast.js | 14 ++-- .../src/kuery/functions/__tests__/and.js | 3 - .../src/kuery/functions/__tests__/is.js | 46 +++++++++++++ .../src/kuery/functions/__tests__/not.js | 3 +- .../src/kuery/functions/__tests__/or.js | 3 +- .../src/kuery/functions/__tests__/range.js | 47 ++++++++++++- .../kbn-es-query/src/kuery/functions/and.js | 4 +- .../kbn-es-query/src/kuery/functions/is.js | 24 +++++-- .../kbn-es-query/src/kuery/functions/not.js | 4 +- .../kbn-es-query/src/kuery/functions/or.js | 4 +- .../kbn-es-query/src/kuery/functions/range.js | 15 ++++- .../kuery/node_types/__tests__/function.js | 1 - .../src/kuery/node_types/function.js | 6 +- .../__tests__/get_time_zone_from_settings.js | 36 ++++++++++ .../src/utils/get_time_zone_from_settings.js | 28 ++++++++ packages/kbn-es-query/src/utils/index.js | 20 ++++++ 28 files changed, 402 insertions(+), 42 deletions(-) create mode 100644 packages/kbn-es-query/src/es_query/__tests__/get_es_query_config.js create mode 100644 packages/kbn-es-query/src/utils/__tests__/get_time_zone_from_settings.js create mode 100644 packages/kbn-es-query/src/utils/get_time_zone_from_settings.js create mode 100644 packages/kbn-es-query/src/utils/index.js diff --git a/packages/kbn-es-query/package.json b/packages/kbn-es-query/package.json index ff68adeac863..091b019c3ea9 100644 --- a/packages/kbn-es-query/package.json +++ b/packages/kbn-es-query/package.json @@ -11,7 +11,8 @@ "kbn:watch": "node scripts/build --source-maps --watch" }, "dependencies": { - "lodash": "npm:@elastic/lodash@3.10.1-kibana1" + "lodash": "npm:@elastic/lodash@3.10.1-kibana1", + "moment-timezone": "^0.5.14" }, "devDependencies": { "@babel/cli": "^7.2.3", diff --git a/packages/kbn-es-query/src/es_query/__tests__/build_es_query.js b/packages/kbn-es-query/src/es_query/__tests__/build_es_query.js index 879fb1cd6c45..5c27a204ef26 100644 --- a/packages/kbn-es-query/src/es_query/__tests__/build_es_query.js +++ b/packages/kbn-es-query/src/es_query/__tests__/build_es_query.js @@ -103,6 +103,36 @@ describe('build query', function () { expect(result).to.eql(expectedResult); }); + it('should use the default time zone set in the Advanced Settings in queries and filters', function () { + const queries = [ + { query: '@timestamp:"2019-03-23T13:18:00"', language: 'kuery' }, + { query: '@timestamp:"2019-03-23T13:18:00"', language: 'lucene' } + ]; + const filters = [ + { match_all: {}, meta: { type: 'match_all' } } + ]; + const config = { + allowLeadingWildcards: true, + queryStringOptions: {}, + ignoreFilterIfFieldNotInIndex: false, + dateFormatTZ: 'Africa/Johannesburg', + }; + + const expectedResult = { + bool: { + must: [ + decorateQuery(luceneStringToDsl('@timestamp:"2019-03-23T13:18:00"'), config.queryStringOptions, config.dateFormatTZ), + { match_all: {} } + ], + filter: [toElasticsearchQuery(fromKueryExpression('@timestamp:"2019-03-23T13:18:00"'), indexPattern, config)], + should: [], + must_not: [], + } + }; + const result = buildEsQuery(indexPattern, queries, filters, config); + expect(result).to.eql(expectedResult); + }); + }); }); diff --git a/packages/kbn-es-query/src/es_query/__tests__/decorate_query.js b/packages/kbn-es-query/src/es_query/__tests__/decorate_query.js index 447a875e9ead..d5978716dac9 100644 --- a/packages/kbn-es-query/src/es_query/__tests__/decorate_query.js +++ b/packages/kbn-es-query/src/es_query/__tests__/decorate_query.js @@ -29,4 +29,9 @@ describe('Query decorator', function () { const decoratedQuery = decorateQuery({ query_string: { query: '*' } }, { analyze_wildcard: true }); expect(decoratedQuery).to.eql({ query_string: { query: '*', analyze_wildcard: true } }); }); + + it('should add a default of a time_zone parameter if one is provided', function () { + const decoratedQuery = decorateQuery({ query_string: { query: '*' } }, { analyze_wildcard: true }, 'America/Phoenix'); + expect(decoratedQuery).to.eql({ query_string: { query: '*', analyze_wildcard: true, time_zone: 'America/Phoenix' } }); + }); }); diff --git a/packages/kbn-es-query/src/es_query/__tests__/from_kuery.js b/packages/kbn-es-query/src/es_query/__tests__/from_kuery.js index 7c285a4416ab..3041e8a2c06d 100644 --- a/packages/kbn-es-query/src/es_query/__tests__/from_kuery.js +++ b/packages/kbn-es-query/src/es_query/__tests__/from_kuery.js @@ -60,6 +60,31 @@ describe('build query', function () { ); }); + + it('should accept a specific date format for a kuery query into an ES query in the bool\'s filter clause', function () { + const queries = [{ query: '@timestamp:"2018-04-03T19:04:17"', language: 'kuery' }]; + + const expectedESQueries = queries.map(query => { + return toElasticsearchQuery(fromKueryExpression(query.query), indexPattern, { dateFormatTZ: 'America/Phoenix' }); + }); + + const result = buildQueryFromKuery(indexPattern, queries, true, 'America/Phoenix'); + + expect(result.filter).to.eql(expectedESQueries); + }); + + it('should gracefully handle date queries when no date format is provided', function () { + const queries = [{ query: '@timestamp:"2018-04-03T19:04:17Z"', language: 'kuery' }]; + + const expectedESQueries = queries.map(query => { + return toElasticsearchQuery(fromKueryExpression(query.query), indexPattern); + }); + + const result = buildQueryFromKuery(indexPattern, queries, true); + + expect(result.filter).to.eql(expectedESQueries); + }); + }); }); diff --git a/packages/kbn-es-query/src/es_query/__tests__/from_lucene.js b/packages/kbn-es-query/src/es_query/__tests__/from_lucene.js index 7a4b6f7b359f..4361659021bd 100644 --- a/packages/kbn-es-query/src/es_query/__tests__/from_lucene.js +++ b/packages/kbn-es-query/src/es_query/__tests__/from_lucene.js @@ -66,4 +66,22 @@ describe('build query', function () { }); + it('should accept a date format in the decorated queries and combine that into the bool\'s must clause', function () { + const queries = [ + { query: 'foo:bar', language: 'lucene' }, + { query: 'bar:baz', language: 'lucene' }, + ]; + const dateFormatTZ = 'America/Phoenix'; + + const expectedESQueries = queries.map( + (query) => { + return decorateQuery(luceneStringToDsl(query.query), {}, dateFormatTZ); + } + ); + + const result = buildQueryFromLucene(queries, {}, dateFormatTZ); + + expect(result.must).to.eql(expectedESQueries); + }); + }); diff --git a/packages/kbn-es-query/src/es_query/__tests__/get_es_query_config.js b/packages/kbn-es-query/src/es_query/__tests__/get_es_query_config.js new file mode 100644 index 000000000000..8ccb04dd4b25 --- /dev/null +++ b/packages/kbn-es-query/src/es_query/__tests__/get_es_query_config.js @@ -0,0 +1,66 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { getEsQueryConfig } from '../get_es_query_config'; + +const config = { + get(item) { + return config[item]; + }, + 'query:allowLeadingWildcards': { + allowLeadingWildcards: true, + }, + 'query:queryString:options': { + queryStringOptions: {}, + }, + 'courier:ignoreFilterIfFieldNotInIndex': { + ignoreFilterIfFieldNotInIndex: true, + }, + 'dateFormat:tz': { + dateFormatTZ: 'Browser', + }, +}; + +describe('getEsQueryConfig', function () { + it('should return the parameters of an Elasticsearch query config requested', function () { + const result = getEsQueryConfig(config); + const expected = { + allowLeadingWildcards: { + allowLeadingWildcards: true, + }, + dateFormatTZ: { + dateFormatTZ: 'Browser', + }, + ignoreFilterIfFieldNotInIndex: { + ignoreFilterIfFieldNotInIndex: true, + }, + queryStringOptions: { + queryStringOptions: {}, + }, + }; + expect(result).to.eql(expected); + expect(result).to.have.keys( + 'allowLeadingWildcards', + 'dateFormatTZ', + 'ignoreFilterIfFieldNotInIndex', + 'queryStringOptions' + ); + }); +}); diff --git a/packages/kbn-es-query/src/es_query/build_es_query.js b/packages/kbn-es-query/src/es_query/build_es_query.js index 556bd8d52c9a..d17147761d8b 100644 --- a/packages/kbn-es-query/src/es_query/build_es_query.js +++ b/packages/kbn-es-query/src/es_query/build_es_query.js @@ -28,6 +28,7 @@ import { buildQueryFromLucene } from './from_lucene'; * @param filters - a filter object or array of filter objects * @param config - an objects with query:allowLeadingWildcards and query:queryString:options UI * settings in form of { allowLeadingWildcards, queryStringOptions } + * config contains dateformat:tz */ export function buildEsQuery( indexPattern, @@ -37,15 +38,15 @@ export function buildEsQuery( allowLeadingWildcards: false, queryStringOptions: {}, ignoreFilterIfFieldNotInIndex: false, + dateFormatTZ: null, }) { queries = Array.isArray(queries) ? queries : [queries]; filters = Array.isArray(filters) ? filters : [filters]; const validQueries = queries.filter((query) => has(query, 'query')); const queriesByLanguage = groupBy(validQueries, 'language'); - - const kueryQuery = buildQueryFromKuery(indexPattern, queriesByLanguage.kuery, config.allowLeadingWildcards); - const luceneQuery = buildQueryFromLucene(queriesByLanguage.lucene, config.queryStringOptions); + const kueryQuery = buildQueryFromKuery(indexPattern, queriesByLanguage.kuery, config.allowLeadingWildcards, config.dateFormatTZ); + const luceneQuery = buildQueryFromLucene(queriesByLanguage.lucene, config.queryStringOptions, config.dateFormatTZ); const filterQuery = buildQueryFromFilters(filters, indexPattern, config.ignoreFilterIfFieldNotInIndex); return { diff --git a/packages/kbn-es-query/src/es_query/decorate_query.js b/packages/kbn-es-query/src/es_query/decorate_query.js index ff7c3ae80c69..8104707e0298 100644 --- a/packages/kbn-es-query/src/es_query/decorate_query.js +++ b/packages/kbn-es-query/src/es_query/decorate_query.js @@ -18,16 +18,22 @@ */ import _ from 'lodash'; +import { getTimeZoneFromSettings } from '../utils/get_time_zone_from_settings'; /** * Decorate queries with default parameters * @param query object * @param queryStringOptions query:queryString:options from UI settings + * @param dateFormatTZ dateFormat:tz from UI settings * @returns {object} */ -export function decorateQuery(query, queryStringOptions) { + +export function decorateQuery(query, queryStringOptions, dateFormatTZ = null) { if (_.has(query, 'query_string.query')) { _.extend(query.query_string, queryStringOptions); + if (dateFormatTZ) { + _.defaults(query.query_string, { time_zone: getTimeZoneFromSettings(dateFormatTZ) }); + } } return query; diff --git a/packages/kbn-es-query/src/es_query/from_kuery.js b/packages/kbn-es-query/src/es_query/from_kuery.js index 9706e82f9bad..723003edc46f 100644 --- a/packages/kbn-es-query/src/es_query/from_kuery.js +++ b/packages/kbn-es-query/src/es_query/from_kuery.js @@ -19,7 +19,7 @@ import { fromLegacyKueryExpression, fromKueryExpression, toElasticsearchQuery, nodeTypes } from '../kuery'; -export function buildQueryFromKuery(indexPattern, queries = [], allowLeadingWildcards) { +export function buildQueryFromKuery(indexPattern, queries = [], allowLeadingWildcards, dateFormatTZ = null) { const queryASTs = queries.map(query => { try { return fromKueryExpression(query.query, { allowLeadingWildcards }); @@ -32,12 +32,12 @@ export function buildQueryFromKuery(indexPattern, queries = [], allowLeadingWild throw Error('OutdatedKuerySyntaxError'); } }); - return buildQuery(indexPattern, queryASTs); + return buildQuery(indexPattern, queryASTs, { dateFormatTZ }); } -function buildQuery(indexPattern, queryASTs) { +function buildQuery(indexPattern, queryASTs, config = null) { const compoundQueryAST = nodeTypes.function.buildNode('and', queryASTs); - const kueryQuery = toElasticsearchQuery(compoundQueryAST, indexPattern); + const kueryQuery = toElasticsearchQuery(compoundQueryAST, indexPattern, config); return { must: [], filter: [], diff --git a/packages/kbn-es-query/src/es_query/from_lucene.js b/packages/kbn-es-query/src/es_query/from_lucene.js index 7d6df6060ba0..8845fd68efb4 100644 --- a/packages/kbn-es-query/src/es_query/from_lucene.js +++ b/packages/kbn-es-query/src/es_query/from_lucene.js @@ -21,10 +21,10 @@ import _ from 'lodash'; import { decorateQuery } from './decorate_query'; import { luceneStringToDsl } from './lucene_string_to_dsl'; -export function buildQueryFromLucene(queries, queryStringOptions) { +export function buildQueryFromLucene(queries, queryStringOptions, dateFormatTZ = null) { const combinedQueries = _.map(queries, (query) => { const queryDsl = luceneStringToDsl(query.query); - return decorateQuery(queryDsl, queryStringOptions); + return decorateQuery(queryDsl, queryStringOptions, dateFormatTZ); }); return { diff --git a/packages/kbn-es-query/src/es_query/get_es_query_config.js b/packages/kbn-es-query/src/es_query/get_es_query_config.js index af6ce412f7b0..2518b1077462 100644 --- a/packages/kbn-es-query/src/es_query/get_es_query_config.js +++ b/packages/kbn-es-query/src/es_query/get_es_query_config.js @@ -21,5 +21,6 @@ export function getEsQueryConfig(config) { const allowLeadingWildcards = config.get('query:allowLeadingWildcards'); const queryStringOptions = config.get('query:queryString:options'); const ignoreFilterIfFieldNotInIndex = config.get('courier:ignoreFilterIfFieldNotInIndex'); - return { allowLeadingWildcards, queryStringOptions, ignoreFilterIfFieldNotInIndex }; + const dateFormatTZ = config.get('dateFormat:tz'); + return { allowLeadingWildcards, queryStringOptions, ignoreFilterIfFieldNotInIndex, dateFormatTZ }; } diff --git a/packages/kbn-es-query/src/kuery/ast/__tests__/ast.js b/packages/kbn-es-query/src/kuery/ast/__tests__/ast.js index 96039526752d..c1e27c68a03b 100644 --- a/packages/kbn-es-query/src/kuery/ast/__tests__/ast.js +++ b/packages/kbn-es-query/src/kuery/ast/__tests__/ast.js @@ -24,6 +24,7 @@ import indexPatternResponse from '../../../__fixtures__/index_pattern_response.j // Helpful utility allowing us to test the PEG parser by simply checking for deep equality between // the nodes the parser generates and the nodes our constructor functions generate. + function fromLegacyKueryExpressionNoMeta(text) { return ast.fromLegacyKueryExpression(text, { includeMetadata: false }); } @@ -416,6 +417,14 @@ describe('kuery AST API', function () { expect(ast.toElasticsearchQuery(unknownTypeNode)).to.eql(expected); }); + it('should return the given node type\'s ES query representation including a time zone parameter when one is provided', function () { + const config = { dateFormatTZ: 'America/Phoenix' }; + const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"'); + const expected = nodeTypes.function.toElasticsearchQuery(node, indexPattern, config); + const result = ast.toElasticsearchQuery(node, indexPattern, config); + expect(result).to.eql(expected); + }); + }); describe('doesKueryExpressionHaveLuceneSyntaxError', function () { diff --git a/packages/kbn-es-query/src/kuery/ast/ast.js b/packages/kbn-es-query/src/kuery/ast/ast.js index 39a22eb8a77a..7c8c9534aa14 100644 --- a/packages/kbn-es-query/src/kuery/ast/ast.js +++ b/packages/kbn-es-query/src/kuery/ast/ast.js @@ -51,15 +51,19 @@ function fromExpression(expression, parseOptions = {}, parse = parseKuery) { return parse(expression, parseOptions); } - -// indexPattern isn't required, but if you pass one in, we can be more intelligent -// about how we craft the queries (e.g. scripted fields) -export function toElasticsearchQuery(node, indexPattern) { +/** + * @params {String} indexPattern + * @params {Object} config - contains the dateFormatTZ + * + * IndexPattern isn't required, but if you pass one in, we can be more intelligent + * about how we craft the queries (e.g. scripted fields) + */ +export function toElasticsearchQuery(node, indexPattern, config = {}) { if (!node || !node.type || !nodeTypes[node.type]) { return toElasticsearchQuery(nodeTypes.function.buildNode('and', [])); } - return nodeTypes[node.type].toElasticsearchQuery(node, indexPattern); + return nodeTypes[node.type].toElasticsearchQuery(node, indexPattern, config); } export function doesKueryExpressionHaveLuceneSyntaxError(expression) { diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/and.js b/packages/kbn-es-query/src/kuery/functions/__tests__/and.js index 5a0d3a9cd58b..07289a878e8c 100644 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/and.js +++ b/packages/kbn-es-query/src/kuery/functions/__tests__/and.js @@ -23,7 +23,6 @@ import { nodeTypes } from '../../node_types'; import * as ast from '../../ast'; import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; - let indexPattern; const childNode1 = nodeTypes.function.buildNode('is', 'machine.os', 'osx'); @@ -57,8 +56,6 @@ describe('kuery functions', function () { [childNode1, childNode2].map((childNode) => ast.toElasticsearchQuery(childNode, indexPattern)) ); }); - }); - }); }); diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/is.js b/packages/kbn-es-query/src/kuery/functions/__tests__/is.js index 7ceac9b605db..18652c9faccb 100644 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/is.js +++ b/packages/kbn-es-query/src/kuery/functions/__tests__/is.js @@ -199,6 +199,52 @@ describe('kuery functions', function () { expect(result.bool.should[0]).to.have.key('script'); }); + it('should support date fields without a dateFormat provided', function () { + const expected = { + bool: { + should: [ + { + range: { + '@timestamp': { + gte: '2018-04-03T19:04:17', + lte: '2018-04-03T19:04:17', + } + } + } + ], + minimum_should_match: 1 + } + }; + + const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"'); + const result = is.toElasticsearchQuery(node, indexPattern); + expect(result).to.eql(expected); + }); + + it('should support date fields with a dateFormat provided', function () { + const config = { dateFormatTZ: 'America/Phoenix' }; + const expected = { + bool: { + should: [ + { + range: { + '@timestamp': { + gte: '2018-04-03T19:04:17', + lte: '2018-04-03T19:04:17', + time_zone: 'America/Phoenix', + } + } + } + ], + minimum_should_match: 1 + } + }; + + const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"'); + const result = is.toElasticsearchQuery(node, indexPattern, config); + expect(result).to.eql(expected); + }); + }); }); }); diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/not.js b/packages/kbn-es-query/src/kuery/functions/__tests__/not.js index 6b5b50e15524..7a2d7fa39c15 100644 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/not.js +++ b/packages/kbn-es-query/src/kuery/functions/__tests__/not.js @@ -23,7 +23,6 @@ import { nodeTypes } from '../../node_types'; import * as ast from '../../ast'; import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; - let indexPattern; const childNode = nodeTypes.function.buildNode('is', 'extension', 'jpg'); @@ -32,7 +31,6 @@ describe('kuery functions', function () { describe('not', function () { - beforeEach(() => { indexPattern = indexPatternResponse; }); @@ -56,6 +54,7 @@ describe('kuery functions', function () { expect(result.bool).to.only.have.keys('must_not'); expect(result.bool.must_not).to.eql(ast.toElasticsearchQuery(childNode, indexPattern)); }); + }); }); }); diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/or.js b/packages/kbn-es-query/src/kuery/functions/__tests__/or.js index 3b5bf27f2d8c..f24f24b98e7f 100644 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/or.js +++ b/packages/kbn-es-query/src/kuery/functions/__tests__/or.js @@ -23,7 +23,6 @@ import { nodeTypes } from '../../node_types'; import * as ast from '../../ast'; import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; - let indexPattern; const childNode1 = nodeTypes.function.buildNode('is', 'machine.os', 'osx'); @@ -33,11 +32,11 @@ describe('kuery functions', function () { describe('or', function () { - beforeEach(() => { indexPattern = indexPatternResponse; }); + describe('buildNodeParams', function () { it('arguments should contain the unmodified child nodes', function () { diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/range.js b/packages/kbn-es-query/src/kuery/functions/__tests__/range.js index a8c0b8157405..4f290206c8bf 100644 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/range.js +++ b/packages/kbn-es-query/src/kuery/functions/__tests__/range.js @@ -22,7 +22,6 @@ import * as range from '../range'; import { nodeTypes } from '../../node_types'; import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; - let indexPattern; describe('kuery functions', function () { @@ -136,6 +135,52 @@ describe('kuery functions', function () { expect(result.bool.should[0]).to.have.key('script'); }); + it('should support date fields without a dateFormat provided', function () { + const expected = { + bool: { + should: [ + { + range: { + '@timestamp': { + gt: '2018-01-03T19:04:17', + lt: '2018-04-03T19:04:17', + } + } + } + ], + minimum_should_match: 1 + } + }; + + const node = nodeTypes.function.buildNode('range', '@timestamp', { gt: '2018-01-03T19:04:17', lt: '2018-04-03T19:04:17' }); + const result = range.toElasticsearchQuery(node, indexPattern); + expect(result).to.eql(expected); + }); + + it('should support date fields with a dateFormat provided', function () { + const config = { dateFormatTZ: 'America/Phoenix' }; + const expected = { + bool: { + should: [ + { + range: { + '@timestamp': { + gt: '2018-01-03T19:04:17', + lt: '2018-04-03T19:04:17', + time_zone: 'America/Phoenix', + } + } + } + ], + minimum_should_match: 1 + } + }; + + const node = nodeTypes.function.buildNode('range', '@timestamp', { gt: '2018-01-03T19:04:17', lt: '2018-04-03T19:04:17' }); + const result = range.toElasticsearchQuery(node, indexPattern, config); + expect(result).to.eql(expected); + }); + }); }); }); diff --git a/packages/kbn-es-query/src/kuery/functions/and.js b/packages/kbn-es-query/src/kuery/functions/and.js index a727af65f56e..68e125ea4de5 100644 --- a/packages/kbn-es-query/src/kuery/functions/and.js +++ b/packages/kbn-es-query/src/kuery/functions/and.js @@ -25,13 +25,13 @@ export function buildNodeParams(children) { }; } -export function toElasticsearchQuery(node, indexPattern) { +export function toElasticsearchQuery(node, indexPattern, config) { const children = node.arguments || []; return { bool: { filter: children.map((child) => { - return ast.toElasticsearchQuery(child, indexPattern); + return ast.toElasticsearchQuery(child, indexPattern, config); }) } }; diff --git a/packages/kbn-es-query/src/kuery/functions/is.js b/packages/kbn-es-query/src/kuery/functions/is.js index 27e64d23c154..0338671e9b3f 100644 --- a/packages/kbn-es-query/src/kuery/functions/is.js +++ b/packages/kbn-es-query/src/kuery/functions/is.js @@ -23,6 +23,7 @@ import * as literal from '../node_types/literal'; import * as wildcard from '../node_types/wildcard'; import { getPhraseScript } from '../../filters'; import { getFields } from './utils/get_fields'; +import { getTimeZoneFromSettings } from '../../utils/get_time_zone_from_settings'; export function buildNodeParams(fieldName, value, isPhrase = false) { if (_.isUndefined(fieldName)) { @@ -35,19 +36,16 @@ export function buildNodeParams(fieldName, value, isPhrase = false) { const fieldNode = typeof fieldName === 'string' ? ast.fromLiteralExpression(fieldName) : literal.buildNode(fieldName); const valueNode = typeof value === 'string' ? ast.fromLiteralExpression(value) : literal.buildNode(value); const isPhraseNode = literal.buildNode(isPhrase); - return { arguments: [fieldNode, valueNode, isPhraseNode], }; } -export function toElasticsearchQuery(node, indexPattern) { +export function toElasticsearchQuery(node, indexPattern = null, config = {}) { const { arguments: [ fieldNameArg, valueArg, isPhraseArg ] } = node; - const fieldName = ast.toElasticsearchQuery(fieldNameArg); const value = !_.isUndefined(valueArg) ? ast.toElasticsearchQuery(valueArg) : valueArg; const type = isPhraseArg.value ? 'phrase' : 'best_fields'; - if (fieldNameArg.value === null) { if (valueArg.type === 'wildcard') { return { @@ -67,7 +65,6 @@ export function toElasticsearchQuery(node, indexPattern) { } const fields = indexPattern ? getFields(fieldNameArg, indexPattern) : []; - // If no fields are found in the index pattern we send through the given field name as-is. We do this to preserve // the behaviour of lucene on dashboards where there are panels based on different index patterns that have different // fields. If a user queries on a field that exists in one pattern but not the other, the index pattern without the @@ -116,6 +113,22 @@ export function toElasticsearchQuery(node, indexPattern) { } }]; } + /* + If we detect that it's a date field and the user wants an exact date, we need to convert the query to both >= and <= the value provided to force a range query. This is because match and match_phrase queries do not accept a timezone parameter. + dateFormatTZ can have the value of 'Browser', in which case we guess the timezone using moment.tz.guess. + */ + else if (field.type === 'date') { + const timeZoneParam = config.dateFormatTZ ? { time_zone: getTimeZoneFromSettings(config.dateFormatTZ) } : {}; + return [...accumulator, { + range: { + [field.name]: { + gte: value, + lte: value, + ...timeZoneParam, + }, + } + }]; + } else { const queryType = type === 'phrase' ? 'match_phrase' : 'match'; return [...accumulator, { @@ -134,3 +147,4 @@ export function toElasticsearchQuery(node, indexPattern) { }; } + diff --git a/packages/kbn-es-query/src/kuery/functions/not.js b/packages/kbn-es-query/src/kuery/functions/not.js index 3f0776604408..d3ab14df16bc 100644 --- a/packages/kbn-es-query/src/kuery/functions/not.js +++ b/packages/kbn-es-query/src/kuery/functions/not.js @@ -25,12 +25,12 @@ export function buildNodeParams(child) { }; } -export function toElasticsearchQuery(node, indexPattern) { +export function toElasticsearchQuery(node, indexPattern, config) { const [ argument ] = node.arguments; return { bool: { - must_not: ast.toElasticsearchQuery(argument, indexPattern) + must_not: ast.toElasticsearchQuery(argument, indexPattern, config) } }; } diff --git a/packages/kbn-es-query/src/kuery/functions/or.js b/packages/kbn-es-query/src/kuery/functions/or.js index ea834b8f5c5f..918d46a6691d 100644 --- a/packages/kbn-es-query/src/kuery/functions/or.js +++ b/packages/kbn-es-query/src/kuery/functions/or.js @@ -25,13 +25,13 @@ export function buildNodeParams(children) { }; } -export function toElasticsearchQuery(node, indexPattern) { +export function toElasticsearchQuery(node, indexPattern, config) { const children = node.arguments || []; return { bool: { should: children.map((child) => { - return ast.toElasticsearchQuery(child, indexPattern); + return ast.toElasticsearchQuery(child, indexPattern, config); }), minimum_should_match: 1, }, diff --git a/packages/kbn-es-query/src/kuery/functions/range.js b/packages/kbn-es-query/src/kuery/functions/range.js index 40c53e55c201..df77baf4b020 100644 --- a/packages/kbn-es-query/src/kuery/functions/range.js +++ b/packages/kbn-es-query/src/kuery/functions/range.js @@ -22,6 +22,7 @@ import { nodeTypes } from '../node_types'; import * as ast from '../ast'; import { getRangeScript } from '../../filters'; import { getFields } from './utils/get_fields'; +import { getTimeZoneFromSettings } from '../../utils/get_time_zone_from_settings'; export function buildNodeParams(fieldName, params) { params = _.pick(params, 'gt', 'lt', 'gte', 'lte', 'format'); @@ -35,7 +36,7 @@ export function buildNodeParams(fieldName, params) { }; } -export function toElasticsearchQuery(node, indexPattern) { +export function toElasticsearchQuery(node, indexPattern = null, config = {}) { const [ fieldNameArg, ...args ] = node.arguments; const fields = indexPattern ? getFields(fieldNameArg, indexPattern) : []; const namedArgs = extractArguments(args); @@ -60,7 +61,17 @@ export function toElasticsearchQuery(node, indexPattern) { script: getRangeScript(field, queryParams), }; } - + else if (field.type === 'date') { + const timeZoneParam = config.dateFormatTZ ? { time_zone: getTimeZoneFromSettings(config.dateFormatTZ) } : {}; + return { + range: { + [field.name]: { + ...queryParams, + ...timeZoneParam, + } + } + }; + } return { range: { [field.name]: queryParams diff --git a/packages/kbn-es-query/src/kuery/node_types/__tests__/function.js b/packages/kbn-es-query/src/kuery/node_types/__tests__/function.js index 2ccb3bd5991d..de00c083fc83 100644 --- a/packages/kbn-es-query/src/kuery/node_types/__tests__/function.js +++ b/packages/kbn-es-query/src/kuery/node_types/__tests__/function.js @@ -31,7 +31,6 @@ describe('kuery node types', function () { let indexPattern; - beforeEach(() => { indexPattern = indexPatternResponse; }); diff --git a/packages/kbn-es-query/src/kuery/node_types/function.js b/packages/kbn-es-query/src/kuery/node_types/function.js index 7bcae9358565..6b10bb1f704c 100644 --- a/packages/kbn-es-query/src/kuery/node_types/function.js +++ b/packages/kbn-es-query/src/kuery/node_types/function.js @@ -21,8 +21,8 @@ import _ from 'lodash'; import { functions } from '../functions'; export function buildNode(functionName, ...functionArgs) { - const kueryFunction = functions[functionName]; + const kueryFunction = functions[functionName]; if (_.isUndefined(kueryFunction)) { throw new Error(`Unknown function "${functionName}"`); } @@ -47,8 +47,8 @@ export function buildNodeWithArgumentNodes(functionName, argumentNodes) { }; } -export function toElasticsearchQuery(node, indexPattern) { +export function toElasticsearchQuery(node, indexPattern, config = {}) { const kueryFunction = functions[node.function]; - return kueryFunction.toElasticsearchQuery(node, indexPattern); + return kueryFunction.toElasticsearchQuery(node, indexPattern, config); } diff --git a/packages/kbn-es-query/src/utils/__tests__/get_time_zone_from_settings.js b/packages/kbn-es-query/src/utils/__tests__/get_time_zone_from_settings.js new file mode 100644 index 000000000000..6deaccadfdb7 --- /dev/null +++ b/packages/kbn-es-query/src/utils/__tests__/get_time_zone_from_settings.js @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { getTimeZoneFromSettings } from '../get_time_zone_from_settings'; + +describe('get timezone from settings', function () { + + it('should return the config timezone if the time zone is set', function () { + const result = getTimeZoneFromSettings('America/Chicago'); + expect(result).to.eql('America/Chicago'); + }); + + it('should return the system timezone if the time zone is set to "Browser"', function () { + const result = getTimeZoneFromSettings('Browser'); + expect(result).to.not.equal('Browser'); + }); + +}); + diff --git a/packages/kbn-es-query/src/utils/get_time_zone_from_settings.js b/packages/kbn-es-query/src/utils/get_time_zone_from_settings.js new file mode 100644 index 000000000000..1a06941ece12 --- /dev/null +++ b/packages/kbn-es-query/src/utils/get_time_zone_from_settings.js @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import moment from 'moment-timezone'; +const detectedTimezone = moment.tz.guess(); + +export function getTimeZoneFromSettings(dateFormatTZ) { + if (dateFormatTZ === 'Browser') { + return detectedTimezone; + } + return dateFormatTZ; +} diff --git a/packages/kbn-es-query/src/utils/index.js b/packages/kbn-es-query/src/utils/index.js new file mode 100644 index 000000000000..27f51c1f44cf --- /dev/null +++ b/packages/kbn-es-query/src/utils/index.js @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './get_time_zone_from_settings';