[Search] Refactor service to register search strategies, not providers (#60342)

* Add async search strategy

* Add async search

* Fix async strategy and add tests

* Move types to separate file

* Revert changes to demo search

* Update demo search strategy to use async

* Add async es search strategy

* Return response as rawResponse

* Poll after initial request

* Add cancellation to search strategies

* Add tests

* Simplify async search strategy

* Move loadingCount to search strategy

* Update abort controller library

* Bootstrap

* Abort when the request is aborted

* Add utility and update value suggestions route

* Fix bad merge conflict

* Update tests

* Move to data_enhanced plugin

* Remove bad merge

* Revert switching abort controller libraries

* Revert package.json in lib

* Move to previous abort controller

* Add support for frozen indices

* Fix test to use fake timers to run debounced handlers

* Revert changes to example plugin

* Fix loading bar not going away when cancelling

* Call getSearchStrategy instead of passing  directly

* Add async demo search strategy

* Fix error with setting state

* Update how aborting works

* Fix type checks

* Add test for loading count

* Attempt to fix broken example test

* Revert changes to test

* Fix test

* Update name to camelCase

* Fix failing test

* Don't require data_enhanced in example plugin

* Actually send DELETE request

* Use waitForCompletion parameter

* Use default search params

* Add support for rollups

* Only make changes needed for frozen indices/rollups

* Only make changes needed for frozen indices/rollups

* Add back in async functionality

* Fix tests/types

* Fix issue with sending empty body in GET

* Don't include skipped in loaded/total

* Don't wait before polling the next time

* Add search interceptor for bulk managing searches

* Simplify search logic

* Fix merge error

* Review feedback

* UI to stop async searches

* Add service for running beyond timeout

* Refactor abort utils

* Remove unneeded changes

* Add tests

* Refactor search service to register strategies directly

* Remove accidental change

* re-generate docs

* Fix merge

* types

* doc

* eslint

* Fix async strategy jest test

* type fix

* Use getStartServices in search strategies

* Code review + snapshot

* eslint

* Type script

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: Liza K <liza.katz@elastic.co>
This commit is contained in:
Lukas Olson 2020-06-08 11:22:09 -07:00 committed by GitHub
parent 16fbbf4345
commit 8174b5ce29
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 329 additions and 434 deletions

View file

@ -1,11 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [ISearchContext](./kibana-plugin-plugins-data-public.isearchcontext.md) &gt; [core](./kibana-plugin-plugins-data-public.isearchcontext.core.md)
## ISearchContext.core property
<b>Signature:</b>
```typescript
core: CoreStart;
```

View file

@ -1,11 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [ISearchContext](./kibana-plugin-plugins-data-public.isearchcontext.md) &gt; [getSearchStrategy](./kibana-plugin-plugins-data-public.isearchcontext.getsearchstrategy.md)
## ISearchContext.getSearchStrategy property
<b>Signature:</b>
```typescript
getSearchStrategy: <T extends TStrategyTypes>(name: T) => TSearchStrategyProvider<T>;
```

View file

@ -1,19 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [ISearchContext](./kibana-plugin-plugins-data-public.isearchcontext.md)
## ISearchContext interface
<b>Signature:</b>
```typescript
export interface ISearchContext
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [core](./kibana-plugin-plugins-data-public.isearchcontext.core.md) | <code>CoreStart</code> | |
| [getSearchStrategy](./kibana-plugin-plugins-data-public.isearchcontext.getsearchstrategy.md) | <code>&lt;T extends TStrategyTypes&gt;(name: T) =&gt; TSearchStrategyProvider&lt;T&gt;</code> | |

View file

@ -67,7 +67,6 @@
| [IndexPatternTypeMeta](./kibana-plugin-plugins-data-public.indexpatterntypemeta.md) | |
| [IRequestTypesMap](./kibana-plugin-plugins-data-public.irequesttypesmap.md) | |
| [IResponseTypesMap](./kibana-plugin-plugins-data-public.iresponsetypesmap.md) | |
| [ISearchContext](./kibana-plugin-plugins-data-public.isearchcontext.md) | |
| [ISearchOptions](./kibana-plugin-plugins-data-public.isearchoptions.md) | |
| [ISearchStrategy](./kibana-plugin-plugins-data-public.isearchstrategy.md) | Search strategy interface contains a search method that takes in a request and returns a promise that resolves to a response. |
| [ISyncSearchRequest](./kibana-plugin-plugins-data-public.isyncsearchrequest.md) | |
@ -157,5 +156,4 @@
| [TabbedAggRow](./kibana-plugin-plugins-data-public.tabbedaggrow.md) | \* |
| [TimefilterContract](./kibana-plugin-plugins-data-public.timefiltercontract.md) | |
| [TimeHistoryContract](./kibana-plugin-plugins-data-public.timehistorycontract.md) | |
| [TSearchStrategyProvider](./kibana-plugin-plugins-data-public.tsearchstrategyprovider.md) | Search strategy provider creates an instance of a search strategy with the request handler context bound to it. This way every search strategy can use whatever information they require from the request context. |

View file

@ -1,13 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [TSearchStrategyProvider](./kibana-plugin-plugins-data-public.tsearchstrategyprovider.md)
## TSearchStrategyProvider type
Search strategy provider creates an instance of a search strategy with the request handler context bound to it. This way every search strategy can use whatever information they require from the request context.
<b>Signature:</b>
```typescript
export declare type TSearchStrategyProvider<T extends TStrategyTypes> = (context: ISearchContext) => ISearchStrategy<T>;
```

View file

@ -17,53 +17,27 @@
* under the License.
*/
import { Observable } from 'rxjs';
import {
ISearchContext,
TSearchStrategyProvider,
ISearchStrategy,
} from '../../../src/plugins/data/public';
import { ASYNC_DEMO_SEARCH_STRATEGY, IAsyncDemoResponse } from '../common';
import { Observable, from } from 'rxjs';
import { CoreSetup } from 'kibana/public';
import { flatMap } from 'rxjs/operators';
import { ISearch } from '../../../src/plugins/data/public';
import { ASYNC_SEARCH_STRATEGY } from '../../../x-pack/plugins/data_enhanced/public';
import { ASYNC_DEMO_SEARCH_STRATEGY, IAsyncDemoResponse } from '../common';
import { DemoDataSearchStartDependencies } from './types';
/**
* This demo search strategy provider simply provides a shortcut for calling the DEMO_ASYNC_SEARCH_STRATEGY
* on the server side, without users having to pass it in explicitly, and it takes advantage of the
* already registered ASYNC_SEARCH_STRATEGY that exists on the client.
*
* so instead of callers having to do:
*
* ```
* search(
* { ...request, serverStrategy: DEMO_ASYNC_SEARCH_STRATEGY },
* options,
* ASYNC_SEARCH_STRATEGY
* ) as Observable<IDemoResponse>,
*```
* They can instead just do
*
* ```
* search(request, options, DEMO_ASYNC_SEARCH_STRATEGY);
* ```
*
* and are ensured type safety in regard to the request and response objects.
*
* @param context - context supplied by other plugins.
* @param search - a search function to access other strategies that have already been registered.
*/
export const asyncDemoClientSearchStrategyProvider: TSearchStrategyProvider<typeof ASYNC_DEMO_SEARCH_STRATEGY> = (
context: ISearchContext
): ISearchStrategy<typeof ASYNC_DEMO_SEARCH_STRATEGY> => {
const asyncStrategyProvider = context.getSearchStrategy(ASYNC_SEARCH_STRATEGY);
const { search } = asyncStrategyProvider(context);
return {
search: (request, options) => {
return search(
{ ...request, serverStrategy: ASYNC_DEMO_SEARCH_STRATEGY },
options
) as Observable<IAsyncDemoResponse>;
},
export function asyncDemoClientSearchStrategyProvider(core: CoreSetup) {
const search: ISearch<typeof ASYNC_DEMO_SEARCH_STRATEGY> = (request, options) => {
return from(core.getStartServices()).pipe(
flatMap((startServices) => {
const asyncStrategy = (startServices[1] as DemoDataSearchStartDependencies).data.search.getSearchStrategy(
ASYNC_SEARCH_STRATEGY
);
return asyncStrategy.search(
{ ...request, serverStrategy: ASYNC_DEMO_SEARCH_STRATEGY },
options
) as Observable<IAsyncDemoResponse>;
})
);
};
};
return { search };
}

View file

@ -17,11 +17,12 @@
* under the License.
*/
import { Observable } from 'rxjs';
import { ISearchContext, SYNC_SEARCH_STRATEGY } from '../../../src/plugins/data/public';
import { TSearchStrategyProvider, ISearchStrategy } from '../../../src/plugins/data/public';
import { Observable, from } from 'rxjs';
import { flatMap } from 'rxjs/operators';
import { CoreSetup } from 'kibana/public';
import { ISearch, SYNC_SEARCH_STRATEGY } from '../../../src/plugins/data/public';
import { DEMO_SEARCH_STRATEGY, IDemoResponse } from '../common';
import { DemoDataSearchStartDependencies } from './types';
/**
* This demo search strategy provider simply provides a shortcut for calling the DEMO_SEARCH_STRATEGY
@ -31,7 +32,7 @@ import { DEMO_SEARCH_STRATEGY, IDemoResponse } from '../common';
* so instead of callers having to do:
*
* ```
* context.search(
* data.search.search(
* { ...request, serverStrategy: DEMO_SEARCH_STRATEGY },
* options,
* SYNC_SEARCH_STRATEGY
@ -41,24 +42,24 @@ import { DEMO_SEARCH_STRATEGY, IDemoResponse } from '../common';
* They can instead just do
*
* ```
* context.search(request, options, DEMO_SEARCH_STRATEGY);
* data.search.search(request, options, DEMO_SEARCH_STRATEGY);
* ```
*
* and are ensured type safety in regard to the request and response objects.
*
* @param context - context supplied by other plugins.
* @param search - a search function to access other strategies that have already been registered.
*/
export const demoClientSearchStrategyProvider: TSearchStrategyProvider<typeof DEMO_SEARCH_STRATEGY> = (
context: ISearchContext
): ISearchStrategy<typeof DEMO_SEARCH_STRATEGY> => {
const syncStrategyProvider = context.getSearchStrategy(SYNC_SEARCH_STRATEGY);
const { search } = syncStrategyProvider(context);
return {
search: (request, options) => {
return search({ ...request, serverStrategy: DEMO_SEARCH_STRATEGY }, options) as Observable<
IDemoResponse
>;
},
export function demoClientSearchStrategyProvider(core: CoreSetup) {
const search: ISearch<typeof DEMO_SEARCH_STRATEGY> = (request, options) => {
return from(core.getStartServices()).pipe(
flatMap((startServices) => {
const syncStrategy = (startServices[1] as DemoDataSearchStartDependencies).data.search.getSearchStrategy(
SYNC_SEARCH_STRATEGY
);
return syncStrategy.search(
{ ...request, serverStrategy: DEMO_SEARCH_STRATEGY },
options
) as Observable<IDemoResponse>;
})
);
};
};
return { search };
}

View file

@ -17,7 +17,6 @@
* under the License.
*/
import { DataPublicPluginSetup } from '../../../src/plugins/data/public';
import { Plugin, CoreSetup } from '../../../src/core/public';
import {
DEMO_SEARCH_STRATEGY,
@ -29,10 +28,7 @@ import {
} from '../common';
import { demoClientSearchStrategyProvider } from './demo_search_strategy';
import { asyncDemoClientSearchStrategyProvider } from './async_demo_search_strategy';
interface DemoDataSearchSetupDependencies {
data: DataPublicPluginSetup;
}
import { DemoDataSearchSetupDependencies, DemoDataSearchStartDependencies } from './types';
/**
* Add the typescript mappings for our search strategy to the request and
@ -55,16 +51,13 @@ declare module '../../../src/plugins/data/public' {
}
}
export class DemoDataPlugin implements Plugin {
public setup(core: CoreSetup, deps: DemoDataSearchSetupDependencies) {
deps.data.search.registerSearchStrategyProvider(
DEMO_SEARCH_STRATEGY,
demoClientSearchStrategyProvider
);
deps.data.search.registerSearchStrategyProvider(
ASYNC_DEMO_SEARCH_STRATEGY,
asyncDemoClientSearchStrategyProvider
);
export class DemoDataPlugin
implements Plugin<void, void, DemoDataSearchSetupDependencies, DemoDataSearchStartDependencies> {
public setup(core: CoreSetup, { data }: DemoDataSearchSetupDependencies) {
const demoClientSearchStrategy = demoClientSearchStrategyProvider(core);
const asyncDemoClientSearchStrategy = asyncDemoClientSearchStrategyProvider(core);
data.search.registerSearchStrategy(DEMO_SEARCH_STRATEGY, demoClientSearchStrategy);
data.search.registerSearchStrategy(ASYNC_DEMO_SEARCH_STRATEGY, asyncDemoClientSearchStrategy);
}
public start() {}

View file

@ -0,0 +1,28 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { DataPublicPluginStart, DataPublicPluginSetup } from '../../../src/plugins/data/public';
export interface DemoDataSearchSetupDependencies {
data: DataPublicPluginSetup;
}
export interface DemoDataSearchStartDependencies {
data: DataPublicPluginStart;
}

View file

@ -341,8 +341,6 @@ export {
SYNC_SEARCH_STRATEGY,
getEsPreference,
getSearchErrorType,
ISearchContext,
TSearchStrategyProvider,
ISearchStrategy,
ISearch,
ISearchOptions,

View file

@ -21,11 +21,17 @@ import { Plugin, IndexPatternsContract } from '.';
import { fieldFormatsServiceMock } from './field_formats/mocks';
import { searchSetupMock, searchStartMock } from './search/mocks';
import { queryServiceMock } from './query/mocks';
import { AutocompleteStart, AutocompleteSetup } from './autocomplete';
export type Setup = jest.Mocked<ReturnType<Plugin['setup']>>;
export type Start = jest.Mocked<ReturnType<Plugin['start']>>;
const autocompleteMock: any = {
const automcompleteSetupMock: jest.Mocked<AutocompleteSetup> = {
addQuerySuggestionProvider: jest.fn(),
getQuerySuggestions: jest.fn(),
};
const autocompleteStartMock: jest.Mocked<AutocompleteStart> = {
getValueSuggestions: jest.fn(),
getQuerySuggestions: jest.fn(),
hasQuerySuggestions: jest.fn(),
@ -34,7 +40,7 @@ const autocompleteMock: any = {
const createSetupContract = (): Setup => {
const querySetupMock = queryServiceMock.createSetupContract();
return {
autocomplete: autocompleteMock,
autocomplete: automcompleteSetupMock,
search: searchSetupMock,
fieldFormats: fieldFormatsServiceMock.createSetupContract(),
query: querySetupMock,
@ -48,7 +54,7 @@ const createStartContract = (): Start => {
createFiltersFromValueClickAction: jest.fn().mockResolvedValue(['yes']),
createFiltersFromRangeSelectAction: jest.fn(),
},
autocomplete: autocompleteMock,
autocomplete: autocompleteStartMock,
search: searchStartMock,
fieldFormats: fieldFormatsServiceMock.createStartContract(),
query: queryStartMock,

View file

@ -1116,16 +1116,6 @@ export interface IResponseTypesMap {
// @public (undocumented)
export type ISearch<T extends TStrategyTypes = typeof DEFAULT_SEARCH_STRATEGY> = (request: IRequestTypesMap[T], options?: ISearchOptions) => Observable<IResponseTypesMap[T]>;
// Warning: (ae-missing-release-tag) "ISearchContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface ISearchContext {
// (undocumented)
core: CoreStart;
// (undocumented)
getSearchStrategy: <T extends TStrategyTypes>(name: T) => TSearchStrategyProvider<T>;
}
// Warning: (ae-missing-release-tag) "ISearchGeneric" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@ -1788,11 +1778,6 @@ export interface TimeRange {
to: string;
}
// Warning: (ae-missing-release-tag) "TSearchStrategyProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export type TSearchStrategyProvider<T extends TStrategyTypes> = (context: ISearchContext) => ISearchStrategy<T>;
// Warning: (ae-missing-release-tag) "UI_SETTINGS" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@ -1866,20 +1851,20 @@ export const UI_SETTINGS: {
// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:377:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:377:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:377:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:377:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:379:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:380:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:389:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:390:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:391:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:395:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:396:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:403:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:375:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:375:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:375:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:375:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:377:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:378:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:387:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:388:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:389:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:393:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:394:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:397:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:398:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/query/state_sync/connect_to_query_state.ts:33:33 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/query/state_sync/connect_to_query_state.ts:37:1 - (ae-forgotten-export) The symbol "QueryStateChange" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/types.ts:52:5 - (ae-forgotten-export) The symbol "createFiltersFromValueClickAction" needs to be exported by the entry point index.d.ts

View file

@ -17,40 +17,31 @@
* under the License.
*/
import { CoreSetup } from '../../../../../core/public';
import { coreMock } from '../../../../../core/public/mocks';
import { esSearchStrategyProvider } from './es_search_strategy';
import { CoreStart } from 'kibana/public';
import { ES_SEARCH_STRATEGY } from '../../../common/search/es_search';
describe('ES search strategy', () => {
let mockCoreStart: MockedKeys<CoreStart>;
const mockSearch = jest.fn();
let mockCoreSetup: MockedKeys<CoreSetup>;
const mockSearch = { search: jest.fn() };
beforeEach(() => {
mockCoreStart = coreMock.createStart();
mockSearch.mockClear();
mockCoreSetup = coreMock.createSetup();
mockSearch.search.mockClear();
});
it('returns a strategy with `search` that calls the sync search `search`', () => {
const request = { params: {} };
const options = {};
const esSearch = esSearchStrategyProvider({
core: mockCoreStart,
getSearchStrategy: jest.fn().mockImplementation(() => {
return () => {
return {
search: mockSearch,
};
};
}),
});
const esSearch = esSearchStrategyProvider(mockCoreSetup, mockSearch);
esSearch.search(request, options);
expect(mockSearch.mock.calls[0][0]).toEqual({
expect(mockSearch.search.mock.calls[0][0]).toEqual({
...request,
serverStrategy: ES_SEARCH_STRATEGY,
});
expect(mockSearch.mock.calls[0][1]).toBe(options);
expect(mockSearch.search.mock.calls[0][1]).toBe(options);
});
});

View file

@ -18,25 +18,26 @@
*/
import { Observable } from 'rxjs';
import { CoreSetup } from '../../../../../core/public';
import { ES_SEARCH_STRATEGY, IEsSearchResponse } from '../../../common/search';
import { ISearch } from '../i_search';
import { ISearchStrategy } from '../types';
import { SYNC_SEARCH_STRATEGY } from '../sync_search_strategy';
import { getEsPreference } from './get_es_preference';
import { ISearchContext, TSearchStrategyProvider, ISearchStrategy } from '../types';
export const esSearchStrategyProvider: TSearchStrategyProvider<typeof ES_SEARCH_STRATEGY> = (
context: ISearchContext
): ISearchStrategy<typeof ES_SEARCH_STRATEGY> => {
const syncStrategyProvider = context.getSearchStrategy(SYNC_SEARCH_STRATEGY);
const { search } = syncStrategyProvider(context);
return {
search: (request, options) => {
request.params = {
preference: getEsPreference(context.core.uiSettings),
...request.params,
};
return search({ ...request, serverStrategy: ES_SEARCH_STRATEGY }, options) as Observable<
IEsSearchResponse
>;
},
export function esSearchStrategyProvider(
core: CoreSetup,
syncStrategy: ISearchStrategy<typeof SYNC_SEARCH_STRATEGY>
) {
const search: ISearch<typeof ES_SEARCH_STRATEGY> = (request, options) => {
request.params = {
preference: getEsPreference(core.uiSettings),
...request.params,
};
return syncStrategy.search(
{ ...request, serverStrategy: ES_SEARCH_STRATEGY },
options
) as Observable<IEsSearchResponse>;
};
};
return { search };
}

View file

@ -21,13 +21,7 @@ export * from './aggs';
export * from './expressions';
export * from './tabify';
export {
ISearchSetup,
ISearchStart,
ISearchContext,
TSearchStrategyProvider,
ISearchStrategy,
} from './types';
export { ISearchSetup, ISearchStart, ISearchStrategy } from './types';
export {
ISearch,

View file

@ -18,18 +18,20 @@
*/
import { searchAggsSetupMock, searchAggsStartMock } from './aggs/mocks';
import { ISearchStart } from './types';
import { ISearchSetup, ISearchStart } from './types';
import { searchSourceMock, createSearchSourceMock } from './search_source/mocks';
const searchSetupMock = {
export * from './search_source/mocks';
const searchSetupMock: jest.Mocked<ISearchSetup> = {
aggs: searchAggsSetupMock(),
registerSearchStrategyContext: jest.fn(),
registerSearchStrategyProvider: jest.fn(),
registerSearchStrategy: jest.fn(),
};
const searchStartMock: jest.Mocked<ISearchStart> = {
aggs: searchAggsStartMock(),
setInterceptor: jest.fn(),
getSearchStrategy: jest.fn(),
search: jest.fn(),
searchSource: searchSourceMock,
__LEGACY: {

View file

@ -38,7 +38,7 @@ describe('Search service', () => {
packageInfo: { version: '8' },
expressions: expressionsPluginMock.createSetupContract(),
} as any);
expect(setup).toHaveProperty('registerSearchStrategyProvider');
expect(setup).toHaveProperty('registerSearchStrategy');
});
});
});

View file

@ -18,11 +18,11 @@
*/
import { Plugin, CoreSetup, CoreStart, PackageInfo } from '../../../../core/public';
import { SYNC_SEARCH_STRATEGY, syncSearchStrategyProvider } from './sync_search_strategy';
import { ISearchSetup, ISearchStart, TSearchStrategiesMap, ISearchStrategy } from './types';
import { ExpressionsSetup } from '../../../../plugins/expressions/public';
import { SYNC_SEARCH_STRATEGY, syncSearchStrategyProvider } from './sync_search_strategy';
import { createSearchSource, SearchSource, SearchSourceDependencies } from './search_source';
import { ISearchSetup, ISearchStart, TSearchStrategyProvider, TSearchStrategiesMap } from './types';
import { TStrategyTypes } from './strategy_types';
import { getEsClient, LegacyApiCaller } from './legacy';
import { ES_SEARCH_STRATEGY, DEFAULT_SEARCH_STRATEGY } from '../../common/search';
@ -54,11 +54,8 @@ interface SearchServiceStartDependencies {
}
/**
* The search plugin exposes two registration methods for other plugins:
* - registerSearchStrategyProvider for plugins to add their own custom
* search strategies
* - registerSearchStrategyContext for plugins to expose information
* and/or functionality for other search strategies to use
* The search plugin exposes a method `registerSearchStrategy` for other plugins
* to add their own custom search strategies.
*
* It also comes with two search strategy implementations - SYNC_SEARCH_STRATEGY and ES_SEARCH_STRATEGY.
*/
@ -73,17 +70,19 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
private readonly aggTypesRegistry = new AggTypesRegistry();
private searchInterceptor!: SearchInterceptor;
private registerSearchStrategyProvider = <T extends TStrategyTypes>(
private registerSearchStrategy = <T extends TStrategyTypes>(
name: T,
strategyProvider: TSearchStrategyProvider<T>
strategy: ISearchStrategy<T>
) => {
this.searchStrategies[name] = strategyProvider;
this.searchStrategies[name] = strategy;
};
private getSearchStrategy = <T extends TStrategyTypes>(name: T): TSearchStrategyProvider<T> => {
const strategyProvider = this.searchStrategies[name];
if (!strategyProvider) throw new Error(`Search strategy ${name} not found`);
return strategyProvider;
private getSearchStrategy = <T extends TStrategyTypes>(name: T): ISearchStrategy<T> => {
const strategy = this.searchStrategies[name];
if (!strategy) {
throw new Error(`Search strategy ${name} not found`);
}
return strategy;
};
public setup(
@ -91,8 +90,11 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
{ expressions, packageInfo, query, getInternalStartServices }: SearchServiceSetupDependencies
): ISearchSetup {
this.esClient = getEsClient(core.injectedMetadata, core.http, packageInfo);
this.registerSearchStrategyProvider(SYNC_SEARCH_STRATEGY, syncSearchStrategyProvider);
this.registerSearchStrategyProvider(ES_SEARCH_STRATEGY, esSearchStrategyProvider);
const syncSearchStrategy = syncSearchStrategyProvider(core);
const esSearchStrategy = esSearchStrategyProvider(core, syncSearchStrategy);
this.registerSearchStrategy(SYNC_SEARCH_STRATEGY, syncSearchStrategy);
this.registerSearchStrategy(ES_SEARCH_STRATEGY, esSearchStrategy);
const aggTypesSetup = this.aggTypesRegistry.setup();
@ -114,7 +116,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
calculateAutoTimeExpression: getCalculateAutoTimeExpression(core.uiSettings),
types: aggTypesSetup,
},
registerSearchStrategyProvider: this.registerSearchStrategyProvider,
registerSearchStrategy: this.registerSearchStrategy,
};
}
@ -134,12 +136,10 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
const aggTypesStart = this.aggTypesRegistry.start();
const search: ISearchGeneric = (request, options, strategyName) => {
const strategyProvider = this.getSearchStrategy(strategyName || DEFAULT_SEARCH_STRATEGY);
const searchStrategy = strategyProvider({
core,
getSearchStrategy: this.getSearchStrategy,
});
return this.searchInterceptor.search(searchStrategy.search as any, request, options);
const { search: defaultSearch } = this.getSearchStrategy(
strategyName || DEFAULT_SEARCH_STRATEGY
);
return this.searchInterceptor.search(defaultSearch as any, request, options);
};
const legacySearch = {
@ -164,6 +164,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
},
types: aggTypesStart,
},
getSearchStrategy: this.getSearchStrategy,
search,
searchSource: {
create: createSearchSource(dependencies.indexPatterns, searchSourceDependencies),

View file

@ -19,26 +19,23 @@
import { coreMock } from '../../../../core/public/mocks';
import { SYNC_SEARCH_STRATEGY, syncSearchStrategyProvider } from './sync_search_strategy';
import { CoreStart } from 'kibana/public';
import { CoreSetup } from 'kibana/public';
describe('Sync search strategy', () => {
let mockCoreStart: MockedKeys<CoreStart>;
let mockCoreSetup: MockedKeys<CoreSetup>;
beforeEach(() => {
mockCoreStart = coreMock.createStart();
mockCoreSetup = coreMock.createSetup();
});
it('returns a strategy with `search` that calls the backend API', () => {
mockCoreStart.http.fetch.mockImplementationOnce(() => Promise.resolve());
mockCoreSetup.http.fetch.mockImplementationOnce(() => Promise.resolve());
const syncSearch = syncSearchStrategyProvider({
core: mockCoreStart,
getSearchStrategy: jest.fn(),
});
const syncSearch = syncSearchStrategyProvider(mockCoreSetup);
const request = { serverStrategy: SYNC_SEARCH_STRATEGY };
syncSearch.search(request, {});
expect(mockCoreStart.http.fetch.mock.calls[0][0]).toEqual({
expect(mockCoreSetup.http.fetch.mock.calls[0][0]).toEqual({
path: `/internal/search/${SYNC_SEARCH_STRATEGY}`,
body: JSON.stringify({
serverStrategy: 'SYNC_SEARCH_STRATEGY',
@ -52,15 +49,12 @@ describe('Sync search strategy', () => {
const expectedLoadingCountValues = [0, 1, 0];
const receivedLoadingCountValues: number[] = [];
mockCoreStart.http.fetch.mockResolvedValueOnce('response');
mockCoreSetup.http.fetch.mockResolvedValueOnce('response');
const syncSearch = syncSearchStrategyProvider({
core: mockCoreStart,
getSearchStrategy: jest.fn(),
});
const syncSearch = syncSearchStrategyProvider(mockCoreSetup);
const request = { serverStrategy: SYNC_SEARCH_STRATEGY };
const loadingCount$ = mockCoreStart.http.addLoadingCountSource.mock.calls[0][0];
const loadingCount$ = mockCoreSetup.http.addLoadingCountSource.mock.calls[0][0];
loadingCount$.subscribe((value) => receivedLoadingCountValues.push(value));
await syncSearch.search(request, {}).toPromise();
@ -73,15 +67,12 @@ describe('Sync search strategy', () => {
const expectedLoadingCountValues = [0, 1, 0];
const receivedLoadingCountValues: number[] = [];
mockCoreStart.http.fetch.mockRejectedValueOnce('error');
mockCoreSetup.http.fetch.mockRejectedValueOnce('error');
const syncSearch = syncSearchStrategyProvider({
core: mockCoreStart,
getSearchStrategy: jest.fn(),
});
const syncSearch = syncSearchStrategyProvider(mockCoreSetup);
const request = { serverStrategy: SYNC_SEARCH_STRATEGY };
const loadingCount$ = mockCoreStart.http.addLoadingCountSource.mock.calls[0][0];
const loadingCount$ = mockCoreSetup.http.addLoadingCountSource.mock.calls[0][0];
loadingCount$.subscribe((value) => receivedLoadingCountValues.push(value));
try {

View file

@ -19,9 +19,9 @@
import { BehaviorSubject, from } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { CoreSetup } from '../../../../core/public';
import { IKibanaSearchRequest } from '../../common/search';
import { ISearch, ISearchOptions } from './i_search';
import { TSearchStrategyProvider, ISearchStrategy, ISearchContext } from './types';
import { ISearch } from './i_search';
export const SYNC_SEARCH_STRATEGY = 'SYNC_SEARCH_STRATEGY';
@ -29,27 +29,22 @@ export interface ISyncSearchRequest extends IKibanaSearchRequest {
serverStrategy: string;
}
export const syncSearchStrategyProvider: TSearchStrategyProvider<typeof SYNC_SEARCH_STRATEGY> = (
context: ISearchContext
): ISearchStrategy<typeof SYNC_SEARCH_STRATEGY> => {
export function syncSearchStrategyProvider(core: CoreSetup) {
const loadingCount$ = new BehaviorSubject(0);
context.core.http.addLoadingCountSource(loadingCount$);
core.http.addLoadingCountSource(loadingCount$);
const search: ISearch<typeof SYNC_SEARCH_STRATEGY> = (
request: ISyncSearchRequest,
options: ISearchOptions = {}
) => {
const search: ISearch<typeof SYNC_SEARCH_STRATEGY> = (request, options) => {
loadingCount$.next(loadingCount$.getValue() + 1);
return from(
context.core.http.fetch({
core.http.fetch({
path: `/internal/search/${request.serverStrategy}`,
method: 'POST',
body: JSON.stringify(request),
signal: options.signal,
signal: options?.signal,
})
).pipe(finalize(() => loadingCount$.next(loadingCount$.getValue() - 1)));
};
return { search };
};
}

View file

@ -17,7 +17,6 @@
* under the License.
*/
import { CoreStart } from 'kibana/public';
import { SearchAggsSetup, SearchAggsStart } from './aggs';
import { ISearch, ISearchGeneric } from './i_search';
import { TStrategyTypes } from './strategy_types';
@ -25,11 +24,6 @@ import { LegacyApiCaller } from './legacy/es_client';
import { SearchInterceptor } from './search_interceptor';
import { ISearchSource, SearchSourceFields } from './search_source';
export interface ISearchContext {
core: CoreStart;
getSearchStrategy: <T extends TStrategyTypes>(name: T) => TSearchStrategyProvider<T>;
}
/**
* Search strategy interface contains a search method that takes in
* a request and returns a promise that resolves to a response.
@ -39,27 +33,23 @@ export interface ISearchStrategy<T extends TStrategyTypes> {
}
export type TSearchStrategiesMap = {
[K in TStrategyTypes]?: TSearchStrategyProvider<any>;
[K in TStrategyTypes]?: ISearchStrategy<any>;
};
/**
* Search strategy provider creates an instance of a search strategy with the request
* handler context bound to it. This way every search strategy can use
* whatever information they require from the request context.
*/
export type TSearchStrategyProvider<T extends TStrategyTypes> = (
context: ISearchContext
) => ISearchStrategy<T>;
/**
* Extension point exposed for other plugins to register their own search
* strategies.
*/
export type TRegisterSearchStrategyProvider = <T extends TStrategyTypes>(
export type TRegisterSearchStrategy = <T extends TStrategyTypes>(
name: T,
searchStrategyProvider: TSearchStrategyProvider<T>
searchStrategy: ISearchStrategy<T>
) => void;
/**
* Used if a plugin needs access to an already registered search strategy.
*/
export type TGetSearchStrategy = <T extends TStrategyTypes>(name: T) => ISearchStrategy<T>;
export interface ISearchStartLegacy {
esClient: LegacyApiCaller;
}
@ -74,12 +64,18 @@ export interface ISearchSetup {
* Extension point exposed for other plugins to register their own search
* strategies.
*/
registerSearchStrategyProvider: TRegisterSearchStrategyProvider;
registerSearchStrategy: TRegisterSearchStrategy;
}
export interface ISearchStart {
aggs: SearchAggsStart;
setInterceptor: (searchInterceptor: SearchInterceptor) => void;
/**
* Used if a plugin needs access to an already registered search strategy.
*/
getSearchStrategy: TGetSearchStrategy;
search: ISearchGeneric;
searchSource: {
create: (fields?: SearchSourceFields) => Promise<ISearchSource>;

View file

@ -277,6 +277,7 @@ exports[`SavedObjectsTable import should show the flyout 1`] = `
"getMetrics": [Function],
},
},
"getSearchStrategy": [MockFunction],
"search": [MockFunction],
"searchSource": Object {
"create": [MockFunction],

View file

@ -29,19 +29,20 @@ export interface DataEnhancedStartDependencies {
export type DataEnhancedSetup = ReturnType<DataEnhancedPlugin['setup']>;
export type DataEnhancedStart = ReturnType<DataEnhancedPlugin['start']>;
export class DataEnhancedPlugin implements Plugin {
constructor() {}
public setup(core: CoreSetup, { data }: DataEnhancedSetupDependencies) {
export class DataEnhancedPlugin
implements Plugin<void, void, DataEnhancedSetupDependencies, DataEnhancedStartDependencies> {
public setup(
core: CoreSetup<DataEnhancedStartDependencies>,
{ data }: DataEnhancedSetupDependencies
) {
data.autocomplete.addQuerySuggestionProvider(
KUERY_LANGUAGE_NAME,
setupKqlQuerySuggestionProvider(core)
);
data.search.registerSearchStrategyProvider(ASYNC_SEARCH_STRATEGY, asyncSearchStrategyProvider);
data.search.registerSearchStrategyProvider(
ES_SEARCH_STRATEGY,
enhancedEsSearchStrategyProvider
);
const asyncSearchStrategy = asyncSearchStrategyProvider(core);
const esSearchStrategy = enhancedEsSearchStrategyProvider(core, asyncSearchStrategy);
data.search.registerSearchStrategy(ASYNC_SEARCH_STRATEGY, asyncSearchStrategy);
data.search.registerSearchStrategy(ES_SEARCH_STRATEGY, esSearchStrategy);
}
public start(core: CoreStart, plugins: DataEnhancedStartDependencies) {

View file

@ -6,35 +6,37 @@
import { of } from 'rxjs';
import { AbortController } from 'abort-controller';
import { CoreSetup } from '../../../../../src/core/public';
import { coreMock } from '../../../../../src/core/public/mocks';
import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
import { asyncSearchStrategyProvider } from './async_search_strategy';
import { IAsyncSearchOptions } from './types';
import { CoreStart } from 'kibana/public';
import { IAsyncSearchOptions } from '.';
import { DataEnhancedStartDependencies } from '../plugin';
describe('Async search strategy', () => {
let mockCoreStart: MockedKeys<CoreStart>;
let mockCoreSetup: jest.Mocked<CoreSetup<DataEnhancedStartDependencies>>;
let mockDataStart: jest.Mocked<DataPublicPluginStart>;
const mockSearch = jest.fn();
const mockRequest = { params: {}, serverStrategy: 'foo' };
const mockOptions: IAsyncSearchOptions = { pollInterval: 0 };
beforeEach(() => {
mockCoreStart = coreMock.createStart();
mockCoreSetup = coreMock.createSetup();
mockDataStart = dataPluginMock.createStartContract();
(mockDataStart.search.getSearchStrategy as jest.Mock).mockReturnValue({ search: mockSearch });
mockCoreSetup.getStartServices.mockResolvedValue([
undefined as any,
{ data: mockDataStart },
undefined,
]);
mockSearch.mockReset();
});
it('only sends one request if the first response is complete', async () => {
mockSearch.mockReturnValueOnce(of({ id: 1, total: 1, loaded: 1 }));
const asyncSearch = asyncSearchStrategyProvider({
core: mockCoreStart,
getSearchStrategy: jest.fn().mockImplementation(() => {
return () => {
return {
search: mockSearch,
};
};
}),
});
const asyncSearch = asyncSearchStrategyProvider(mockCoreSetup);
await asyncSearch.search(mockRequest, mockOptions).toPromise();
@ -51,17 +53,7 @@ describe('Async search strategy', () => {
of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: false })
);
const asyncSearch = asyncSearchStrategyProvider({
core: mockCoreStart,
getSearchStrategy: jest.fn().mockImplementation(() => {
return () => {
return {
search: mockSearch,
};
};
}),
});
const asyncSearch = asyncSearchStrategyProvider(mockCoreSetup);
expect(mockSearch).toBeCalledTimes(0);
await asyncSearch.search(mockRequest, mockOptions).toPromise();
@ -75,17 +67,7 @@ describe('Async search strategy', () => {
.mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: true }))
.mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: true }));
const asyncSearch = asyncSearchStrategyProvider({
core: mockCoreStart,
getSearchStrategy: jest.fn().mockImplementation(() => {
return () => {
return {
search: mockSearch,
};
};
}),
});
const asyncSearch = asyncSearchStrategyProvider(mockCoreSetup);
expect(mockSearch).toBeCalledTimes(0);
await asyncSearch
@ -104,16 +86,7 @@ describe('Async search strategy', () => {
of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: false })
);
const asyncSearch = asyncSearchStrategyProvider({
core: mockCoreStart,
getSearchStrategy: jest.fn().mockImplementation(() => {
return () => {
return {
search: mockSearch,
};
};
}),
});
const asyncSearch = asyncSearchStrategyProvider(mockCoreSetup);
expect(mockSearch).toBeCalledTimes(0);
@ -131,16 +104,7 @@ describe('Async search strategy', () => {
of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: false })
);
const asyncSearch = asyncSearchStrategyProvider({
core: mockCoreStart,
getSearchStrategy: jest.fn().mockImplementation(() => {
return () => {
return {
search: mockSearch,
};
};
}),
});
const asyncSearch = asyncSearchStrategyProvider(mockCoreSetup);
expect(mockSearch).toBeCalledTimes(0);
@ -157,16 +121,7 @@ describe('Async search strategy', () => {
.mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2 }))
.mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2 }));
const asyncSearch = asyncSearchStrategyProvider({
core: mockCoreStart,
getSearchStrategy: jest.fn().mockImplementation(() => {
return () => {
return {
search: mockSearch,
};
};
}),
});
const asyncSearch = asyncSearchStrategyProvider(mockCoreSetup);
const abortController = new AbortController();
const options = { ...mockOptions, signal: abortController.signal };
@ -178,7 +133,7 @@ describe('Async search strategy', () => {
} catch (e) {
expect(e.name).toBe('AbortError');
expect(mockSearch).toBeCalledTimes(1);
expect(mockCoreStart.http.delete).toBeCalled();
expect(mockCoreSetup.http.delete).toBeCalled();
}
});
});

View file

@ -4,17 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EMPTY, fromEvent, NEVER, Observable, throwError, timer } from 'rxjs';
import { mergeMap, expand, takeUntil } from 'rxjs/operators';
import { EMPTY, fromEvent, NEVER, throwError, timer, Observable, from } from 'rxjs';
import { mergeMap, expand, takeUntil, share, flatMap } from 'rxjs/operators';
import { CoreSetup } from '../../../../../src/core/public';
import { AbortError } from '../../../../../src/plugins/data/common';
import {
IKibanaSearchResponse,
ISearchContext,
ISearch,
ISearchStrategy,
ISyncSearchRequest,
SYNC_SEARCH_STRATEGY,
TSearchStrategyProvider,
} from '../../../../../src/plugins/data/public';
import { IAsyncSearchRequest, IAsyncSearchOptions, IAsyncSearchResponse } from './types';
import { IAsyncSearchOptions, IAsyncSearchResponse, IAsyncSearchRequest } from './types';
import { DataEnhancedStartDependencies } from '../plugin';
export const ASYNC_SEARCH_STRATEGY = 'ASYNC_SEARCH_STRATEGY';
@ -24,55 +25,59 @@ declare module '../../../../../src/plugins/data/public' {
}
}
export const asyncSearchStrategyProvider: TSearchStrategyProvider<typeof ASYNC_SEARCH_STRATEGY> = (
context: ISearchContext
): ISearchStrategy<typeof ASYNC_SEARCH_STRATEGY> => {
const syncStrategyProvider = context.getSearchStrategy(SYNC_SEARCH_STRATEGY);
const { search } = syncStrategyProvider(context);
return {
search: (
request: IAsyncSearchRequest,
{ pollInterval = 1000, ...options }: IAsyncSearchOptions = {}
): Observable<IKibanaSearchResponse> => {
const { serverStrategy } = request;
let id: string | undefined = request.id;
export function asyncSearchStrategyProvider(
core: CoreSetup<DataEnhancedStartDependencies>
): ISearchStrategy<typeof ASYNC_SEARCH_STRATEGY> {
const startServices$ = from(core.getStartServices()).pipe(share());
const aborted$ = options.signal
? fromEvent(options.signal, 'abort').pipe(
mergeMap(() => {
// If we haven't received the response to the initial request, including the ID, then
// we don't need to send a follow-up request to delete this search. Otherwise, we
// send the follow-up request to delete this search, then throw an abort error.
if (id !== undefined) {
context.core.http.delete(`/internal/search/${request.serverStrategy}/${id}`);
}
return throwError(new AbortError());
})
)
: NEVER;
const search: ISearch<typeof ASYNC_SEARCH_STRATEGY> = (
request: ISyncSearchRequest,
{ pollInterval = 1000, ...options }: IAsyncSearchOptions = {}
) => {
const { serverStrategy } = request;
let { id } = request;
return search(request, options).pipe(
expand((response: IAsyncSearchResponse) => {
// If the response indicates of an error, stop polling and complete the observable
if (!response || (response.is_partial && !response.is_running)) {
const aborted$ = options.signal
? fromEvent(options.signal, 'abort').pipe(
mergeMap(() => {
// If we haven't received the response to the initial request, including the ID, then
// we don't need to send a follow-up request to delete this search. Otherwise, we
// send the follow-up request to delete this search, then throw an abort error.
if (id !== undefined) {
core.http.delete(`/internal/search/${request.serverStrategy}/${id}`);
}
return throwError(new AbortError());
}
})
)
: NEVER;
// If the response indicates it is complete, stop polling and complete the observable
if (!response.is_running) return EMPTY;
return startServices$.pipe(
flatMap((startServices) => {
const syncSearch = startServices[1].data.search.getSearchStrategy(SYNC_SEARCH_STRATEGY);
return (syncSearch.search(request, options) as Observable<IAsyncSearchResponse>).pipe(
expand((response) => {
// If the response indicates of an error, stop polling and complete the observable
if (!response || (response.is_partial && !response.is_running)) {
return throwError(new AbortError());
}
id = response.id;
// If the response indicates it is complete, stop polling and complete the observable
if (!response.is_running) return EMPTY;
// Delay by the given poll interval
return timer(pollInterval).pipe(
// Send future requests using just the ID from the response
mergeMap(() => {
return search({ id, serverStrategy }, options);
})
);
}),
takeUntil(aborted$)
);
},
id = response.id;
// Delay by the given poll interval
return timer(pollInterval).pipe(
// Send future requests using just the ID from the response
mergeMap(() => {
return search({ id, serverStrategy }, options);
})
);
}),
takeUntil(aborted$)
);
})
);
};
};
return { search };
}

View file

@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { CoreSetup } from '../../../../../src/core/public';
import { coreMock } from '../../../../../src/core/public/mocks';
import { ES_SEARCH_STRATEGY } from '../../../../../src/plugins/data/common';
import { enhancedEsSearchStrategyProvider } from './es_search_strategy';
import { IAsyncSearchOptions } from '.';
describe('Enhanced ES search strategy', () => {
let mockCoreSetup: jest.Mocked<CoreSetup>;
const mockSearch = { search: jest.fn() };
beforeEach(() => {
mockCoreSetup = coreMock.createSetup();
mockSearch.search.mockClear();
});
it('returns a strategy with `search` that calls the async search `search`', () => {
const request = { params: {} };
const options: IAsyncSearchOptions = { pollInterval: 0 };
const esSearch = enhancedEsSearchStrategyProvider(mockCoreSetup, mockSearch);
esSearch.search(request, options);
expect(mockSearch.search.mock.calls[0][0]).toEqual({
...request,
serverStrategy: ES_SEARCH_STRATEGY,
});
expect(mockSearch.search.mock.calls[0][1]).toEqual(options);
});
});

View file

@ -5,42 +5,40 @@
*/
import { Observable } from 'rxjs';
import { CoreSetup } from '../../../../../src/core/public';
import { ES_SEARCH_STRATEGY, IEsSearchResponse } from '../../../../../src/plugins/data/common';
import {
TSearchStrategyProvider,
ISearchContext,
ISearch,
getEsPreference,
ISearchStrategy,
UI_SETTINGS,
} from '../../../../../src/plugins/data/public';
import { IEnhancedEsSearchRequest, EnhancedSearchParams } from '../../common';
import { ASYNC_SEARCH_STRATEGY } from './async_search_strategy';
import { IAsyncSearchOptions } from './types';
export const enhancedEsSearchStrategyProvider: TSearchStrategyProvider<typeof ES_SEARCH_STRATEGY> = (
context: ISearchContext
) => {
const asyncStrategyProvider = context.getSearchStrategy(ASYNC_SEARCH_STRATEGY);
const { search: asyncSearch } = asyncStrategyProvider(context);
export function enhancedEsSearchStrategyProvider(
core: CoreSetup,
asyncStrategy: ISearchStrategy<typeof ASYNC_SEARCH_STRATEGY>
) {
const search: ISearch<typeof ES_SEARCH_STRATEGY> = (
request: IEnhancedEsSearchRequest,
options
) => {
const params: EnhancedSearchParams = {
ignoreThrottled: !context.core.uiSettings.get<boolean>(UI_SETTINGS.SEARCH_INCLUDE_FROZEN),
preference: getEsPreference(context.core.uiSettings),
ignoreThrottled: !core.uiSettings.get<boolean>(UI_SETTINGS.SEARCH_INCLUDE_FROZEN),
preference: getEsPreference(core.uiSettings),
...request.params,
};
request.params = params;
const asyncOptions: IAsyncSearchOptions = { pollInterval: 0, ...options };
return asyncSearch(
return asyncStrategy.search(
{ ...request, serverStrategy: ES_SEARCH_STRATEGY },
asyncOptions
) as Observable<IEsSearchResponse>;
};
return { search };
};
}