diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md index b8b6ee1f0b28..9c47ea1a166d 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md @@ -17,4 +17,5 @@ export interface ISearchStartAggsStart | | | [getSearchStrategy](./kibana-plugin-plugins-data-server.isearchstart.getsearchstrategy.md) | (name: string) => ISearchStrategy<SearchStrategyRequest, SearchStrategyResponse> | Get other registered search strategies. For example, if a new strategy needs to use the already-registered ES search strategy, it can use this function to accomplish that. | | [search](./kibana-plugin-plugins-data-server.isearchstart.search.md) | (context: RequestHandlerContext, request: SearchStrategyRequest, options: ISearchOptions) => Promise<SearchStrategyResponse> | | +| [searchSource](./kibana-plugin-plugins-data-server.isearchstart.searchsource.md) | {
asScoped: (request: KibanaRequest) => Promise<ISearchStartSearchSource>;
} | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.searchsource.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.searchsource.md new file mode 100644 index 000000000000..66a43fe29c43 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.searchsource.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchStart](./kibana-plugin-plugins-data-server.isearchstart.md) > [searchSource](./kibana-plugin-plugins-data-server.isearchstart.searchsource.md) + +## ISearchStart.searchSource property + +Signature: + +```typescript +searchSource: { + asScoped: (request: KibanaRequest) => Promise; + }; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md index 84aeb4cf80cc..e44cb5c65774 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md @@ -8,13 +8,13 @@ ```typescript start(core: CoreStart): { - search: ISearchStart>; fieldFormats: { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { indexPatternsServiceFactory: (kibanaRequest: import("../../../core/server").KibanaRequest) => Promise; }; + search: ISearchStart>; }; ``` @@ -27,12 +27,12 @@ start(core: CoreStart): { Returns: `{ - search: ISearchStart>; fieldFormats: { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { indexPatternsServiceFactory: (kibanaRequest: import("../../../core/server").KibanaRequest) => Promise; }; + search: ISearchStart>; }` diff --git a/src/plugins/data/common/search/search_source/index.ts b/src/plugins/data/common/search/search_source/index.ts index 70c9cfcee234..2ef1b4c3c519 100644 --- a/src/plugins/data/common/search/search_source/index.ts +++ b/src/plugins/data/common/search/search_source/index.ts @@ -17,11 +17,12 @@ * under the License. */ -export { SearchSource, ISearchSource, SearchSourceDependencies } from './search_source'; export { createSearchSource } from './create_search_source'; -export { SortDirection, EsQuerySortValue, SearchSourceFields } from './types'; export { injectReferences } from './inject_references'; export { extractReferences } from './extract_references'; export { parseSearchSourceJSON } from './parse_json'; export * from './fetch'; export * from './legacy'; +export * from './search_source'; +export * from './search_source_service'; +export * from './types'; diff --git a/src/plugins/data/common/search/search_source/inject_references.test.ts b/src/plugins/data/common/search/search_source/inject_references.test.ts new file mode 100644 index 000000000000..c607f16d2473 --- /dev/null +++ b/src/plugins/data/common/search/search_source/inject_references.test.ts @@ -0,0 +1,120 @@ +/* + * 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 { SavedObjectReference } from 'src/core/types'; +import { SearchSourceFields } from './types'; + +import { injectReferences } from './inject_references'; + +describe('injectSearchSourceReferences', () => { + let searchSourceJSON: SearchSourceFields & { indexRefName: string }; + let references: SavedObjectReference[]; + + beforeEach(() => { + searchSourceJSON = { + highlightAll: true, + version: true, + query: { + query: 'play_name:"Henry IV"', + language: 'kuery', + }, + indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index', + }; + references = [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: '033af690-fde7-11ea-91f3-fb9e73f9bbe9', + }, + ]; + }); + + test('injects references', () => { + const actual = injectReferences(searchSourceJSON, references); + expect(actual).toMatchInlineSnapshot(` + Object { + "highlightAll": true, + "index": "033af690-fde7-11ea-91f3-fb9e73f9bbe9", + "query": Object { + "language": "kuery", + "query": "play_name:\\"Henry IV\\"", + }, + "version": true, + } + `); + }); + + test('skips injecting references if none exists', () => { + // @ts-expect-error + delete searchSourceJSON.indexRefName; + references = []; + const actual = injectReferences(searchSourceJSON, references); + expect(actual).toMatchInlineSnapshot(` + Object { + "highlightAll": true, + "query": Object { + "language": "kuery", + "query": "play_name:\\"Henry IV\\"", + }, + "version": true, + } + `); + }); + + test('throws an error if there is a broken reference', () => { + searchSourceJSON.indexRefName = 'oops'; + expect(() => injectReferences(searchSourceJSON, references)).toThrowErrorMatchingInlineSnapshot( + `"Could not find reference for oops"` + ); + }); + + test('handles filters', () => { + searchSourceJSON.filter = [ + // @ts-expect-error + { meta: { indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index' } }, + ]; + const actual = injectReferences(searchSourceJSON, references); + expect(actual).toMatchInlineSnapshot(` + Object { + "filter": Array [ + Object { + "meta": Object { + "index": "033af690-fde7-11ea-91f3-fb9e73f9bbe9", + }, + }, + ], + "highlightAll": true, + "index": "033af690-fde7-11ea-91f3-fb9e73f9bbe9", + "query": Object { + "language": "kuery", + "query": "play_name:\\"Henry IV\\"", + }, + "version": true, + } + `); + }); + + test('throws an error if there is a broken filter reference', () => { + // @ts-expect-error + searchSourceJSON.filter = [{ meta: { indexRefName: 'oops' } }]; + expect(() => injectReferences(searchSourceJSON, references)).toThrowErrorMatchingInlineSnapshot( + `"Could not find reference for oops"` + ); + }); +}); diff --git a/src/plugins/data/common/search/search_source/legacy/types.ts b/src/plugins/data/common/search/search_source/legacy/types.ts index 1a0a96a76a70..8ac713a65893 100644 --- a/src/plugins/data/common/search/search_source/legacy/types.ts +++ b/src/plugins/data/common/search/search_source/legacy/types.ts @@ -18,15 +18,36 @@ */ import { BehaviorSubject } from 'rxjs'; +import { ApiResponse } from '@elastic/elasticsearch'; import { SearchResponse } from 'elasticsearch'; import { FetchHandlers, SearchRequest } from '../fetch'; +interface MsearchHeaders { + index: string; + preference?: number | string; +} + +interface MsearchRequest { + header: MsearchHeaders; + body: any; +} + +// @internal +export interface MsearchRequestBody { + searches: MsearchRequest[]; +} + +// @internal +export interface MsearchResponse { + body: ApiResponse<{ responses: Array> }>; +} + // @internal export interface LegacyFetchHandlers { callMsearch: (params: { - body: SearchRequest; + body: MsearchRequestBody; signal: AbortSignal; - }) => Promise>>; + }) => Promise; loadingCount$: BehaviorSubject; } diff --git a/src/plugins/data/common/search/search_source/mocks.ts b/src/plugins/data/common/search/search_source/mocks.ts index f582861e37c1..d4c0707f950b 100644 --- a/src/plugins/data/common/search/search_source/mocks.ts +++ b/src/plugins/data/common/search/search_source/mocks.ts @@ -20,8 +20,8 @@ import { BehaviorSubject } from 'rxjs'; import { uiSettingsServiceMock } from '../../../../../core/public/mocks'; -import { ISearchSource, SearchSource } from './search_source'; -import { SearchSourceFields } from './types'; +import { SearchSource } from './search_source'; +import { ISearchStartSearchSource, ISearchSource, SearchSourceFields } from './types'; export const searchSourceInstanceMock: MockedKeys = { setPreferredSearchStrategyId: jest.fn(), @@ -45,7 +45,7 @@ export const searchSourceInstanceMock: MockedKeys = { serialize: jest.fn(), }; -export const searchSourceMock = { +export const searchSourceCommonMock: jest.Mocked = { create: jest.fn().mockReturnValue(searchSourceInstanceMock), createEmpty: jest.fn().mockReturnValue(searchSourceInstanceMock), }; diff --git a/src/plugins/data/common/search/search_source/search_source.test.ts b/src/plugins/data/common/search/search_source/search_source.test.ts index 74abd9238bc2..00e06663e998 100644 --- a/src/plugins/data/common/search/search_source/search_source.test.ts +++ b/src/plugins/data/common/search/search_source/search_source.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Observable, BehaviorSubject } from 'rxjs'; +import { BehaviorSubject } from 'rxjs'; import { IndexPattern } from '../../index_patterns'; import { GetConfigFn } from '../../types'; import { fetchSoon } from './legacy'; @@ -53,16 +53,7 @@ describe('SearchSource', () => { let searchSourceDependencies: SearchSourceDependencies; beforeEach(() => { - mockSearchMethod = jest.fn(() => { - return new Observable((subscriber) => { - setTimeout(() => { - subscriber.next({ - rawResponse: '', - }); - subscriber.complete(); - }, 100); - }); - }); + mockSearchMethod = jest.fn().mockResolvedValue({ rawResponse: '' }); searchSourceDependencies = { getConfig: jest.fn(), diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts index d8a036ce970d..3eafea06f9f5 100644 --- a/src/plugins/data/common/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -71,22 +71,15 @@ import { setWith } from '@elastic/safer-lodash-set'; import { uniqueId, uniq, extend, pick, difference, omit, isObject, keys, isFunction } from 'lodash'; -import { map } from 'rxjs/operators'; import { normalizeSortRequest } from './normalize_sort_request'; import { filterDocvalueFields } from './filter_docvalue_fields'; import { fieldWildcardFilter } from '../../../../kibana_utils/common'; import { IIndexPattern } from '../../index_patterns'; -import { ISearchGeneric } from '../..'; -import { SearchSourceOptions, SearchSourceFields } from './types'; +import { IEsSearchRequest, IEsSearchResponse, ISearchOptions } from '../..'; +import { ISearchSource, SearchSourceOptions, SearchSourceFields } from './types'; import { FetchHandlers, RequestFailure, getSearchParamsFromRequest, SearchRequest } from './fetch'; -import { - getEsQueryConfig, - buildEsQuery, - Filter, - UI_SETTINGS, - ISearchOptions, -} from '../../../common'; +import { getEsQueryConfig, buildEsQuery, Filter, UI_SETTINGS } from '../../../common'; import { getHighlightRequest } from '../../../common/field_formats'; import { fetchSoon } from './legacy'; import { extractReferences } from './extract_references'; @@ -108,7 +101,7 @@ export const searchSourceRequiredUiSettings = [ ]; export interface SearchSourceDependencies extends FetchHandlers { - search: ISearchGeneric; + search: (request: IEsSearchRequest, options: ISearchOptions) => Promise; } /** @public **/ @@ -264,7 +257,7 @@ export class SearchSource { if (getConfig(UI_SETTINGS.COURIER_BATCH_SEARCHES)) { response = await this.legacyFetch(searchRequest, options); } else { - response = await this.fetch$(searchRequest, options).toPromise(); + response = await this.fetchSearch(searchRequest, options); } // TODO: Remove casting when https://github.com/elastic/elasticsearch-js/issues/1287 is resolved @@ -308,17 +301,17 @@ export class SearchSource { /** * Run a search using the search service - * @return {Observable>} + * @return {Promise>} */ - private fetch$(searchRequest: SearchRequest, options: ISearchOptions) { + private fetchSearch(searchRequest: SearchRequest, options: ISearchOptions) { const { search, getConfig, onResponse } = this.dependencies; const params = getSearchParamsFromRequest(searchRequest, { getConfig, }); - return search({ params, indexType: searchRequest.indexType }, options).pipe( - map(({ rawResponse }) => onResponse(searchRequest, rawResponse)) + return search({ params, indexType: searchRequest.indexType }, options).then(({ rawResponse }) => + onResponse(searchRequest, rawResponse) ); } @@ -558,9 +551,3 @@ export class SearchSource { return [filterField]; } } - -/** - * search source interface - * @public - */ -export type ISearchSource = Pick; diff --git a/src/plugins/data/common/search/search_source/search_source_service.test.ts b/src/plugins/data/common/search/search_source/search_source_service.test.ts new file mode 100644 index 000000000000..bbbaac11bbe9 --- /dev/null +++ b/src/plugins/data/common/search/search_source/search_source_service.test.ts @@ -0,0 +1,50 @@ +/* + * 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 { BehaviorSubject } from 'rxjs'; +import { IndexPatternsContract } from '../../index_patterns/index_patterns'; +import { SearchSourceService, SearchSourceDependencies } from './'; + +describe('SearchSource service', () => { + let dependencies: jest.Mocked; + + beforeEach(() => { + jest.resetModules(); + dependencies = { + getConfig: jest.fn(), + search: jest.fn(), + onResponse: jest.fn(), + legacy: { + callMsearch: jest.fn(), + loadingCount$: new BehaviorSubject(0), + }, + }; + }); + + describe('start()', () => { + test('exposes proper contract', () => { + const start = new SearchSourceService().start( + (jest.fn() as unknown) as jest.Mocked, + dependencies + ); + + expect(Object.keys(start)).toEqual(['create', 'createEmpty']); + }); + }); +}); diff --git a/src/plugins/data/common/search/search_source/search_source_service.ts b/src/plugins/data/common/search/search_source/search_source_service.ts new file mode 100644 index 000000000000..40981402419c --- /dev/null +++ b/src/plugins/data/common/search/search_source/search_source_service.ts @@ -0,0 +1,42 @@ +/* + * 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 { createSearchSource, SearchSource, SearchSourceDependencies } from './'; +import { IndexPatternsContract } from '../../index_patterns/index_patterns'; + +export class SearchSourceService { + public setup() {} + + public start(indexPatterns: IndexPatternsContract, dependencies: SearchSourceDependencies) { + return { + /** + * creates searchsource based on serialized search source fields + */ + create: createSearchSource(indexPatterns, dependencies), + /** + * creates an enpty search source + */ + createEmpty: () => { + return new SearchSource({}, dependencies); + }, + }; + } + + public stop() {} +} diff --git a/src/plugins/data/common/search/search_source/types.ts b/src/plugins/data/common/search/search_source/types.ts index 0882aa9a2cee..bbfc308e0459 100644 --- a/src/plugins/data/common/search/search_source/types.ts +++ b/src/plugins/data/common/search/search_source/types.ts @@ -16,9 +16,32 @@ * specific language governing permissions and limitations * under the License. */ + import { NameList } from 'elasticsearch'; -import { IndexPattern, Query } from '../..'; -import { Filter } from '../../../common'; +import { Filter, IndexPattern, Query } from '../..'; +import { SearchSource } from './search_source'; + +/** + * search source interface + * @public + */ +export type ISearchSource = Pick; + +/** + * high level search service + * @public + */ +export interface ISearchStartSearchSource { + /** + * creates {@link SearchSource} based on provided serialized {@link SearchSourceFields} + * @param fields + */ + create: (fields?: SearchSourceFields) => Promise; + /** + * creates empty {@link SearchSource} + */ + createEmpty: () => ISearchSource; +} export type EsQuerySearchAfter = [string | number, string | number]; @@ -75,8 +98,6 @@ export interface SearchSourceOptions { callParentStartHandlers?: boolean; } -export { ISearchSource } from './search_source'; - export interface SortOptions { mode?: 'min' | 'max' | 'sum' | 'avg' | 'median'; type?: 'double' | 'long' | 'date' | 'date_nanos'; diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index 0eb0e3b65804..1b83eb569b1a 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -78,7 +78,7 @@ const createStartContract = (): Start => { }; }; -export { createSearchSourceMock } from './search/mocks'; +export { createSearchSourceMock } from '../common/search/search_source/mocks'; export { getCalculateAutoTimeExpression } from '../common/search/aggs'; export const dataPluginMock = { diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 1ee453a0f141..947ba8224625 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -7,7 +7,8 @@ import { $Values } from '@kbn/utility-types'; import _ from 'lodash'; import { Action } from 'history'; -import { ApiResponse } from '@elastic/elasticsearch/lib/Transport'; +import { ApiResponse } from '@elastic/elasticsearch'; +import { ApiResponse as ApiResponse_2 } from '@elastic/elasticsearch/lib/Transport'; import { ApplicationStart } from 'kibana/public'; import { Assign } from '@kbn/utility-types'; import { BehaviorSubject } from 'rxjs'; diff --git a/src/plugins/data/public/search/mocks.ts b/src/plugins/data/public/search/mocks.ts index e931b39eae2a..8bad4cd269b3 100644 --- a/src/plugins/data/public/search/mocks.ts +++ b/src/plugins/data/public/search/mocks.ts @@ -18,10 +18,8 @@ */ import { searchAggsSetupMock, searchAggsStartMock } from './aggs/mocks'; +import { searchSourceMock } from './search_source/mocks'; import { ISearchSetup, ISearchStart } from './types'; -import { searchSourceMock, createSearchSourceMock } from '../../common/search/search_source/mocks'; - -export * from '../../common/search/search_source/mocks'; function createSetupContract(): jest.Mocked { return { @@ -35,7 +33,7 @@ function createStartContract(): jest.Mocked { aggs: searchAggsStartMock(), search: jest.fn(), showError: jest.fn(), - searchSource: searchSourceMock, + searchSource: searchSourceMock.createStartContract(), }; } @@ -43,5 +41,3 @@ export const searchServiceMock = { createSetupContract, createStartContract, }; - -export { createSearchSourceMock }; diff --git a/src/plugins/data/public/search/search_service.test.ts b/src/plugins/data/public/search/search_service.test.ts index 15356fb1a31b..b59fa6fa16bf 100644 --- a/src/plugins/data/public/search/search_service.test.ts +++ b/src/plugins/data/public/search/search_service.test.ts @@ -52,6 +52,7 @@ describe('Search service', () => { describe('start()', () => { it('exposes proper contract', async () => { const start = searchService.start(mockCoreStart, { + fieldFormats: {}, indexPatterns: {}, } as any); expect(start).toHaveProperty('aggs'); diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index dc0568af5d4a..5014418d6b70 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -23,9 +23,10 @@ import { ISearchSetup, ISearchStart, SearchEnhancements } from './types'; import { handleResponse } from './fetch'; import { - createSearchSource, + IEsSearchRequest, ISearchGeneric, - SearchSource, + ISearchOptions, + SearchSourceService, SearchSourceDependencies, } from '../../common/search'; import { getCallMsearch } from './legacy'; @@ -57,6 +58,7 @@ export interface SearchServiceStartDependencies { export class SearchService implements Plugin { private readonly aggsService = new AggsService(); + private readonly searchSourceService = new SearchSourceService(); private searchInterceptor!: ISearchInterceptor; private usageCollector?: SearchUsageCollector; @@ -115,7 +117,9 @@ export class SearchService implements Plugin { const searchSourceDependencies: SearchSourceDependencies = { getConfig: uiSettings.get.bind(uiSettings), - search, + search: (request: IEsSearchRequest, options: ISearchOptions) => { + return search(request, options).toPromise(); + }, onResponse: handleResponse, legacy: { callMsearch: getCallMsearch({ http }), @@ -129,22 +133,12 @@ export class SearchService implements Plugin { showError: (e: Error) => { this.searchInterceptor.showError(e); }, - searchSource: { - /** - * creates searchsource based on serialized search source fields - */ - create: createSearchSource(indexPatterns, searchSourceDependencies), - /** - * creates an enpty search source - */ - createEmpty: () => { - return new SearchSource({}, searchSourceDependencies); - }, - }, + searchSource: this.searchSourceService.start(indexPatterns, searchSourceDependencies), }; } public stop() { this.aggsService.stop(); + this.searchSourceService.stop(); } } diff --git a/src/plugins/data/public/search/search_source/mocks.ts b/src/plugins/data/public/search/search_source/mocks.ts new file mode 100644 index 000000000000..bc382f09947c --- /dev/null +++ b/src/plugins/data/public/search/search_source/mocks.ts @@ -0,0 +1,29 @@ +/* + * 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 { searchSourceCommonMock } from '../../../common/search/search_source/mocks'; +import { ISearchStart } from '../types'; + +function createStartContract(): jest.Mocked { + return searchSourceCommonMock; +} + +export const searchSourceMock = { + createStartContract, +}; diff --git a/src/plugins/data/public/search/types.ts b/src/plugins/data/public/search/types.ts index a133a8cd4be9..85ef7aa4d97c 100644 --- a/src/plugins/data/public/search/types.ts +++ b/src/plugins/data/public/search/types.ts @@ -21,10 +21,12 @@ import { PackageInfo } from 'kibana/server'; import { ISearchInterceptor } from './search_interceptor'; import { SearchUsageCollector } from './collectors'; import { AggsSetup, AggsSetupDependencies, AggsStartDependencies, AggsStart } from './aggs'; -import { ISearchGeneric, ISearchSource, SearchSourceFields } from '../../common/search'; +import { ISearchGeneric, ISearchStartSearchSource } from '../../common/search'; import { IndexPatternsContract } from '../../common/index_patterns/index_patterns'; import { UsageCollectionSetup } from '../../../usage_collection/public'; +export { ISearchStartSearchSource }; + export interface SearchEnhancements { searchInterceptor: ISearchInterceptor; } @@ -42,21 +44,6 @@ export interface ISearchSetup { __enhance: (enhancements: SearchEnhancements) => void; } -/** - * high level search service - * @public - */ -export interface ISearchStartSearchSource { - /** - * creates {@link SearchSource} based on provided serialized {@link SearchSourceFields} - * @param fields - */ - create: (fields?: SearchSourceFields) => Promise; - /** - * creates empty {@link SearchSource} - */ - createEmpty: () => ISearchSource; -} /** * search service * @public diff --git a/src/plugins/data/server/plugin.ts b/src/plugins/data/server/plugin.ts index 588885391262..88f24b7ca5a7 100644 --- a/src/plugins/data/server/plugin.ts +++ b/src/plugins/data/server/plugin.ts @@ -111,13 +111,15 @@ export class DataServerPlugin public start(core: CoreStart) { const fieldFormats = this.fieldFormats.start(); - return { - search: this.searchService.start(core, { fieldFormats }), + const indexPatterns = this.indexPatterns.start(core, { fieldFormats, - indexPatterns: this.indexPatterns.start(core, { - fieldFormats, - logger: this.logger.get('indexPatterns'), - }), + logger: this.logger.get('indexPatterns'), + }); + + return { + fieldFormats, + indexPatterns, + search: this.searchService.start(core, { fieldFormats, indexPatterns }), }; } diff --git a/src/plugins/data/server/search/mocks.ts b/src/plugins/data/server/search/mocks.ts index 0c74ecb4b2c9..0d4ba0cba24a 100644 --- a/src/plugins/data/server/search/mocks.ts +++ b/src/plugins/data/server/search/mocks.ts @@ -19,6 +19,7 @@ import { ISearchSetup, ISearchStart } from './types'; import { searchAggsSetupMock, searchAggsStartMock } from './aggs/mocks'; +import { searchSourceMock } from './search_source/mocks'; export function createSearchSetupMock(): jest.Mocked { return { @@ -33,5 +34,6 @@ export function createSearchStartMock(): jest.Mocked { aggs: searchAggsStartMock(), getSearchStrategy: jest.fn(), search: jest.fn(), + searchSource: searchSourceMock.createStartContract(), }; } diff --git a/src/plugins/data/server/search/routes/call_msearch.test.ts b/src/plugins/data/server/search/routes/call_msearch.test.ts new file mode 100644 index 000000000000..3d409e22aaa8 --- /dev/null +++ b/src/plugins/data/server/search/routes/call_msearch.test.ts @@ -0,0 +1,101 @@ +/* + * 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 { Observable } from 'rxjs'; +import { IUiSettingsClient, IScopedClusterClient, SharedGlobalConfig } from 'src/core/server'; + +import { + coreMock, + pluginInitializerContextConfigMock, +} from '../../../../../../src/core/server/mocks'; +import { convertRequestBody, getCallMsearch } from './call_msearch'; + +describe('callMsearch', () => { + let esClient: DeeplyMockedKeys; + let globalConfig$: Observable; + let uiSettings: IUiSettingsClient; + let callMsearch: ReturnType; + + beforeEach(() => { + const coreContext = coreMock.createRequestHandlerContext(); + esClient = coreContext.elasticsearch.client; + globalConfig$ = pluginInitializerContextConfigMock({}).legacy.globalConfig$; + uiSettings = coreContext.uiSettings.client; + + callMsearch = getCallMsearch({ esClient, globalConfig$, uiSettings }); + }); + + it('handler calls msearch with the given request', async () => { + const mockBody = { + searches: [{ header: { index: 'foo' }, body: { query: { match_all: {} } } }], + }; + + await callMsearch({ + body: mockBody, + signal: new AbortController().signal, + }); + + expect(esClient.asCurrentUser.msearch).toHaveBeenCalledTimes(1); + expect(esClient.asCurrentUser.msearch.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "body": "{\\"ignore_unavailable\\":true,\\"index\\":\\"foo\\"} + {\\"query\\":{\\"match_all\\":{}}} + ", + }, + Object { + "querystring": Object { + "ignore_throttled": true, + "ignore_unavailable": true, + "max_concurrent_shard_requests": undefined, + }, + }, + ] + `); + }); + + describe('convertRequestBody', () => { + it('combines header & body into proper msearch request', () => { + const request = { + searches: [{ header: { index: 'foo', preference: 0 }, body: { test: true } }], + }; + expect(convertRequestBody(request, { timeout: '30000ms' })).toMatchInlineSnapshot(` + "{\\"ignore_unavailable\\":true,\\"index\\":\\"foo\\",\\"preference\\":0} + {\\"timeout\\":\\"30000ms\\",\\"test\\":true} + " + `); + }); + + it('handles multiple searches', () => { + const request = { + searches: [ + { header: { index: 'foo', preference: 0 }, body: { test: true } }, + { header: { index: 'bar', preference: 1 }, body: { hello: 'world' } }, + ], + }; + expect(convertRequestBody(request, { timeout: '30000ms' })).toMatchInlineSnapshot(` + "{\\"ignore_unavailable\\":true,\\"index\\":\\"foo\\",\\"preference\\":0} + {\\"timeout\\":\\"30000ms\\",\\"test\\":true} + {\\"ignore_unavailable\\":true,\\"index\\":\\"bar\\",\\"preference\\":1} + {\\"timeout\\":\\"30000ms\\",\\"hello\\":\\"world\\"} + " + `); + }); + }); +}); diff --git a/src/plugins/data/server/search/routes/call_msearch.ts b/src/plugins/data/server/search/routes/call_msearch.ts new file mode 100644 index 000000000000..764dcd189f8d --- /dev/null +++ b/src/plugins/data/server/search/routes/call_msearch.ts @@ -0,0 +1,100 @@ +/* + * 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 { Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; +import { ApiResponse } from '@elastic/elasticsearch'; +import { SearchResponse } from 'elasticsearch'; +import { IUiSettingsClient, IScopedClusterClient, SharedGlobalConfig } from 'src/core/server'; + +import { MsearchRequestBody, MsearchResponse } from '../../../common/search/search_source'; +import { shimHitsTotal } from './shim_hits_total'; +import { getShardTimeout, getDefaultSearchParams, toSnakeCase } from '..'; + +/** @internal */ +export function convertRequestBody( + requestBody: MsearchRequestBody, + { timeout }: { timeout?: string } +): string { + return requestBody.searches.reduce((req, curr) => { + const header = JSON.stringify({ + ignore_unavailable: true, + ...curr.header, + }); + const body = JSON.stringify({ + timeout, + ...curr.body, + }); + return `${req}${header}\n${body}\n`; + }, ''); +} + +interface CallMsearchDependencies { + esClient: IScopedClusterClient; + globalConfig$: Observable; + uiSettings: IUiSettingsClient; +} + +/** + * Helper for the `/internal/_msearch` route, exported separately here + * so that it can be reused elsewhere in the data plugin on the server, + * e.g. SearchSource + * + * @internal + */ +export function getCallMsearch(dependencies: CallMsearchDependencies) { + return async (params: { + body: MsearchRequestBody; + signal?: AbortSignal; + }): Promise => { + const { esClient, globalConfig$, uiSettings } = dependencies; + + // get shardTimeout + const config = await globalConfig$.pipe(first()).toPromise(); + const timeout = getShardTimeout(config); + + // trackTotalHits is not supported by msearch + const { trackTotalHits, ...defaultParams } = await getDefaultSearchParams(uiSettings); + + const body = convertRequestBody(params.body, timeout); + + // Temporary workaround until https://github.com/elastic/elasticsearch-js/issues/1297 + const promise = esClient.asCurrentUser.msearch( + { + body, + }, + { + querystring: toSnakeCase(defaultParams), + } + ); + if (params.signal) { + params.signal.addEventListener('abort', () => promise.abort()); + } + const response = (await promise) as ApiResponse<{ responses: Array> }>; + + return { + body: { + ...response, + body: { + responses: response.body.responses?.map((r: SearchResponse) => shimHitsTotal(r)), + }, + }, + }; + }; +} diff --git a/src/plugins/data/server/search/routes/index.ts b/src/plugins/data/server/search/routes/index.ts index a290f08f9b84..f858653f2643 100644 --- a/src/plugins/data/server/search/routes/index.ts +++ b/src/plugins/data/server/search/routes/index.ts @@ -17,6 +17,7 @@ * under the License. */ +export * from './call_msearch'; export * from './msearch'; export * from './search'; export * from './shim_hits_total'; diff --git a/src/plugins/data/server/search/routes/msearch.test.ts b/src/plugins/data/server/search/routes/msearch.test.ts index 3a7d67c31b8b..2f414fe0b492 100644 --- a/src/plugins/data/server/search/routes/msearch.test.ts +++ b/src/plugins/data/server/search/routes/msearch.test.ts @@ -30,7 +30,8 @@ import { httpServerMock, pluginInitializerContextConfigMock, } from '../../../../../../src/core/server/mocks'; -import { registerMsearchRoute, convertRequestBody } from './msearch'; +import { convertRequestBody } from './call_msearch'; +import { registerMsearchRoute } from './msearch'; import { DataPluginStart } from '../../plugin'; import { dataPluginMock } from '../../mocks'; @@ -49,7 +50,7 @@ describe('msearch route', () => { it('handler calls /_msearch with the given request', async () => { const response = { id: 'yay', body: { responses: [{ hits: { total: 5 } }] } }; - const mockClient = { transport: { request: jest.fn().mockResolvedValue(response) } }; + const mockClient = { msearch: jest.fn().mockResolvedValue(response) }; const mockContext = { core: { elasticsearch: { client: { asCurrentUser: mockClient } }, @@ -70,11 +71,16 @@ describe('msearch route', () => { const handler = mockRouter.post.mock.calls[0][1]; await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse); - expect(mockClient.transport.request.mock.calls[0][0].method).toBe('GET'); - expect(mockClient.transport.request.mock.calls[0][0].path).toBe('/_msearch'); - expect(mockClient.transport.request.mock.calls[0][0].body).toEqual( + expect(mockClient.msearch.mock.calls[0][0].body).toEqual( convertRequestBody(mockBody as any, {}) ); + expect(mockClient.msearch.mock.calls[0][1].querystring).toMatchInlineSnapshot(` + Object { + "ignore_throttled": true, + "ignore_unavailable": true, + "max_concurrent_shard_requests": undefined, + } + `); expect(mockResponse.ok).toBeCalled(); expect(mockResponse.ok.mock.calls[0][0]).toEqual({ body: response, @@ -89,7 +95,7 @@ describe('msearch route', () => { }, }; const mockClient = { - transport: { request: jest.fn().mockReturnValue(Promise.reject(response)) }, + msearch: jest.fn().mockReturnValue(Promise.reject(response)), }; const mockContext = { core: { @@ -111,40 +117,11 @@ describe('msearch route', () => { const handler = mockRouter.post.mock.calls[0][1]; await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse); - expect(mockClient.transport.request).toBeCalled(); + expect(mockClient.msearch).toBeCalled(); expect(mockResponse.customError).toBeCalled(); const error: any = mockResponse.customError.mock.calls[0][0]; expect(error.body.message).toBe('oh no'); expect(error.body.attributes.error).toBe('oops'); }); - - describe('convertRequestBody', () => { - it('combines header & body into proper msearch request', () => { - const request = { - searches: [{ header: { index: 'foo', preference: 0 }, body: { test: true } }], - }; - expect(convertRequestBody(request, { timeout: '30000ms' })).toMatchInlineSnapshot(` - "{\\"ignore_unavailable\\":true,\\"index\\":\\"foo\\",\\"preference\\":0} - {\\"timeout\\":\\"30000ms\\",\\"test\\":true} - " - `); - }); - - it('handles multiple searches', () => { - const request = { - searches: [ - { header: { index: 'foo', preference: 0 }, body: { test: true } }, - { header: { index: 'bar', preference: 1 }, body: { hello: 'world' } }, - ], - }; - expect(convertRequestBody(request, { timeout: '30000ms' })).toMatchInlineSnapshot(` - "{\\"ignore_unavailable\\":true,\\"index\\":\\"foo\\",\\"preference\\":0} - {\\"timeout\\":\\"30000ms\\",\\"test\\":true} - {\\"ignore_unavailable\\":true,\\"index\\":\\"bar\\",\\"preference\\":1} - {\\"timeout\\":\\"30000ms\\",\\"hello\\":\\"world\\"} - " - `); - }); - }); }); diff --git a/src/plugins/data/server/search/routes/msearch.ts b/src/plugins/data/server/search/routes/msearch.ts index e1ddb06e4fb6..7b44aa18bf8f 100644 --- a/src/plugins/data/server/search/routes/msearch.ts +++ b/src/plugins/data/server/search/routes/msearch.ts @@ -17,46 +17,12 @@ * under the License. */ -import { first } from 'rxjs/operators'; import { schema } from '@kbn/config-schema'; -import { SearchResponse } from 'elasticsearch'; import { IRouter } from 'src/core/server'; import { SearchRouteDependencies } from '../search_service'; -import { shimHitsTotal } from './shim_hits_total'; -import { getShardTimeout, getDefaultSearchParams, toSnakeCase } from '..'; -interface MsearchHeaders { - index: string; - preference?: number | string; -} - -interface MsearchRequest { - header: MsearchHeaders; - body: any; -} - -interface RequestBody { - searches: MsearchRequest[]; -} - -/** @internal */ -export function convertRequestBody( - requestBody: RequestBody, - { timeout }: { timeout?: string } -): string { - return requestBody.searches.reduce((req, curr) => { - const header = JSON.stringify({ - ignore_unavailable: true, - ...curr.header, - }); - const body = JSON.stringify({ - timeout, - ...curr.body, - }); - return `${req}${header}\n${body}\n`; - }, ''); -} +import { getCallMsearch } from './call_msearch'; /** * The msearch route takes in an array of searches, each consisting of header @@ -93,35 +59,15 @@ export function registerMsearchRoute(router: IRouter, deps: SearchRouteDependenc }, }, async (context, request, res) => { - const client = context.core.elasticsearch.client.asCurrentUser; - - // get shardTimeout - const config = await deps.globalConfig$.pipe(first()).toPromise(); - const timeout = getShardTimeout(config); - - const body = convertRequestBody(request.body, timeout); - - // trackTotalHits is not supported by msearch - const { trackTotalHits, ...defaultParams } = await getDefaultSearchParams( - context.core.uiSettings.client - ); + const callMsearch = getCallMsearch({ + esClient: context.core.elasticsearch.client, + globalConfig$: deps.globalConfig$, + uiSettings: context.core.uiSettings.client, + }); try { - const response = await client.transport.request({ - method: 'GET', - path: '/_msearch', - body, - querystring: toSnakeCase(defaultParams), - }); - - return res.ok({ - body: { - ...response, - body: { - responses: response.body.responses?.map((r: SearchResponse) => shimHitsTotal(r)), - }, - }, - }); + const response = await callMsearch({ body: request.body }); + return res.ok(response); } catch (err) { return res.customError({ statusCode: err.statusCode || 500, diff --git a/src/plugins/data/server/search/search_service.test.ts b/src/plugins/data/server/search/search_service.test.ts index 7057c9c7ca15..a001d56b3651 100644 --- a/src/plugins/data/server/search/search_service.test.ts +++ b/src/plugins/data/server/search/search_service.test.ts @@ -22,6 +22,7 @@ import { coreMock } from '../../../../core/server/mocks'; import { DataPluginStart } from '../plugin'; import { createFieldFormatsStartMock } from '../field_formats/mocks'; +import { createIndexPatternsStartMock } from '../index_patterns/mocks'; import { SearchService, SearchServiceSetupDependencies } from './search_service'; @@ -54,6 +55,7 @@ describe('Search service', () => { it('exposes proper contract', async () => { const start = plugin.start(mockCoreStart, { fieldFormats: createFieldFormatsStartMock(), + indexPatterns: createIndexPatternsStartMock(), }); expect(start).toHaveProperty('aggs'); expect(start).toHaveProperty('getSearchStrategy'); diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index 285146e510f8..1a9e7d83bc95 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -17,10 +17,12 @@ * under the License. */ -import { Observable } from 'rxjs'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { pick } from 'lodash'; import { CoreSetup, CoreStart, + KibanaRequest, Logger, Plugin, PluginInitializerContext, @@ -34,7 +36,8 @@ import { ISearchSetup, ISearchStart, ISearchStrategy, SearchEnhancements } from import { AggsService, AggsSetupDependencies } from './aggs'; import { FieldFormatsStart } from '../field_formats'; -import { registerMsearchRoute, registerSearchRoute } from './routes'; +import { IndexPatternsServiceStart } from '../index_patterns'; +import { getCallMsearch, registerMsearchRoute, registerSearchRoute } from './routes'; import { ES_SEARCH_STRATEGY, esSearchStrategyProvider } from './es_search'; import { DataPluginStart } from '../plugin'; import { UsageCollectionSetup } from '../../../usage_collection/server'; @@ -47,13 +50,16 @@ import { IEsSearchRequest, IEsSearchResponse, ISearchOptions, -} from '../../common'; + SearchSourceDependencies, + SearchSourceService, + searchSourceRequiredUiSettings, +} from '../../common/search'; import { getShardDelayBucketAgg, SHARD_DELAY_AGG_NAME, } from '../../common/search/aggs/buckets/shard_delay'; -import { ConfigSchema } from '../../config'; import { aggShardDelay } from '../../common/search/aggs/buckets/shard_delay_fn'; +import { ConfigSchema } from '../../config'; type StrategyMap = Record>; @@ -66,6 +72,7 @@ export interface SearchServiceSetupDependencies { /** @internal */ export interface SearchServiceStartDependencies { fieldFormats: FieldFormatsStart; + indexPatterns: IndexPatternsServiceStart; } /** @internal */ @@ -76,6 +83,7 @@ export interface SearchRouteDependencies { export class SearchService implements Plugin { private readonly aggsService = new AggsService(); + private readonly searchSourceService = new SearchSourceService(); private defaultSearchStrategyName: string = ES_SEARCH_STRATEGY; private searchStrategies: StrategyMap = {}; @@ -137,8 +145,8 @@ export class SearchService implements Plugin { }; } public start( - { uiSettings }: CoreStart, - { fieldFormats }: SearchServiceStartDependencies + { elasticsearch, savedObjects, uiSettings }: CoreStart, + { fieldFormats, indexPatterns }: SearchServiceStartDependencies ): ISearchStart { return { aggs: this.aggsService.start({ fieldFormats, uiSettings }), @@ -150,6 +158,58 @@ export class SearchService implements Plugin { ) => { return this.search(context, searchRequest, options); }, + searchSource: { + asScoped: async (request: KibanaRequest) => { + const esClient = elasticsearch.client.asScoped(request); + const savedObjectsClient = savedObjects.getScopedClient(request); + const scopedIndexPatterns = await indexPatterns.indexPatternsServiceFactory(request); + const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); + + // cache ui settings, only including items which are explicitly needed by SearchSource + const uiSettingsCache = pick( + await uiSettingsClient.getAll(), + searchSourceRequiredUiSettings + ); + + const searchSourceDependencies: SearchSourceDependencies = { + getConfig: (key: string): T => uiSettingsCache[key], + search: (searchRequest, options) => { + /** + * Unless we want all SearchSource users to provide both a KibanaRequest + * (needed for index patterns) AND the RequestHandlerContext (needed for + * low-level search), we need to fake the context as it can be derived + * from the request object anyway. This will pose problems for folks who + * are registering custom search strategies as they are only getting a + * subset of the entire context. Ideally low-level search should be + * refactored to only require the needed dependencies: esClient & uiSettings. + */ + const fakeRequestHandlerContext = { + core: { + elasticsearch: { + client: esClient, + }, + uiSettings: { + client: uiSettingsClient, + }, + }, + } as RequestHandlerContext; + return this.search(fakeRequestHandlerContext, searchRequest, options); + }, + // onResponse isn't used on the server, so we just return the original value + onResponse: (req, res) => res, + legacy: { + callMsearch: getCallMsearch({ + esClient, + globalConfig$: this.initializerContext.config.legacy.globalConfig$, + uiSettings: uiSettingsClient, + }), + loadingCount$: new BehaviorSubject(0), + }, + }; + + return this.searchSourceService.start(scopedIndexPatterns, searchSourceDependencies); + }, + }, }; } diff --git a/src/plugins/data/server/search/search_source/mocks.ts b/src/plugins/data/server/search/search_source/mocks.ts new file mode 100644 index 000000000000..7e9cc8f2ff42 --- /dev/null +++ b/src/plugins/data/server/search/search_source/mocks.ts @@ -0,0 +1,35 @@ +/* + * 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 { KibanaRequest } from 'src/core/server'; + +import { searchSourceCommonMock } from '../../../common/search/search_source/mocks'; +import { ISearchStart } from '../types'; + +function createStartContract(): MockedKeys { + return { + asScoped: async (request: jest.Mocked) => { + return searchSourceCommonMock; + }, + }; +} + +export const searchSourceMock = { + createStartContract, +}; diff --git a/src/plugins/data/server/search/types.ts b/src/plugins/data/server/search/types.ts index 4764bd77278a..0de4ef529e89 100644 --- a/src/plugins/data/server/search/types.ts +++ b/src/plugins/data/server/search/types.ts @@ -17,8 +17,13 @@ * under the License. */ -import { RequestHandlerContext } from '../../../../core/server'; -import { ISearchOptions, IKibanaSearchRequest, IKibanaSearchResponse } from '../../common/search'; +import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; +import { + ISearchOptions, + ISearchStartSearchSource, + IKibanaSearchRequest, + IKibanaSearchResponse, +} from '../../common/search'; import { AggsSetup, AggsStart } from './aggs'; import { SearchUsage } from './collectors'; import { IEsSearchRequest, IEsSearchResponse } from './es_search'; @@ -69,6 +74,9 @@ export interface ISearchStart< request: SearchStrategyRequest, options: ISearchOptions ) => Promise; + searchSource: { + asScoped: (request: KibanaRequest) => Promise; + }; } /** diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index eefc373559af..ae5159e7882d 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -5,173 +5,52 @@ ```ts import { $Values } from '@kbn/utility-types'; -import { ApiResponse } from '@elastic/elasticsearch/lib/Transport'; +import { ApiResponse } from '@elastic/elasticsearch'; import { Assign } from '@kbn/utility-types'; -import Boom from 'boom'; -import { BulkIndexDocumentsParams } from 'elasticsearch'; -import { CatAliasesParams } from 'elasticsearch'; -import { CatAllocationParams } from 'elasticsearch'; -import { CatCommonParams } from 'elasticsearch'; -import { CatFielddataParams } from 'elasticsearch'; -import { CatHealthParams } from 'elasticsearch'; -import { CatHelpParams } from 'elasticsearch'; -import { CatIndicesParams } from 'elasticsearch'; -import { CatRecoveryParams } from 'elasticsearch'; -import { CatSegmentsParams } from 'elasticsearch'; -import { CatShardsParams } from 'elasticsearch'; -import { CatSnapshotsParams } from 'elasticsearch'; -import { CatTasksParams } from 'elasticsearch'; -import { CatThreadPoolParams } from 'elasticsearch'; -import { ClearScrollParams } from 'elasticsearch'; -import { Client } from 'elasticsearch'; -import { ClusterAllocationExplainParams } from 'elasticsearch'; -import { ClusterGetSettingsParams } from 'elasticsearch'; -import { ClusterHealthParams } from 'elasticsearch'; -import { ClusterPendingTasksParams } from 'elasticsearch'; -import { ClusterPutSettingsParams } from 'elasticsearch'; -import { ClusterRerouteParams } from 'elasticsearch'; -import { ClusterStateParams } from 'elasticsearch'; -import { ClusterStatsParams } from 'elasticsearch'; +import { BehaviorSubject } from 'rxjs'; import { ConfigDeprecationProvider } from '@kbn/config'; import { CoreSetup } from 'src/core/server'; import { CoreSetup as CoreSetup_2 } from 'kibana/server'; import { CoreStart } from 'src/core/server'; import { CoreStart as CoreStart_2 } from 'kibana/server'; -import { CountParams } from 'elasticsearch'; -import { CreateDocumentParams } from 'elasticsearch'; -import { DeleteDocumentByQueryParams } from 'elasticsearch'; -import { DeleteDocumentParams } from 'elasticsearch'; -import { DeleteScriptParams } from 'elasticsearch'; -import { DeleteTemplateParams } from 'elasticsearch'; import { Duration } from 'moment'; -import { ElasticsearchClient as ElasticsearchClient_2 } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { Ensure } from '@kbn/utility-types'; import { EnvironmentMode } from '@kbn/config'; import { ErrorToastOptions } from 'src/core/public/notifications'; -import { ExistsParams } from 'elasticsearch'; -import { ExplainParams } from 'elasticsearch'; import { ExpressionAstFunction } from 'src/plugins/expressions/common'; import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; -import { FieldStatsParams } from 'elasticsearch'; -import { GenericParams } from 'elasticsearch'; -import { GetParams } from 'elasticsearch'; -import { GetResponse } from 'elasticsearch'; -import { GetScriptParams } from 'elasticsearch'; -import { GetSourceParams } from 'elasticsearch'; -import { GetTemplateParams } from 'elasticsearch'; -import { IncomingHttpHeaders } from 'http'; -import { IndexDocumentParams } from 'elasticsearch'; -import { IndicesAnalyzeParams } from 'elasticsearch'; -import { IndicesClearCacheParams } from 'elasticsearch'; -import { IndicesCloseParams } from 'elasticsearch'; -import { IndicesCreateParams } from 'elasticsearch'; -import { IndicesDeleteAliasParams } from 'elasticsearch'; -import { IndicesDeleteParams } from 'elasticsearch'; -import { IndicesDeleteTemplateParams } from 'elasticsearch'; -import { IndicesExistsAliasParams } from 'elasticsearch'; -import { IndicesExistsParams } from 'elasticsearch'; -import { IndicesExistsTemplateParams } from 'elasticsearch'; -import { IndicesExistsTypeParams } from 'elasticsearch'; -import { IndicesFlushParams } from 'elasticsearch'; -import { IndicesFlushSyncedParams } from 'elasticsearch'; -import { IndicesForcemergeParams } from 'elasticsearch'; -import { IndicesGetAliasParams } from 'elasticsearch'; -import { IndicesGetFieldMappingParams } from 'elasticsearch'; -import { IndicesGetMappingParams } from 'elasticsearch'; -import { IndicesGetParams } from 'elasticsearch'; -import { IndicesGetSettingsParams } from 'elasticsearch'; -import { IndicesGetTemplateParams } from 'elasticsearch'; -import { IndicesGetUpgradeParams } from 'elasticsearch'; -import { IndicesOpenParams } from 'elasticsearch'; -import { IndicesPutAliasParams } from 'elasticsearch'; -import { IndicesPutMappingParams } from 'elasticsearch'; -import { IndicesPutSettingsParams } from 'elasticsearch'; -import { IndicesPutTemplateParams } from 'elasticsearch'; -import { IndicesRecoveryParams } from 'elasticsearch'; -import { IndicesRefreshParams } from 'elasticsearch'; -import { IndicesRolloverParams } from 'elasticsearch'; -import { IndicesSegmentsParams } from 'elasticsearch'; -import { IndicesShardStoresParams } from 'elasticsearch'; -import { IndicesShrinkParams } from 'elasticsearch'; -import { IndicesStatsParams } from 'elasticsearch'; -import { IndicesUpdateAliasesParams } from 'elasticsearch'; -import { IndicesUpgradeParams } from 'elasticsearch'; -import { IndicesValidateQueryParams } from 'elasticsearch'; -import { InfoParams } from 'elasticsearch'; -import { IngestDeletePipelineParams } from 'elasticsearch'; -import { IngestGetPipelineParams } from 'elasticsearch'; -import { IngestPutPipelineParams } from 'elasticsearch'; -import { IngestSimulateParams } from 'elasticsearch'; import { ISearchOptions as ISearchOptions_2 } from 'src/plugins/data/public'; import { ISearchSource } from 'src/plugins/data/public'; -import { KibanaClient } from '@elastic/elasticsearch/api/kibana'; -import { KibanaConfigType as KibanaConfigType_2 } from 'src/core/server/kibana_config'; -import { KibanaRequest } from 'kibana/server'; -import { LegacyAPICaller as LegacyAPICaller_2 } from 'kibana/server'; -import { Logger } from '@kbn/logging'; -import { Logger as Logger_2 } from 'kibana/server'; +import { KibanaRequest } from 'src/core/server'; +import { KibanaRequest as KibanaRequest_2 } from 'kibana/server'; +import { LegacyAPICaller } from 'kibana/server'; +import { Logger } from 'kibana/server'; import { LoggerFactory } from '@kbn/logging'; -import { LogMeta } from '@kbn/logging'; -import { MGetParams } from 'elasticsearch'; -import { MGetResponse } from 'elasticsearch'; import { Moment } from 'moment'; import moment from 'moment'; -import { MSearchParams } from 'elasticsearch'; -import { MSearchResponse } from 'elasticsearch'; -import { MSearchTemplateParams } from 'elasticsearch'; -import { MTermVectorsParams } from 'elasticsearch'; -import { NodesHotThreadsParams } from 'elasticsearch'; -import { NodesInfoParams } from 'elasticsearch'; -import { NodesStatsParams } from 'elasticsearch'; +import { NameList } from 'elasticsearch'; import { Observable } from 'rxjs'; import { PackageInfo } from '@kbn/config'; import { PathConfigType } from '@kbn/utils'; -import { PingParams } from 'elasticsearch'; import { Plugin as Plugin_2 } from 'src/core/server'; import { Plugin as Plugin_3 } from 'kibana/server'; import { PluginInitializerContext as PluginInitializerContext_2 } from 'src/core/server'; -import { PutScriptParams } from 'elasticsearch'; -import { PutTemplateParams } from 'elasticsearch'; import { RecursiveReadonly } from '@kbn/utility-types'; -import { ReindexParams } from 'elasticsearch'; -import { ReindexRethrottleParams } from 'elasticsearch'; -import { RenderSearchTemplateParams } from 'elasticsearch'; import { RequestAdapter } from 'src/plugins/inspector/common'; +import { RequestHandlerContext } from 'src/core/server'; import { RequestStatistics } from 'src/plugins/inspector/common'; import { SavedObject } from 'src/core/server'; -import { SavedObjectsClientContract as SavedObjectsClientContract_2 } from 'src/core/server'; -import { ScrollParams } from 'elasticsearch'; +import { SavedObjectsClientContract } from 'src/core/server'; import { Search } from '@elastic/elasticsearch/api/requestParams'; -import { SearchParams } from 'elasticsearch'; import { SearchResponse } from 'elasticsearch'; -import { SearchShardsParams } from 'elasticsearch'; -import { SearchTemplateParams } from 'elasticsearch'; import { SerializedFieldFormat as SerializedFieldFormat_2 } from 'src/plugins/expressions/common'; import { ShardsResponse } from 'elasticsearch'; -import { SnapshotCreateParams } from 'elasticsearch'; -import { SnapshotCreateRepositoryParams } from 'elasticsearch'; -import { SnapshotDeleteParams } from 'elasticsearch'; -import { SnapshotDeleteRepositoryParams } from 'elasticsearch'; -import { SnapshotGetParams } from 'elasticsearch'; -import { SnapshotGetRepositoryParams } from 'elasticsearch'; -import { SnapshotRestoreParams } from 'elasticsearch'; -import { SnapshotStatusParams } from 'elasticsearch'; -import { SnapshotVerifyRepositoryParams } from 'elasticsearch'; -import { SuggestParams } from 'elasticsearch'; -import { TasksCancelParams } from 'elasticsearch'; -import { TasksGetParams } from 'elasticsearch'; -import { TasksListParams } from 'elasticsearch'; -import { TermvectorsParams } from 'elasticsearch'; import { ToastInputFields } from 'src/core/public/notifications'; -import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport'; -import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport'; -import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; import { Unit } from '@elastic/datemath'; import { UnwrapPromiseOrReturn } from '@kbn/utility-types'; -import { UpdateDocumentByQueryParams } from 'elasticsearch'; -import { UpdateDocumentParams } from 'elasticsearch'; // Warning: (ae-forgotten-export) The symbol "AggConfigSerialized" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "AggConfigOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -764,7 +643,7 @@ export const indexPatterns: { // // @public (undocumented) export class IndexPatternsFetcher { - constructor(callDataCluster: LegacyAPICaller_2); + constructor(callDataCluster: LegacyAPICaller); getFieldsForTimePattern(options: { pattern: string; metaFields: string[]; @@ -791,7 +670,7 @@ export class IndexPatternsService implements Plugin_3 Promise; + indexPatternsServiceFactory: (kibanaRequest: KibanaRequest_2) => Promise; }; } @@ -828,10 +707,12 @@ export interface ISearchStart ISearchStrategy; - // Warning: (ae-forgotten-export) The symbol "RequestHandlerContext" needs to be exported by the entry point index.d.ts - // // (undocumented) search: (context: RequestHandlerContext, request: SearchStrategyRequest, options: ISearchOptions) => Promise; + // (undocumented) + searchSource: { + asScoped: (request: KibanaRequest) => Promise; + }; } // Warning: (ae-missing-release-tag) "ISearchStrategy" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -989,13 +870,13 @@ export class Plugin implements Plugin_2>; fieldFormats: { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { indexPatternsServiceFactory: (kibanaRequest: import("../../../core/server").KibanaRequest) => Promise; }; + search: ISearchStart>; }; // (undocumented) stop(): void; @@ -1251,6 +1132,7 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:252:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index_patterns/index_patterns_service.ts:50:14 - (ae-forgotten-export) The symbol "IndexPatternsService" needs to be exported by the entry point index.d.ts // src/plugins/data/server/plugin.ts:88:66 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/search/types.ts:78:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/test/plugin_functional/plugins/data_search/kibana.json b/test/plugin_functional/plugins/data_search/kibana.json new file mode 100644 index 000000000000..3acbe9f97d8f --- /dev/null +++ b/test/plugin_functional/plugins/data_search/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "data_search_plugin", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["data_search_test_plugin"], + "server": true, + "ui": false, + "requiredPlugins": ["data"] +} diff --git a/test/plugin_functional/plugins/data_search/package.json b/test/plugin_functional/plugins/data_search/package.json new file mode 100644 index 000000000000..de2bb5cfaa56 --- /dev/null +++ b/test/plugin_functional/plugins/data_search/package.json @@ -0,0 +1,15 @@ +{ + "name": "data_search_plugin", + "version": "1.0.0", + "kibana": { + "version": "kibana" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "4.0.2" + } +} diff --git a/test/plugin_functional/plugins/data_search/server/index.ts b/test/plugin_functional/plugins/data_search/server/index.ts new file mode 100644 index 000000000000..0dc4b0afd4f4 --- /dev/null +++ b/test/plugin_functional/plugins/data_search/server/index.ts @@ -0,0 +1,24 @@ +/* + * 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 { PluginInitializer } from 'src/core/server'; +import { DataSearchTestPlugin, TestPluginSetup, TestPluginStart } from './plugin'; + +export const plugin: PluginInitializer = () => + new DataSearchTestPlugin(); diff --git a/test/plugin_functional/plugins/data_search/server/plugin.ts b/test/plugin_functional/plugins/data_search/server/plugin.ts new file mode 100644 index 000000000000..4252008dcd7e --- /dev/null +++ b/test/plugin_functional/plugins/data_search/server/plugin.ts @@ -0,0 +1,82 @@ +/* + * 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 { CoreSetup, Plugin } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; +import { PluginStart as DataPluginStart } from '../../../../../src/plugins/data/server'; + +export interface DataSearchTestStartDeps { + data: DataPluginStart; +} + +export class DataSearchTestPlugin + implements Plugin { + public setup(core: CoreSetup) { + const router = core.http.createRouter(); + + router.get( + { path: '/api/data_search_plugin/search_source/as_scoped', validate: false }, + async (context, req, res) => { + const [, { data }] = await core.getStartServices(); + // Just make sure `asScoped` can be called + await data.search.searchSource.asScoped(req); + return res.ok(); + } + ); + + router.get( + { path: '/api/data_search_plugin/search_source/create_empty', validate: false }, + async (context, req, res) => { + const [, { data }] = await core.getStartServices(); + const service = await data.search.searchSource.asScoped(req); + const searchSource = service.createEmpty(); + return res.ok({ body: searchSource.serialize() }); + } + ); + + router.post( + { + path: '/api/data_search_plugin/search_source/create', + validate: { + body: schema.object({}, { unknowns: 'allow' }), + }, + }, + async (context, req, res) => { + const [, { data }] = await core.getStartServices(); + const service = await data.search.searchSource.asScoped(req); + + // Since the index pattern ID can change on each test run, we need + // to look it up on the fly and insert it into the request. + const indexPatterns = await data.indexPatterns.indexPatternsServiceFactory(req); + const ids = await indexPatterns.getIds(); + // @ts-expect-error Force overwriting the request + req.body.index = ids[0]; + const searchSource = await service.create(req.body); + + return res.ok({ body: searchSource.serialize() }); + } + ); + } + + public start() {} + public stop() {} +} + +export type TestPluginSetup = ReturnType; +export type TestPluginStart = ReturnType; diff --git a/test/plugin_functional/plugins/data_search/tsconfig.json b/test/plugin_functional/plugins/data_search/tsconfig.json new file mode 100644 index 000000000000..72793a327d97 --- /dev/null +++ b/test/plugin_functional/plugins/data_search/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "server/**/*.ts", + "../../../../typings/**/*" + ], + "exclude": [], + "references": [ + { "path": "../../../../src/core/tsconfig.json" } + ] +} diff --git a/test/plugin_functional/test_suites/data_plugin/index.ts b/test/plugin_functional/test_suites/data_plugin/index.ts index 1c3f118135ff..bbf9d823e357 100644 --- a/test/plugin_functional/test_suites/data_plugin/index.ts +++ b/test/plugin_functional/test_suites/data_plugin/index.ts @@ -17,9 +17,25 @@ * under the License. */ -// @ts-expect-error -export default function ({ loadTestFile }) { +import { PluginFunctionalProviderContext } from '../../services'; + +export default function ({ + getPageObjects, + getService, + loadTestFile, +}: PluginFunctionalProviderContext) { + const esArchiver = getService('esArchiver'); + const PageObjects = getPageObjects(['common', 'header', 'settings']); + describe('data plugin', () => { + before(async () => { + await esArchiver.loadIfNeeded( + '../functional/fixtures/es_archiver/getting_started/shakespeare' + ); + await PageObjects.common.navigateToApp('settings'); + await PageObjects.settings.createIndexPattern('shakespeare', ''); + }); loadTestFile(require.resolve('./index_patterns')); + loadTestFile(require.resolve('./search')); }); } diff --git a/test/plugin_functional/test_suites/data_plugin/index_patterns.ts b/test/plugin_functional/test_suites/data_plugin/index_patterns.ts index 7e736ea7a066..4359816efb95 100644 --- a/test/plugin_functional/test_suites/data_plugin/index_patterns.ts +++ b/test/plugin_functional/test_suites/data_plugin/index_patterns.ts @@ -20,20 +20,11 @@ import expect from '@kbn/expect'; import { PluginFunctionalProviderContext } from '../../services'; import '../../plugins/core_provider_plugin/types'; -export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) { +export default function ({ getService }: PluginFunctionalProviderContext) { const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - const PageObjects = getPageObjects(['common', 'settings']); describe('index patterns', function () { let indexPatternId = ''; - before(async () => { - await esArchiver.loadIfNeeded( - '../functional/fixtures/es_archiver/getting_started/shakespeare' - ); - await PageObjects.common.navigateToApp('settings'); - await PageObjects.settings.createIndexPattern('shakespeare', ''); - }); it('can get all ids', async () => { const body = await (await supertest.get('/api/index-patterns-plugin/get-all').expect(200)) diff --git a/test/plugin_functional/test_suites/data_plugin/search.ts b/test/plugin_functional/test_suites/data_plugin/search.ts new file mode 100644 index 000000000000..730b05cdb65e --- /dev/null +++ b/test/plugin_functional/test_suites/data_plugin/search.ts @@ -0,0 +1,65 @@ +/* + * 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 { PluginFunctionalProviderContext } from '../../services'; + +export default function ({ getService }: PluginFunctionalProviderContext) { + const supertest = getService('supertest'); + + describe('data.search', () => { + describe('searchSource', () => { + const searchSourceFields = { + highlightAll: true, + index: '', + query: { + language: 'kuery', + query: 'play_name:\\"Henry IV\\"', + }, + version: true, + }; + + it('asScoped()', async () => { + await supertest.get('/api/data_search_plugin/search_source/as_scoped').expect(200); + }); + + it('createEmpty()', async () => { + await supertest + .get('/api/data_search_plugin/search_source/create_empty') + .expect(200) + .expect(JSON.stringify({ searchSourceJSON: '{}', references: [] })); + }); + + it('create()', async () => { + await supertest + .post('/api/data_search_plugin/search_source/create') + .set('kbn-xsrf', 'anything') + .send(searchSourceFields) + .expect(200) + .expect(({ body }) => { + const searchSourceJSON = JSON.parse(body.searchSourceJSON); + expect(Object.keys(body)).to.eql(['searchSourceJSON', 'references']); + expect(searchSourceJSON.query).to.eql(searchSourceFields.query); + expect(body.references[0].type).to.equal('index-pattern'); + expect(searchSourceJSON.indexRefName).to.equal(body.references[0].name); + }); + }); + }); + }); +}