From 4ffb0d43d9cc54a0e8092fcadbff33253cb2ece2 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Mon, 13 Apr 2020 17:31:06 +0300 Subject: [PATCH] WIP [Regression] Histogram aggregation always shows an error message Closes: #62624 --- .../public/control/control.test.ts | 4 +- .../public/control/control.ts | 5 +- .../public/control/create_search_source.ts | 11 +- .../public/control/list_control_factory.ts | 13 +- .../public/control/range_control_factory.ts | 9 +- .../public/legacy_imports.ts | 26 - .../test_utils/get_search_service_mock.ts | 8 +- .../public/vis_controller.tsx | 10 +- .../kibana/public/discover/build_services.ts | 8 + .../kibana/public/discover/kibana_services.ts | 1 - .../np_ready/angular/context/api/context.ts | 4 +- .../np_ready/angular/context/query/actions.js | 4 +- src/plugins/data/public/index.ts | 3 +- .../data/public/search/expressions/esaggs.ts | 2 +- src/plugins/data/public/search/index.ts | 3 +- .../search/search_source/search_source.ts | 772 +++++++++--------- 16 files changed, 446 insertions(+), 437 deletions(-) delete mode 100644 src/legacy/core_plugins/input_control_vis/public/legacy_imports.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/control/control.test.ts b/src/legacy/core_plugins/input_control_vis/public/control/control.test.ts index e76b199a0262c55..7f4d8a945b8eb1a 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/control.test.ts +++ b/src/legacy/core_plugins/input_control_vis/public/control/control.test.ts @@ -21,7 +21,7 @@ import expect from '@kbn/expect'; import { Control } from './control'; import { ControlParams } from '../editor_utils'; import { FilterManager as BaseFilterManager } from './filter_manager/filter_manager'; -import { SearchSource } from '../legacy_imports'; +import { ISearchSource } from '../../../../../plugins/data/public'; function createControlParams(id: string, label: string): ControlParams { return { @@ -51,7 +51,7 @@ class ControlMock extends Control { destroy() {} } -const mockKbnApi: SearchSource = {} as SearchSource; +const mockKbnApi: ISearchSource = {} as ISearchSource; describe('hasChanged', () => { let control: ControlMock; diff --git a/src/legacy/core_plugins/input_control_vis/public/control/control.ts b/src/legacy/core_plugins/input_control_vis/public/control/control.ts index 6fddef777f73eec..b69a0c3ce0693cb 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/control.ts +++ b/src/legacy/core_plugins/input_control_vis/public/control/control.ts @@ -22,8 +22,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { Filter } from '../../../../../plugins/data/public'; -import { SearchSource as SearchSourceClass } from '../legacy_imports'; +import { Filter, SearchSourceType } from '../../../../../plugins/data/public'; import { ControlParams, ControlParamsOptions, CONTROL_TYPES } from '../editor_utils'; import { RangeFilterManager } from './filter_manager/range_filter_manager'; import { PhraseFilterManager } from './filter_manager/phrase_filter_manager'; @@ -62,7 +61,7 @@ export abstract class Control { public controlParams: ControlParams, public filterManager: FilterManager, public useTimeFilter: boolean, - public SearchSource: SearchSourceClass + public SearchSource: SearchSourceType ) { this.id = controlParams.id; this.controlParams = controlParams; diff --git a/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.ts b/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.ts index f238a2287ecdbf3..10ef7c229f369a4 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.ts +++ b/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.ts @@ -17,11 +17,16 @@ * under the License. */ -import { PhraseFilter, IndexPattern, TimefilterContract } from '../../../../../plugins/data/public'; -import { SearchSource as SearchSourceClass, SearchSourceFields } from '../legacy_imports'; +import { + SearchSourceFields, + SearchSourceType, + PhraseFilter, + IndexPattern, + TimefilterContract, +} from '../../../../../plugins/data/public'; export function createSearchSource( - SearchSource: SearchSourceClass, + SearchSource: SearchSourceType, initialState: SearchSourceFields | null, indexPattern: IndexPattern, aggs: any, diff --git a/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.ts b/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.ts index 8364c82efecdb5f..5a7174701843e77 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.ts +++ b/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.ts @@ -19,14 +19,17 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; - -import { SearchSource as SearchSourceClass, SearchSourceFields } from '../legacy_imports'; import { Control, noValuesDisableMsg, noIndexPatternMsg } from './control'; import { PhraseFilterManager } from './filter_manager/phrase_filter_manager'; import { createSearchSource } from './create_search_source'; import { ControlParams } from '../editor_utils'; import { InputControlVisDependencies } from '../plugin'; -import { IFieldType, TimefilterContract } from '../../../../../plugins/data/public'; +import { + IFieldType, + TimefilterContract, + SearchSourceType, + SearchSourceFields, +} from '../../../../../plugins/data/public'; function getEscapedQuery(query = '') { // https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html#_standard_operators @@ -86,7 +89,7 @@ export class ListControl extends Control { controlParams: ControlParams, filterManager: PhraseFilterManager, useTimeFilter: boolean, - SearchSource: SearchSourceClass, + SearchSource: SearchSourceType, deps: InputControlVisDependencies ) { super(controlParams, filterManager, useTimeFilter, SearchSource); @@ -202,7 +205,7 @@ export class ListControl extends Control { export async function listControlFactory( controlParams: ControlParams, useTimeFilter: boolean, - SearchSource: SearchSourceClass, + SearchSource: SearchSourceType, deps: InputControlVisDependencies ) { const [, { data: dataPluginStart }] = await deps.core.getStartServices(); diff --git a/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.ts b/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.ts index d9b43c9dff20120..c905dfa025c0542 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.ts +++ b/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.ts @@ -20,13 +20,16 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { SearchSource as SearchSourceClass } from '../legacy_imports'; import { Control, noValuesDisableMsg, noIndexPatternMsg } from './control'; import { RangeFilterManager } from './filter_manager/range_filter_manager'; import { createSearchSource } from './create_search_source'; import { ControlParams } from '../editor_utils'; import { InputControlVisDependencies } from '../plugin'; -import { IFieldType, TimefilterContract } from '../.../../../../../../plugins/data/public'; +import { + IFieldType, + TimefilterContract, + SearchSourceType, +} from '../.../../../../../../plugins/data/public'; const minMaxAgg = (field?: IFieldType) => { const aggBody: any = {}; @@ -61,7 +64,7 @@ export class RangeControl extends Control { controlParams: ControlParams, filterManager: RangeFilterManager, useTimeFilter: boolean, - SearchSource: SearchSourceClass, + SearchSource: SearchSourceType, deps: InputControlVisDependencies ) { super(controlParams, filterManager, useTimeFilter, SearchSource); diff --git a/src/legacy/core_plugins/input_control_vis/public/legacy_imports.ts b/src/legacy/core_plugins/input_control_vis/public/legacy_imports.ts deleted file mode 100644 index 8c58ac2386da432..000000000000000 --- a/src/legacy/core_plugins/input_control_vis/public/legacy_imports.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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 { Class } from '@kbn/utility-types'; -import { SearchSource as SearchSourceClass, ISearchSource } from '../../../../plugins/data/public'; - -export { SearchSourceFields } from '../../../../plugins/data/public'; - -export type SearchSource = Class; -export const SearchSource = SearchSourceClass; diff --git a/src/legacy/core_plugins/input_control_vis/public/test_utils/get_search_service_mock.ts b/src/legacy/core_plugins/input_control_vis/public/test_utils/get_search_service_mock.ts index 94a460086e9da78..ef22fafec2bd9a5 100644 --- a/src/legacy/core_plugins/input_control_vis/public/test_utils/get_search_service_mock.ts +++ b/src/legacy/core_plugins/input_control_vis/public/test_utils/get_search_service_mock.ts @@ -17,10 +17,10 @@ * under the License. */ -import { SearchSource } from '../legacy_imports'; +import { ISearchSource } from '../../../../../plugins/data/public'; -export const getSearchSourceMock = (esSearchResponse?: any): SearchSource => - jest.fn().mockImplementation(() => ({ +export const getSearchSourceMock = (esSearchResponse?: any) => + (jest.fn().mockImplementation(() => ({ setParent: jest.fn(), setField: jest.fn(), fetch: jest.fn().mockResolvedValue( @@ -43,4 +43,4 @@ export const getSearchSourceMock = (esSearchResponse?: any): SearchSource => }, } ), - })); + })) as unknown) as ISearchSource; diff --git a/src/legacy/core_plugins/input_control_vis/public/vis_controller.tsx b/src/legacy/core_plugins/input_control_vis/public/vis_controller.tsx index c4a7d286850e363..325a8f9d1d6b222 100644 --- a/src/legacy/core_plugins/input_control_vis/public/vis_controller.tsx +++ b/src/legacy/core_plugins/input_control_vis/public/vis_controller.tsx @@ -21,8 +21,6 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { I18nStart } from 'kibana/public'; -import { SearchSource } from './legacy_imports'; - import { InputControlVis } from './components/vis/input_control_vis'; import { getControlFactory } from './control/control_factory'; import { getLineageMap } from './lineage'; @@ -30,7 +28,7 @@ import { ControlParams } from './editor_utils'; import { RangeControl } from './control/range_control_factory'; import { ListControl } from './control/list_control_factory'; import { InputControlVisDependencies } from './plugin'; -import { FilterManager, Filter } from '../../../../plugins/data/public'; +import { FilterManager, Filter, getSearchSource } from '../../../../plugins/data/public'; import { VisParams, Vis } from '../../../../plugins/visualizations/public'; export const createInputControlVisController = (deps: InputControlVisDependencies) => { @@ -100,8 +98,14 @@ export const createInputControlVisController = (deps: InputControlVisDependencie } ); + const [coreStart, { data }] = await deps.core.getStartServices(); const controlFactoryPromises = controlParamsList.map(controlParams => { const factory = getControlFactory(controlParams); + const SearchSource = getSearchSource({ + searchService: data.search, + uiSettings: coreStart.uiSettings, + injectedMetadata: coreStart.injectedMetadata, + }); return factory(controlParams, this.visParams?.useTimeFilter, SearchSource, deps); }); const controls = await Promise.all(controlFactoryPromises); diff --git a/src/legacy/core_plugins/kibana/public/discover/build_services.ts b/src/legacy/core_plugins/kibana/public/discover/build_services.ts index a3a99a0ded523cf..fd0df3040d5f926 100644 --- a/src/legacy/core_plugins/kibana/public/discover/build_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/build_services.ts @@ -27,10 +27,12 @@ import { IUiSettingsClient, } from 'kibana/public'; import { + getSearchSource, FilterManager, TimefilterContract, IndexPatternsContract, DataPublicPluginStart, + SearchSourceType, } from 'src/plugins/data/public'; import { DiscoverStartPlugins } from './plugin'; @@ -64,6 +66,7 @@ export interface DiscoverServices { getSavedSearchUrlById: (id: string) => Promise; uiSettings: IUiSettingsClient; visualizations: VisualizationsStart; + SearchSource: SearchSourceType; } export async function buildServices( core: CoreStart, @@ -99,5 +102,10 @@ export async function buildServices( toastNotifications: core.notifications.toasts, uiSettings: core.uiSettings, visualizations: plugins.visualizations, + SearchSource: getSearchSource({ + searchService: plugins.data.search, + uiSettings: core.uiSettings, + injectedMetadata: core.injectedMetadata, + }), }; } diff --git a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts index 0a81ca0222b0a05..c127c6426df16f1 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -71,7 +71,6 @@ export { IndexPattern, indexPatterns, IFieldType, - SearchSource, ISearchSource, EsQuerySortValue, SortDirection, diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.ts index 507f927c608e1b4..26c15dc4925d0bf 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.ts @@ -27,8 +27,8 @@ import { Filter, IndexPatternsContract, IndexPattern, - SearchSource, } from '../../../../../../../../../plugins/data/public'; +import { getServices } from '../../../../kibana_services'; export type SurrDocType = 'successors' | 'predecessors'; export interface EsHitRecord { @@ -115,6 +115,8 @@ function fetchContextProvider(indexPatterns: IndexPatternsContract) { } async function createSearchSource(indexPattern: IndexPattern, filters: Filter[]) { + const { SearchSource } = getServices(); + return new SearchSource() .setParent(undefined) .setField('index', indexPattern) diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/query/actions.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/query/actions.js index 9efddc52750699c..4d61821b2422e39 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/query/actions.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/query/actions.js @@ -20,7 +20,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { getServices, SearchSource } from '../../../../kibana_services'; +import { getServices } from '../../../../kibana_services'; import { fetchAnchorProvider } from '../api/anchor'; import { fetchContextProvider } from '../api/context'; @@ -29,7 +29,7 @@ import { FAILURE_REASONS, LOADING_STATUS } from './constants'; import { MarkdownSimple } from '../../../../../../../../../plugins/kibana_react/public'; export function QueryActionsProvider(Promise) { - const { filterManager, indexPatterns } = getServices(); + const { filterManager, indexPatterns, SearchSource } = getServices(); const fetchAnchor = fetchAnchorProvider(indexPatterns, new SearchSource()); const { fetchSurroundingDocs } = fetchContextProvider(indexPatterns); const { setPredecessorCount, setQueryParameters, setSuccessorCount } = getQueryParameterActions( diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 06a46065baa8477..e426a3eae55eacb 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -365,7 +365,8 @@ export { SearchError, SearchStrategyProvider, ISearchSource, - SearchSource, + SearchSourceType, + getSearchSource, createSearchSource, SearchSourceFields, EsQuerySortValue, diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts index 2341f4fe447dbc2..4ad916a81c76940 100644 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ b/src/plugins/data/public/search/expressions/esaggs.ts @@ -30,7 +30,7 @@ import { PersistedState } from '../../../../../plugins/visualizations/public'; import { Adapters } from '../../../../../plugins/inspector/public'; import { IAggConfigs } from '../aggs'; -import { ISearchSource, SearchSource } from '../search_source'; +import { ISearchSource, getSearchSource } from '../search_source'; import { tabifyAggResponse } from '../tabify'; import { Filter, Query, serializeFieldFormat, TimeRange } from '../../../common'; import { FilterManager, getTime } from '../../query'; diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts index cce973d632f4173..2dcd0ff82495cd6 100644 --- a/src/plugins/data/public/search/index.ts +++ b/src/plugins/data/public/search/index.ts @@ -50,7 +50,8 @@ export { SearchError, SearchStrategyProvider, getSearchErrorType } from './searc export { ISearchSource, - SearchSource, + SearchSourceType, + getSearchSource, SearchSourceFields, EsQuerySortValue, SortDirection, diff --git a/src/plugins/data/public/search/search_source/search_source.ts b/src/plugins/data/public/search/search_source/search_source.ts index c70db7bb82ef7fd..5965b420d5c7559 100644 --- a/src/plugins/data/public/search/search_source/search_source.ts +++ b/src/plugins/data/public/search/search_source/search_source.ts @@ -70,435 +70,445 @@ */ import _ from 'lodash'; -import { SavedObjectReference } from 'kibana/public'; +import { CoreStart, SavedObjectReference } from 'kibana/public'; import { normalizeSortRequest } from './normalize_sort_request'; import { filterDocvalueFields } from './filter_docvalue_fields'; import { fieldWildcardFilter } from '../../../../kibana_utils/public'; -import { IIndexPattern, SearchRequest } from '../..'; +import { DataPublicPluginStart, IIndexPattern, SearchRequest } from '../..'; import { SearchSourceOptions, SearchSourceFields } from './types'; import { fetchSoon, FetchOptions, RequestFailure } from '../fetch'; - -import { getSearchService, getUiSettings, getInjectedMetadata } from '../../services'; import { getEsQueryConfig, buildEsQuery, Filter } from '../../../common'; import { getHighlightRequest } from '../../../common/field_formats'; -export type ISearchSource = Pick; - -export class SearchSource { - private id: string = _.uniqueId('data_source'); - private searchStrategyId?: string; - private parent?: SearchSource; - private requestStartHandlers: Array< - (searchSource: ISearchSource, options?: FetchOptions) => Promise - > = []; - private inheritOptions: SearchSourceOptions = {}; - public history: SearchRequest[] = []; - - constructor(private fields: SearchSourceFields = {}) {} - - /** *** - * PUBLIC API - *****/ - - setPreferredSearchStrategyId(searchStrategyId: string) { - this.searchStrategyId = searchStrategyId; - } - - setFields(newFields: SearchSourceFields) { - this.fields = newFields; - return this; - } - - setField(field: K, value: SearchSourceFields[K]) { - if (value == null) { - delete this.fields[field]; - } else { - this.fields[field] = value; - } - return this; - } - - getId() { - return this.id; - } - - getFields() { - return { ...this.fields }; - } - - /** - * Get fields from the fields - */ - getField(field: K, recurse = true): SearchSourceFields[K] { - if (!recurse || this.fields[field] !== void 0) { - return this.fields[field]; +export type SearchSourceType = ReturnType; +export type ISearchSource = InstanceType; + +interface SearchSourceDependencies { + searchService: DataPublicPluginStart['search']; + uiSettings: CoreStart['uiSettings']; + injectedMetadata: CoreStart['injectedMetadata']; +} + +export const getSearchSource = ({ + searchService, + uiSettings, + injectedMetadata, +}: SearchSourceDependencies) => + class SearchSource { + private id: string = _.uniqueId('data_source'); + private searchStrategyId?: string; + private parent?: SearchSource; + private requestStartHandlers: Array< + (searchSource: ISearchSource, options?: FetchOptions) => Promise + > = []; + private inheritOptions: SearchSourceOptions = {}; + public history: SearchRequest[] = []; + + constructor(private fields: SearchSourceFields = {}) {} + + /** *** + * PUBLIC API + *****/ + + setPreferredSearchStrategyId(searchStrategyId: string) { + this.searchStrategyId = searchStrategyId; } - const parent = this.getParent(); - return parent && parent.getField(field); - } - - /** - * Get the field from our own fields, don't traverse up the chain - */ - getOwnField(field: K): SearchSourceFields[K] { - return this.getField(field, false); - } - - create() { - return new SearchSource(); - } - - createCopy() { - const newSearchSource = new SearchSource(); - newSearchSource.setFields({ ...this.fields }); - // when serializing the internal fields we lose the internal classes used in the index - // pattern, so we have to set it again to workaround this behavior - newSearchSource.setField('index', this.getField('index')); - newSearchSource.setParent(this.getParent()); - return newSearchSource; - } - - createChild(options = {}) { - const childSearchSource = new SearchSource(); - childSearchSource.setParent(this, options); - return childSearchSource; - } - - /** - * Set a searchSource that this source should inherit from - * @param {SearchSource} parent - the parent searchSource - * @param {SearchSourceOptions} options - the inherit options - * @return {this} - chainable - */ - setParent(parent?: ISearchSource, options: SearchSourceOptions = {}) { - this.parent = parent as SearchSource; - this.inheritOptions = options; - return this; - } - - /** - * Get the parent of this SearchSource - * @return {undefined|searchSource} - */ - getParent() { - return this.parent; - } - - /** - * Fetch this source and reject the returned Promise on error - * - * @async - */ - async fetch(options: FetchOptions = {}) { - await this.requestIsStarting(options); - - const searchRequest = await this.flatten(); - this.history = [searchRequest]; - - const esShardTimeout = getInjectedMetadata().getInjectedVar('esShardTimeout') as number; - const response = await fetchSoon( - searchRequest, - { - ...(this.searchStrategyId && { searchStrategyId: this.searchStrategyId }), - ...options, - }, - { - searchService: getSearchService(), - config: getUiSettings(), - esShardTimeout, - } - ); - if (response.error) { - throw new RequestFailure(null, response); + setFields(newFields: SearchSourceFields) { + this.fields = newFields; + return this; } - return response; - } - - /** - * Add a handler that will be notified whenever requests start - * @param {Function} handler - * @return {undefined} - */ - onRequestStart( - handler: (searchSource: ISearchSource, options?: FetchOptions) => Promise - ) { - this.requestStartHandlers.push(handler); - } - - async getSearchRequestBody() { - const searchRequest = await this.flatten(); - return searchRequest.body; - } - - /** - * Completely destroy the SearchSource. - * @return {undefined} - */ - destroy() { - this.requestStartHandlers.length = 0; - } - - /** **** - * PRIVATE APIS - ******/ - - /** - * Called by requests of this search source when they are started - * @param {Courier.Request} request - * @param options - * @return {Promise} - */ - private requestIsStarting(options: FetchOptions = {}) { - const handlers = [...this.requestStartHandlers]; - // If callParentStartHandlers has been set to true, we also call all - // handlers of parent search sources. - if (this.inheritOptions.callParentStartHandlers) { - let searchSource = this.getParent(); - while (searchSource) { - handlers.push(...searchSource.requestStartHandlers); - searchSource = searchSource.getParent(); + setField(field: K, value: SearchSourceFields[K]) { + if (value == null) { + delete this.fields[field]; + } else { + this.fields[field] = value; } + return this; } - return Promise.all(handlers.map(fn => fn(this, options))); - } - - /** - * Used to merge properties into the data within ._flatten(). - * The data is passed in and modified by the function - * - * @param {object} data - the current merged data - * @param {*} val - the value at `key` - * @param {*} key - The key of `val` - * @return {undefined} - */ - private mergeProp( - data: SearchRequest, - val: SearchSourceFields[K], - key: K - ) { - val = typeof val === 'function' ? val(this) : val; - if (val == null || !key) return; - - const addToRoot = (rootKey: string, value: any) => { - data[rootKey] = value; - }; + getId() { + return this.id; + } + + getFields() { + return { ...this.fields }; + } /** - * Add the key and val to the body of the request + * Get fields from the fields */ - const addToBody = (bodyKey: string, value: any) => { - // ignore if we already have a value - if (data.body[bodyKey] == null) { - data.body[bodyKey] = value; + getField(field: K, recurse = true): SearchSourceFields[K] { + if (!recurse || this.fields[field] !== void 0) { + return this.fields[field]; } - }; - - switch (key) { - case 'filter': - return addToRoot('filters', (data.filters || []).concat(val)); - case 'query': - return addToRoot(key, (data[key] || []).concat(val)); - case 'fields': - const fields = _.uniq((data[key] || []).concat(val)); - return addToRoot(key, fields); - case 'index': - case 'type': - case 'highlightAll': - return key && data[key] == null && addToRoot(key, val); - case 'searchAfter': - return addToBody('search_after', val); - case 'source': - return addToBody('_source', val); - case 'sort': - const sort = normalizeSortRequest( - val, - this.getField('index'), - getUiSettings().get('sort:options') - ); - return addToBody(key, sort); - default: - return addToBody(key, val); - } - } - - /** - * Walk the inheritance chain of a source and return its - * flat representation (taking into account merging rules) - * @returns {Promise} - * @resolved {Object|null} - the flat data of the SearchSource - */ - private mergeProps(root = this, searchRequest: SearchRequest = { body: {} }) { - Object.entries(this.fields).forEach(([key, value]) => { - this.mergeProp(searchRequest, value, key as keyof SearchSourceFields); - }); - if (this.parent) { - this.parent.mergeProps(root, searchRequest); + const parent = this.getParent(); + return parent && parent.getField(field); } - return searchRequest; - } - - private getIndexType(index: IIndexPattern) { - if (this.searchStrategyId) { - return this.searchStrategyId === 'default' ? undefined : this.searchStrategyId; - } else { - return index?.type; - } - } - private flatten() { - const searchRequest = this.mergeProps(); + /** + * Get the field from our own fields, don't traverse up the chain + */ + getOwnField(field: K): SearchSourceFields[K] { + return this.getField(field, false); + } - searchRequest.body = searchRequest.body || {}; - const { body, index, fields, query, filters, highlightAll } = searchRequest; - searchRequest.indexType = this.getIndexType(index); + create() { + return new SearchSource(); + } - const computedFields = index ? index.getComputedFields() : {}; + createCopy() { + const newSearchSource = new SearchSource(); + newSearchSource.setFields({ ...this.fields }); + // when serializing the internal fields we lose the internal classes used in the index + // pattern, so we have to set it again to workaround this behavior + newSearchSource.setField('index', this.getField('index')); + newSearchSource.setParent(this.getParent()); + return newSearchSource; + } - body.stored_fields = computedFields.storedFields; - body.script_fields = body.script_fields || {}; - _.extend(body.script_fields, computedFields.scriptFields); + createChild(options = {}) { + const childSearchSource = new SearchSource(); + childSearchSource.setParent(this, options); + return childSearchSource; + } - const defaultDocValueFields = computedFields.docvalueFields - ? computedFields.docvalueFields - : []; - body.docvalue_fields = body.docvalue_fields || defaultDocValueFields; + /** + * Set a searchSource that this source should inherit from + * @param {SearchSource} parent - the parent searchSource + * @param {SearchSourceOptions} options - the inherit options + * @return {this} - chainable + */ + setParent(parent?: ISearchSource, options: SearchSourceOptions = {}) { + this.parent = parent as SearchSource; + this.inheritOptions = options; + return this; + } - if (!body.hasOwnProperty('_source') && index) { - body._source = index.getSourceFiltering(); + /** + * Get the parent of this SearchSource + * @return {undefined|searchSource} + */ + getParent() { + return this.parent; } - if (body._source) { - // exclude source fields for this index pattern specified by the user - const filter = fieldWildcardFilter(body._source.excludes, getUiSettings().get('metaFields')); - body.docvalue_fields = body.docvalue_fields.filter((docvalueField: any) => - filter(docvalueField.field) + /** + * Fetch this source and reject the returned Promise on error + * + * @async + */ + async fetch(options: FetchOptions = {}) { + await this.requestIsStarting(options); + + const searchRequest = await this.flatten(); + this.history = [searchRequest]; + + const esShardTimeout = injectedMetadata.getInjectedVar('esShardTimeout') as number; + const response = await fetchSoon( + searchRequest, + { + ...(this.searchStrategyId && { searchStrategyId: this.searchStrategyId }), + ...options, + }, + { + searchService, + config: uiSettings, + esShardTimeout, + } ); - } - // if we only want to search for certain fields - if (fields) { - // filter out the docvalue_fields, and script_fields to only include those that we are concerned with - body.docvalue_fields = filterDocvalueFields(body.docvalue_fields, fields); - body.script_fields = _.pick(body.script_fields, fields); + if (response.error) { + throw new RequestFailure(null, response); + } - // request the remaining fields from both stored_fields and _source - const remainingFields = _.difference(fields, _.keys(body.script_fields)); - body.stored_fields = remainingFields; - _.set(body, '_source.includes', remainingFields); + return response; } - const esQueryConfigs = getEsQueryConfig(getUiSettings()); - body.query = buildEsQuery(index, query, filters, esQueryConfigs); + /** + * Add a handler that will be notified whenever requests start + * @param {Function} handler + * @return {undefined} + */ + onRequestStart( + handler: (searchSource: ISearchSource, options?: FetchOptions) => Promise + ) { + this.requestStartHandlers.push(handler); + } - if (highlightAll && body.query) { - body.highlight = getHighlightRequest(body.query, getUiSettings().get('doc_table:highlight')); - delete searchRequest.highlightAll; + async getSearchRequestBody() { + const searchRequest = await this.flatten(); + return searchRequest.body; } - const translateToQuery = (filter: Filter) => filter && (filter.query || filter); + /** + * Completely destroy the SearchSource. + * @return {undefined} + */ + destroy() { + this.requestStartHandlers.length = 0; + } - // re-write filters within filter aggregations - (function recurse(aggBranch) { - if (!aggBranch) return; - Object.keys(aggBranch).forEach(function(id) { - const agg = aggBranch[id]; + /** **** + * PRIVATE APIS + ******/ - if (agg.filters) { - // translate filters aggregations - const { filters: aggFilters } = agg.filters; - Object.keys(aggFilters).forEach(filterId => { - aggFilters[filterId] = translateToQuery(aggFilters[filterId]); - }); + /** + * Called by requests of this search source when they are started + * @param {Courier.Request} request + * @param options + * @return {Promise} + */ + private requestIsStarting(options: FetchOptions = {}) { + const handlers = [...this.requestStartHandlers]; + // If callParentStartHandlers has been set to true, we also call all + // handlers of parent search sources. + if (this.inheritOptions.callParentStartHandlers) { + let searchSource = this.getParent(); + while (searchSource) { + handlers.push(...searchSource.requestStartHandlers); + searchSource = searchSource.getParent(); } + } - recurse(agg.aggs || agg.aggregations); - }); - })(body.aggs || body.aggregations); - - return searchRequest; - } - - /** - * Serializes the instance to a JSON string and a set of referenced objects. - * Use this method to get a representation of the search source which can be stored in a saved object. - * - * The references returned by this function can be mixed with other references in the same object, - * however make sure there are no name-collisions. The references will be named `kibanaSavedObjectMeta.searchSourceJSON.index` - * and `kibanaSavedObjectMeta.searchSourceJSON.filter[].meta.index`. - * - * Using `createSearchSource`, the instance can be re-created. - * @param searchSource The search source to serialize - * @public */ - public serialize() { - const references: SavedObjectReference[] = []; - - const { - filter: originalFilters, - ...searchSourceFields - }: Omit = _.omit(this.getFields(), ['sort', 'size']); - let serializedSearchSourceFields: Omit & { - indexRefName?: string; - filter?: Array & { meta: Filter['meta'] & { indexRefName?: string } }>; - } = searchSourceFields; - if (searchSourceFields.index) { - const indexId = searchSourceFields.index.id!; - const refName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; - references.push({ - name: refName, - type: 'index-pattern', - id: indexId, - }); - serializedSearchSourceFields = { - ...serializedSearchSourceFields, - indexRefName: refName, - index: undefined, - }; + return Promise.all(handlers.map(fn => fn(this, options))); } - if (originalFilters) { - const filters = this.getFilters(originalFilters); - serializedSearchSourceFields = { - ...serializedSearchSourceFields, - filter: filters.map((filterRow, i) => { - if (!filterRow.meta || !filterRow.meta.index) { - return filterRow; - } - const refName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`; - references.push({ - name: refName, - type: 'index-pattern', - id: filterRow.meta.index, - }); - return { - ...filterRow, - meta: { - ...filterRow.meta, - indexRefName: refName, - index: undefined, - }, - }; - }), + + /** + * Used to merge properties into the data within ._flatten(). + * The data is passed in and modified by the function + * + * @param {object} data - the current merged data + * @param {*} val - the value at `key` + * @param {*} key - The key of `val` + * @return {undefined} + */ + private mergeProp( + data: SearchRequest, + val: SearchSourceFields[K], + key: K + ) { + val = typeof val === 'function' ? val(this) : val; + if (val == null || !key) return; + + const addToRoot = (rootKey: string, value: any) => { + data[rootKey] = value; }; + + /** + * Add the key and val to the body of the request + */ + const addToBody = (bodyKey: string, value: any) => { + // ignore if we already have a value + if (data.body[bodyKey] == null) { + data.body[bodyKey] = value; + } + }; + + switch (key) { + case 'filter': + return addToRoot('filters', (data.filters || []).concat(val)); + case 'query': + return addToRoot(key, (data[key] || []).concat(val)); + case 'fields': + const fields = _.uniq((data[key] || []).concat(val)); + return addToRoot(key, fields); + case 'index': + case 'type': + case 'highlightAll': + return key && data[key] == null && addToRoot(key, val); + case 'searchAfter': + return addToBody('search_after', val); + case 'source': + return addToBody('_source', val); + case 'sort': + const sort = normalizeSortRequest( + val, + this.getField('index'), + uiSettings.get('sort:options') + ); + return addToBody(key, sort); + default: + return addToBody(key, val); + } } - return { searchSourceJSON: JSON.stringify(serializedSearchSourceFields), references }; - } + /** + * Walk the inheritance chain of a source and return its + * flat representation (taking into account merging rules) + * @returns {Promise} + * @resolved {Object|null} - the flat data of the SearchSource + */ + private mergeProps(root = this, searchRequest: SearchRequest = { body: {} }) { + Object.entries(this.fields).forEach(([key, value]) => { + this.mergeProp(searchRequest, value, key as keyof SearchSourceFields); + }); + if (this.parent) { + this.parent.mergeProps(root, searchRequest); + } + return searchRequest; + } - private getFilters(filterField: SearchSourceFields['filter']): Filter[] { - if (!filterField) { - return []; + private getIndexType(index: IIndexPattern) { + if (this.searchStrategyId) { + return this.searchStrategyId === 'default' ? undefined : this.searchStrategyId; + } else { + return index?.type; + } } - if (Array.isArray(filterField)) { - return filterField; + private flatten() { + const searchRequest = this.mergeProps(); + + searchRequest.body = searchRequest.body || {}; + const { body, index, fields, query, filters, highlightAll } = searchRequest; + searchRequest.indexType = this.getIndexType(index); + + const computedFields = index ? index.getComputedFields() : {}; + + body.stored_fields = computedFields.storedFields; + body.script_fields = body.script_fields || {}; + _.extend(body.script_fields, computedFields.scriptFields); + + const defaultDocValueFields = computedFields.docvalueFields + ? computedFields.docvalueFields + : []; + body.docvalue_fields = body.docvalue_fields || defaultDocValueFields; + + if (!body.hasOwnProperty('_source') && index) { + body._source = index.getSourceFiltering(); + } + + if (body._source) { + // exclude source fields for this index pattern specified by the user + const filter = fieldWildcardFilter(body._source.excludes, uiSettings.get('metaFields')); + body.docvalue_fields = body.docvalue_fields.filter((docvalueField: any) => + filter(docvalueField.field) + ); + } + + // if we only want to search for certain fields + if (fields) { + // filter out the docvalue_fields, and script_fields to only include those that we are concerned with + body.docvalue_fields = filterDocvalueFields(body.docvalue_fields, fields); + body.script_fields = _.pick(body.script_fields, fields); + + // request the remaining fields from both stored_fields and _source + const remainingFields = _.difference(fields, _.keys(body.script_fields)); + body.stored_fields = remainingFields; + _.set(body, '_source.includes', remainingFields); + } + + const esQueryConfigs = getEsQueryConfig(uiSettings); + body.query = buildEsQuery(index, query, filters, esQueryConfigs); + + if (highlightAll && body.query) { + body.highlight = getHighlightRequest(body.query, uiSettings.get('doc_table:highlight')); + delete searchRequest.highlightAll; + } + + const translateToQuery = (filter: Filter) => filter && (filter.query || filter); + + // re-write filters within filter aggregations + (function recurse(aggBranch) { + if (!aggBranch) return; + Object.keys(aggBranch).forEach(function(id) { + const agg = aggBranch[id]; + + if (agg.filters) { + // translate filters aggregations + const { filters: aggFilters } = agg.filters; + Object.keys(aggFilters).forEach(filterId => { + aggFilters[filterId] = translateToQuery(aggFilters[filterId]); + }); + } + + recurse(agg.aggs || agg.aggregations); + }); + })(body.aggs || body.aggregations); + + return searchRequest; } - if (_.isFunction(filterField)) { - return this.getFilters(filterField()); + /** + * Serializes the instance to a JSON string and a set of referenced objects. + * Use this method to get a representation of the search source which can be stored in a saved object. + * + * The references returned by this function can be mixed with other references in the same object, + * however make sure there are no name-collisions. The references will be named `kibanaSavedObjectMeta.searchSourceJSON.index` + * and `kibanaSavedObjectMeta.searchSourceJSON.filter[].meta.index`. + * + * Using `createSearchSource`, the instance can be re-created. + * @param searchSource The search source to serialize + * @public */ + public serialize() { + const references: SavedObjectReference[] = []; + + const { + filter: originalFilters, + ...searchSourceFields + }: Omit = _.omit(this.getFields(), ['sort', 'size']); + let serializedSearchSourceFields: Omit & { + indexRefName?: string; + filter?: Array & { meta: Filter['meta'] & { indexRefName?: string } }>; + } = searchSourceFields; + if (searchSourceFields.index) { + const indexId = searchSourceFields.index.id!; + const refName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; + references.push({ + name: refName, + type: 'index-pattern', + id: indexId, + }); + serializedSearchSourceFields = { + ...serializedSearchSourceFields, + indexRefName: refName, + index: undefined, + }; + } + if (originalFilters) { + const filters = this.getFilters(originalFilters); + serializedSearchSourceFields = { + ...serializedSearchSourceFields, + filter: filters.map((filterRow, i) => { + if (!filterRow.meta || !filterRow.meta.index) { + return filterRow; + } + const refName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`; + references.push({ + name: refName, + type: 'index-pattern', + id: filterRow.meta.index, + }); + return { + ...filterRow, + meta: { + ...filterRow.meta, + indexRefName: refName, + index: undefined, + }, + }; + }), + }; + } + + return { searchSourceJSON: JSON.stringify(serializedSearchSourceFields), references }; } - return [filterField]; - } -} + private getFilters(filterField: SearchSourceFields['filter']): Filter[] { + if (!filterField) { + return []; + } + + if (Array.isArray(filterField)) { + return filterField; + } + + if (_.isFunction(filterField)) { + return this.getFilters(filterField()); + } + + return [filterField]; + } + };