[data.search] Add search session methods to search service contract (#87966)

* [data.search] Add search session methods to search service contract

* Fix types

* Fix tests and switch to cancel

* Update docs

* Fix types/tests

* Fix tests

* Update status of SO before cancelling search requests

* Add API integration test

* Fix types

* Update expiration route to use config defaultExpiration

* Fix test

* Update docs

* New logic for extend

* Remove declare module

* Review feedback

* fix ts

* Remove test that is no longer valid

* Fix undefined bug

* Use DataRequestHandlerContext in maps

* ts

Co-authored-by: Liza K <liza.katz@elastic.co>
This commit is contained in:
Lukas Olson 2021-02-03 08:08:54 -07:00 committed by GitHub
parent 2145768c0d
commit a9273ca001
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 641 additions and 704 deletions

View file

@ -1,18 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) &gt; [DataApiRequestHandlerContext](./kibana-plugin-plugins-data-server.dataapirequesthandlercontext.md)
## DataApiRequestHandlerContext interface
<b>Signature:</b>
```typescript
export interface DataApiRequestHandlerContext extends ISearchClient
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [session](./kibana-plugin-plugins-data-server.dataapirequesthandlercontext.session.md) | <code>IScopedSessionService</code> | |

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-server](./kibana-plugin-plugins-data-server.md) &gt; [DataApiRequestHandlerContext](./kibana-plugin-plugins-data-server.dataapirequesthandlercontext.md) &gt; [session](./kibana-plugin-plugins-data-server.dataapirequesthandlercontext.session.md)
## DataApiRequestHandlerContext.session property
<b>Signature:</b>
```typescript
session: IScopedSessionService;
```

View file

@ -1,18 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) &gt; [IScopedSessionService](./kibana-plugin-plugins-data-server.iscopedsessionservice.md)
## IScopedSessionService interface
<b>Signature:</b>
```typescript
export interface IScopedSessionService
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [search](./kibana-plugin-plugins-data-server.iscopedsessionservice.search.md) | <code>&lt;Request extends IKibanaSearchRequest, Response extends IKibanaSearchResponse&gt;(strategy: ISearchStrategy&lt;Request, Response&gt;, ...args: Parameters&lt;ISearchStrategy&lt;Request, Response&gt;['search']&gt;) =&gt; Observable&lt;Response&gt;</code> | |

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-server](./kibana-plugin-plugins-data-server.md) &gt; [IScopedSessionService](./kibana-plugin-plugins-data-server.iscopedsessionservice.md) &gt; [search](./kibana-plugin-plugins-data-server.iscopedsessionservice.search.md)
## IScopedSessionService.search property
<b>Signature:</b>
```typescript
search: <Request extends IKibanaSearchRequest, Response extends IKibanaSearchResponse>(strategy: ISearchStrategy<Request, Response>, ...args: Parameters<ISearchStrategy<Request, Response>['search']>) => Observable<Response>;
```

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) &gt; [ISearchSessionService](./kibana-plugin-plugins-data-server.isearchsessionservice.md) &gt; [asScopedProvider](./kibana-plugin-plugins-data-server.isearchsessionservice.asscopedprovider.md)
## ISearchSessionService.asScopedProvider property
<b>Signature:</b>
```typescript
asScopedProvider: (core: CoreStart) => (request: KibanaRequest) => IScopedSearchSessionsClient<T>;
```

View file

@ -0,0 +1,18 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) &gt; [ISearchSessionService](./kibana-plugin-plugins-data-server.isearchsessionservice.md)
## ISearchSessionService interface
<b>Signature:</b>
```typescript
export interface ISearchSessionService<T = unknown>
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [asScopedProvider](./kibana-plugin-plugins-data-server.isearchsessionservice.asscopedprovider.md) | <code>(core: CoreStart) =&gt; (request: KibanaRequest) =&gt; IScopedSearchSessionsClient&lt;T&gt;</code> | |

View file

@ -7,5 +7,5 @@
<b>Signature:</b>
```typescript
asScoped: (request: KibanaRequest) => ISearchClient;
asScoped: (request: KibanaRequest) => IScopedSearchClient;
```

View file

@ -15,7 +15,7 @@ export interface ISearchStart<SearchStrategyRequest extends IKibanaSearchRequest
| Property | Type | Description |
| --- | --- | --- |
| [aggs](./kibana-plugin-plugins-data-server.isearchstart.aggs.md) | <code>AggsStart</code> | |
| [asScoped](./kibana-plugin-plugins-data-server.isearchstart.asscoped.md) | <code>(request: KibanaRequest) =&gt; ISearchClient</code> | |
| [asScoped](./kibana-plugin-plugins-data-server.isearchstart.asscoped.md) | <code>(request: KibanaRequest) =&gt; IScopedSearchClient</code> | |
| [getSearchStrategy](./kibana-plugin-plugins-data-server.isearchstart.getsearchstrategy.md) | <code>(name?: string) =&gt; ISearchStrategy&lt;SearchStrategyRequest, SearchStrategyResponse&gt;</code> | Get other registered search strategies by name (or, by default, the Elasticsearch strategy). For example, if a new strategy needs to use the already-registered ES search strategy, it can use this function to accomplish that. |
| [searchSource](./kibana-plugin-plugins-data-server.isearchstart.searchsource.md) | <code>{</code><br/><code> asScoped: (request: KibanaRequest) =&gt; Promise&lt;ISearchStartSearchSource&gt;;</code><br/><code> }</code> | |

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-server](./kibana-plugin-plugins-data-server.md) &gt; [ISessionService](./kibana-plugin-plugins-data-server.isessionservice.md) &gt; [asScopedProvider](./kibana-plugin-plugins-data-server.isessionservice.asscopedprovider.md)
## ISessionService.asScopedProvider property
<b>Signature:</b>
```typescript
asScopedProvider: (core: CoreStart) => (request: KibanaRequest) => IScopedSessionService;
```

View file

@ -1,18 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) &gt; [ISessionService](./kibana-plugin-plugins-data-server.isessionservice.md)
## ISessionService interface
<b>Signature:</b>
```typescript
export interface ISessionService
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [asScopedProvider](./kibana-plugin-plugins-data-server.isessionservice.asscopedprovider.md) | <code>(core: CoreStart) =&gt; (request: KibanaRequest) =&gt; IScopedSessionService</code> | |

View file

@ -14,7 +14,6 @@
| [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) | |
| [OptionedParamType](./kibana-plugin-plugins-data-server.optionedparamtype.md) | |
| [Plugin](./kibana-plugin-plugins-data-server.plugin.md) | |
| [SessionService](./kibana-plugin-plugins-data-server.sessionservice.md) | The OSS session service. See data\_enhanced in X-Pack for the search session service. |
## Enumerations
@ -45,7 +44,6 @@
| --- | --- |
| [AggFunctionsMapping](./kibana-plugin-plugins-data-server.aggfunctionsmapping.md) | A global list of the expression function definitions for each agg type function. |
| [AggParamOption](./kibana-plugin-plugins-data-server.aggparamoption.md) | |
| [DataApiRequestHandlerContext](./kibana-plugin-plugins-data-server.dataapirequesthandlercontext.md) | |
| [EsQueryConfig](./kibana-plugin-plugins-data-server.esqueryconfig.md) | |
| [FieldDescriptor](./kibana-plugin-plugins-data-server.fielddescriptor.md) | |
| [FieldFormatConfig](./kibana-plugin-plugins-data-server.fieldformatconfig.md) | |
@ -53,12 +51,11 @@
| [IFieldSubType](./kibana-plugin-plugins-data-server.ifieldsubtype.md) | |
| [IFieldType](./kibana-plugin-plugins-data-server.ifieldtype.md) | |
| [IndexPatternAttributes](./kibana-plugin-plugins-data-server.indexpatternattributes.md) | Interface for an index pattern saved object |
| [IScopedSessionService](./kibana-plugin-plugins-data-server.iscopedsessionservice.md) | |
| [ISearchOptions](./kibana-plugin-plugins-data-server.isearchoptions.md) | |
| [ISearchSessionService](./kibana-plugin-plugins-data-server.isearchsessionservice.md) | |
| [ISearchSetup](./kibana-plugin-plugins-data-server.isearchsetup.md) | |
| [ISearchStart](./kibana-plugin-plugins-data-server.isearchstart.md) | |
| [ISearchStrategy](./kibana-plugin-plugins-data-server.isearchstrategy.md) | Search strategy interface contains a search method that takes in a request and returns a promise that resolves to a response. |
| [ISessionService](./kibana-plugin-plugins-data-server.isessionservice.md) | |
| [KueryNode](./kibana-plugin-plugins-data-server.kuerynode.md) | |
| [OptionedValueProp](./kibana-plugin-plugins-data-server.optionedvalueprop.md) | |
| [PluginSetup](./kibana-plugin-plugins-data-server.pluginsetup.md) | |
@ -110,5 +107,6 @@
| [KibanaContext](./kibana-plugin-plugins-data-server.kibanacontext.md) | |
| [ParsedInterval](./kibana-plugin-plugins-data-server.parsedinterval.md) | |
| [Query](./kibana-plugin-plugins-data-server.query.md) | |
| [SearchRequestHandlerContext](./kibana-plugin-plugins-data-server.searchrequesthandlercontext.md) | |
| [TimeRange](./kibana-plugin-plugins-data-server.timerange.md) | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) &gt; [SearchRequestHandlerContext](./kibana-plugin-plugins-data-server.searchrequesthandlercontext.md)
## SearchRequestHandlerContext type
<b>Signature:</b>
```typescript
export declare type SearchRequestHandlerContext = IScopedSearchClient;
```

View file

@ -16,5 +16,6 @@ export interface SearchStrategyDependencies
| --- | --- | --- |
| [esClient](./kibana-plugin-plugins-data-server.searchstrategydependencies.esclient.md) | <code>IScopedClusterClient</code> | |
| [savedObjectsClient](./kibana-plugin-plugins-data-server.searchstrategydependencies.savedobjectsclient.md) | <code>SavedObjectsClientContract</code> | |
| [searchSessionsClient](./kibana-plugin-plugins-data-server.searchstrategydependencies.searchsessionsclient.md) | <code>IScopedSearchSessionsClient</code> | |
| [uiSettingsClient](./kibana-plugin-plugins-data-server.searchstrategydependencies.uisettingsclient.md) | <code>IUiSettingsClient</code> | |

View file

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

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-server](./kibana-plugin-plugins-data-server.md) &gt; [SessionService](./kibana-plugin-plugins-data-server.sessionservice.md) &gt; [(constructor)](./kibana-plugin-plugins-data-server.sessionservice._constructor_.md)
## SessionService.(constructor)
Constructs a new instance of the `SessionService` class
<b>Signature:</b>
```typescript
constructor();
```

View file

@ -1,26 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) &gt; [SessionService](./kibana-plugin-plugins-data-server.sessionservice.md) &gt; [asScopedProvider](./kibana-plugin-plugins-data-server.sessionservice.asscopedprovider.md)
## SessionService.asScopedProvider() method
<b>Signature:</b>
```typescript
asScopedProvider(core: CoreStart): (request: KibanaRequest) => {
search: <Request_1 extends IKibanaSearchRequest<any>, Response_1 extends IKibanaSearchResponse<any>>(strategy: ISearchStrategy<Request_1, Response_1>, request: Request_1, options: import("../../../common").ISearchOptions, deps: import("../types").SearchStrategyDependencies) => import("rxjs").Observable<Response_1>;
};
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| core | <code>CoreStart</code> | |
<b>Returns:</b>
`(request: KibanaRequest) => {
search: <Request_1 extends IKibanaSearchRequest<any>, Response_1 extends IKibanaSearchResponse<any>>(strategy: ISearchStrategy<Request_1, Response_1>, request: Request_1, options: import("../../../common").ISearchOptions, deps: import("../types").SearchStrategyDependencies) => import("rxjs").Observable<Response_1>;
}`

View file

@ -1,27 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) &gt; [SessionService](./kibana-plugin-plugins-data-server.sessionservice.md)
## SessionService class
The OSS session service. See data\_enhanced in X-Pack for the search session service.
<b>Signature:</b>
```typescript
export declare class SessionService implements ISessionService
```
## Constructors
| Constructor | Modifiers | Description |
| --- | --- | --- |
| [(constructor)()](./kibana-plugin-plugins-data-server.sessionservice._constructor_.md) | | Constructs a new instance of the <code>SessionService</code> class |
## Methods
| Method | Modifiers | Description |
| --- | --- | --- |
| [asScopedProvider(core)](./kibana-plugin-plugins-data-server.sessionservice.asscopedprovider.md) | | |
| [search(strategy, args)](./kibana-plugin-plugins-data-server.sessionservice.search.md) | | |

View file

@ -1,23 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) &gt; [SessionService](./kibana-plugin-plugins-data-server.sessionservice.md) &gt; [search](./kibana-plugin-plugins-data-server.sessionservice.search.md)
## SessionService.search() method
<b>Signature:</b>
```typescript
search<Request extends IKibanaSearchRequest, Response extends IKibanaSearchResponse>(strategy: ISearchStrategy<Request, Response>, ...args: Parameters<ISearchStrategy<Request, Response>['search']>): import("rxjs").Observable<Response>;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| strategy | <code>ISearchStrategy&lt;Request, Response&gt;</code> | |
| args | <code>Parameters&lt;ISearchStrategy&lt;Request, Response&gt;['search']&gt;</code> | |
<b>Returns:</b>
`import("rxjs").Observable<Response>`

View file

@ -12,10 +12,9 @@ import type {
CoreStart,
Plugin,
Logger,
RequestHandlerContext,
} from 'src/core/server';
import type { DataApiRequestHandlerContext } from 'src/plugins/data/server';
import type { DataRequestHandlerContext } from 'src/plugins/data/server';
import {
SearchExamplesPluginSetup,
@ -45,9 +44,7 @@ export class SearchExamplesPlugin
deps: SearchExamplesPluginSetupDeps
) {
this.logger.debug('search_examples: Setup');
const router = core.http.createRouter<
RequestHandlerContext & { search: DataApiRequestHandlerContext }
>();
const router = core.http.createRouter<DataRequestHandlerContext>();
core.getStartServices().then(([_, depsStart]) => {
const myStrategy = mySearchStrategyProvider(depsStart.data);

View file

@ -6,12 +6,10 @@
* Public License, v 1.
*/
import type { IRouter, RequestHandlerContext } from 'kibana/server';
import { DataApiRequestHandlerContext } from 'src/plugins/data/server';
import type { IRouter } from 'kibana/server';
import { DataRequestHandlerContext } from 'src/plugins/data/server';
import { registerServerSearchRoute } from './server_search_route';
export function registerRoutes(
router: IRouter<RequestHandlerContext & { search: DataApiRequestHandlerContext }>
) {
export function registerRoutes(router: IRouter<DataRequestHandlerContext>) {
registerServerSearchRoute(router);
}

View file

@ -9,13 +9,11 @@
import { IEsSearchRequest } from 'src/plugins/data/server';
import { schema } from '@kbn/config-schema';
import { IEsSearchResponse } from 'src/plugins/data/common';
import type { DataApiRequestHandlerContext } from 'src/plugins/data/server';
import type { IRouter, RequestHandlerContext } from 'src/core/server';
import type { DataRequestHandlerContext } from 'src/plugins/data/server';
import type { IRouter } from 'src/core/server';
import { SERVER_SEARCH_ROUTE_PATH } from '../../common';
export function registerServerSearchRoute(
router: IRouter<RequestHandlerContext & { search: DataApiRequestHandlerContext }>
) {
export function registerServerSearchRoute(router: IRouter<DataRequestHandlerContext>) {
router.get(
{
path: SERVER_SEARCH_ROUTE_PATH,

View file

@ -17,6 +17,7 @@ export function getSessionsClientMock(): jest.Mocked<ISessionsClient> {
create: jest.fn(),
find: jest.fn(),
update: jest.fn(),
extend: jest.fn(),
delete: jest.fn(),
};
}

View file

@ -68,6 +68,12 @@ export class SessionsClient {
});
}
public extend(sessionId: string, keepAlive: string): Promise<SavedObjectsFindResponse> {
return this.http!.post(`/internal/session/${encodeURIComponent(sessionId)}/_extend`, {
body: JSON.stringify({ keepAlive }),
});
}
public delete(sessionId: string): Promise<void> {
return this.http!.delete(`/internal/session/${encodeURIComponent(sessionId)}`);
}

View file

@ -229,10 +229,9 @@ export {
searchUsageObserver,
shimAbortSignal,
SearchUsage,
SessionService,
ISessionService,
IScopedSessionService,
DataApiRequestHandlerContext,
SearchSessionService,
ISearchSessionService,
SearchRequestHandlerContext,
DataRequestHandlerContext,
} from './search';

View file

@ -6,9 +6,14 @@
* Public License, v 1.
*/
import { createSearchSetupMock, createSearchStartMock } from './search/mocks';
import {
createSearchSetupMock,
createSearchStartMock,
createSearchRequestHandlerContext,
} from './search/mocks';
import { createFieldFormatsSetupMock, createFieldFormatsStartMock } from './field_formats/mocks';
import { createIndexPatternsStartMock } from './index_patterns/mocks';
import { DataRequestHandlerContext } from './search';
function createSetupContract() {
return {
@ -25,7 +30,14 @@ function createStartContract() {
};
}
function createRequestHandlerContext() {
return ({
search: createSearchRequestHandlerContext(),
} as unknown) as jest.Mocked<DataRequestHandlerContext>;
}
export const dataPluginMock = {
createSetupContract,
createStartContract,
createRequestHandlerContext,
};

View file

@ -10,6 +10,8 @@ import { ISearchSetup, ISearchStart } from './types';
import { searchAggsSetupMock, searchAggsStartMock } from './aggs/mocks';
import { searchSourceMock } from './search_source/mocks';
export { createSearchSessionsClientMock } from './session/mocks';
export function createSearchSetupMock(): jest.Mocked<ISearchSetup> {
return {
aggs: searchAggsSetupMock(),
@ -22,10 +24,21 @@ export function createSearchStartMock(): jest.Mocked<ISearchStart> {
return {
aggs: searchAggsStartMock(),
getSearchStrategy: jest.fn(),
asScoped: jest.fn().mockReturnValue({
search: jest.fn(),
cancel: jest.fn(),
}),
asScoped: jest.fn().mockReturnValue(createSearchRequestHandlerContext()),
searchSource: searchSourceMock.createStartContract(),
};
}
export function createSearchRequestHandlerContext() {
return {
search: jest.fn(),
cancel: jest.fn(),
extend: jest.fn(),
saveSession: jest.fn(),
getSession: jest.fn(),
findSessions: jest.fn(),
updateSession: jest.fn(),
extendSession: jest.fn(),
cancelSession: jest.fn(),
};
}

View file

@ -7,21 +7,17 @@
*/
import { catchError, first } from 'rxjs/operators';
import { CoreStart, KibanaRequest } from 'src/core/server';
import { BfetchServerSetup } from 'src/plugins/bfetch/server';
import {
IKibanaSearchRequest,
IKibanaSearchResponse,
ISearchClient,
ISearchOptions,
} from '../../../common/search';
type GetScopedProider = (coreStart: CoreStart) => (request: KibanaRequest) => ISearchClient;
import { ISearchStart } from '../types';
export function registerBsearchRoute(
bfetch: BfetchServerSetup,
coreStartPromise: Promise<[CoreStart, {}, {}]>,
getScopedProvider: GetScopedProider
getScoped: ISearchStart['asScoped']
): void {
bfetch.addBatchProcessingRoute<
{ request: IKibanaSearchRequest; options?: ISearchOptions },
@ -33,8 +29,7 @@ export function registerBsearchRoute(
* @throws `KibanaServerError`
*/
onBatchItem: async ({ request: requestData, options }) => {
const coreStart = await coreStartPromise;
const search = getScopedProvider(coreStart[0])(request);
const search = getScoped(request);
return search
.search(requestData, options)
.pipe(

View file

@ -6,8 +6,9 @@
* Public License, v 1.
*/
import { BehaviorSubject, Observable, throwError } from 'rxjs';
import { BehaviorSubject, from, Observable, throwError } from 'rxjs';
import { pick } from 'lodash';
import moment from 'moment';
import {
CoreSetup,
CoreStart,
@ -18,10 +19,11 @@ import {
SharedGlobalConfig,
StartServicesAccessor,
} from 'src/core/server';
import { first } from 'rxjs/operators';
import { first, switchMap } from 'rxjs/operators';
import { BfetchServerSetup } from 'src/plugins/bfetch/server';
import { ExpressionsServerSetup } from 'src/plugins/expressions/server';
import type {
IScopedSearchClient,
ISearchSetup,
ISearchStart,
ISearchStrategy,
@ -46,7 +48,6 @@ import {
IEsSearchResponse,
IKibanaSearchRequest,
IKibanaSearchResponse,
ISearchClient,
ISearchOptions,
kibana,
kibanaContext,
@ -62,8 +63,9 @@ import {
} from '../../common/search/aggs/buckets/shard_delay';
import { aggShardDelay } from '../../common/search/aggs/buckets/shard_delay_fn';
import { ConfigSchema } from '../../config';
import { IScopedSessionService, ISessionService, SessionService } from './session';
import { ISearchSessionService, SearchSessionService } from './session';
import { KbnServerError } from '../../../kibana_utils/server';
import { tapFirst } from '../../common';
import { registerBsearchRoute } from './routes/bsearch';
type StrategyMap = Record<string, ISearchStrategy<any, any>>;
@ -92,14 +94,14 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
private readonly searchSourceService = new SearchSourceService();
private defaultSearchStrategyName: string = ES_SEARCH_STRATEGY;
private searchStrategies: StrategyMap = {};
private sessionService: ISessionService;
private coreStart?: CoreStart;
private sessionService: ISearchSessionService;
private asScoped!: ISearchStart['asScoped'];
constructor(
private initializerContext: PluginInitializerContext<ConfigSchema>,
private readonly logger: Logger
) {
this.sessionService = new SessionService();
this.sessionService = new SearchSessionService();
}
public setup(
@ -116,16 +118,10 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
registerSearchRoute(router);
registerMsearchRoute(router, routeDependencies);
core.getStartServices().then(([coreStart]) => {
this.coreStart = coreStart;
});
core.http.registerRouteHandlerContext<DataRequestHandlerContext, 'search'>(
'search',
async (context, request) => {
const search = this.asScopedProvider(this.coreStart!)(request);
const session = this.sessionService.asScopedProvider(this.coreStart!)(request);
return { ...search, session };
return this.asScoped(request);
}
);
@ -138,7 +134,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
)
);
registerBsearchRoute(bfetch, core.getStartServices(), this.asScopedProvider);
registerBsearchRoute(bfetch, (request: KibanaRequest) => this.asScoped(request));
core.savedObjects.registerType(searchTelemetry);
if (usageCollection) {
@ -181,7 +177,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
{ fieldFormats, indexPatterns }: SearchServiceStartDependencies
): ISearchStart {
const { elasticsearch, savedObjects, uiSettings } = core;
const asScoped = this.asScopedProvider(core);
this.asScoped = this.asScopedProvider(core);
return {
aggs: this.aggsService.start({
fieldFormats,
@ -189,7 +185,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
indexPatterns,
}),
getSearchStrategy: this.getSearchStrategy,
asScoped,
asScoped: this.asScoped,
searchSource: {
asScoped: async (request: KibanaRequest) => {
const esClient = elasticsearch.client.asScoped(request);
@ -208,7 +204,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
const searchSourceDependencies: SearchSourceDependencies = {
getConfig: <T = any>(key: string): T => uiSettingsCache[key],
search: asScoped(request).search,
search: this.asScoped(request).search,
onResponse: (req, res) => res,
legacy: {
callMsearch: getCallMsearch({
@ -241,49 +237,6 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
this.searchStrategies[name] = strategy;
};
private search = <
SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest,
SearchStrategyResponse extends IKibanaSearchResponse = IEsSearchResponse
>(
session: IScopedSessionService,
request: SearchStrategyRequest,
options: ISearchOptions,
deps: SearchStrategyDependencies
) => {
try {
const strategy = this.getSearchStrategy<SearchStrategyRequest, SearchStrategyResponse>(
options.strategy
);
return session.search(strategy, request, options, deps);
} catch (e) {
return throwError(e);
}
};
private cancel = (id: string, options: ISearchOptions, deps: SearchStrategyDependencies) => {
const strategy = this.getSearchStrategy(options.strategy);
if (!strategy.cancel) {
throw new KbnServerError(
`Search strategy ${options.strategy} doesn't support cancellations`,
400
);
}
return strategy.cancel(id, options, deps);
};
private extend = (
id: string,
keepAlive: string,
options: ISearchOptions,
deps: SearchStrategyDependencies
) => {
const strategy = this.getSearchStrategy(options.strategy);
if (!strategy.extend) {
throw new KbnServerError(`Search strategy ${options.strategy} does not support extend`, 400);
}
return strategy.extend(id, keepAlive, options, deps);
};
private getSearchStrategy = <
SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest,
SearchStrategyResponse extends IKibanaSearchResponse = IEsSearchResponse
@ -298,22 +251,128 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
return strategy;
};
private search = <
SearchStrategyRequest extends IKibanaSearchRequest,
SearchStrategyResponse extends IKibanaSearchResponse
>(
deps: SearchStrategyDependencies,
request: SearchStrategyRequest,
options: ISearchOptions
) => {
try {
const strategy = this.getSearchStrategy<SearchStrategyRequest, SearchStrategyResponse>(
options.strategy
);
const getSearchRequest = async () =>
!options.sessionId || !options.isRestore || request.id
? request
: {
...request,
id: await deps.searchSessionsClient.getId(request, options),
};
return from(getSearchRequest()).pipe(
switchMap((searchRequest) => strategy.search(searchRequest, options, deps)),
tapFirst((response) => {
if (request.id || !options.sessionId || !response.id || options.isRestore) return;
deps.searchSessionsClient.trackId(request, response.id, options);
})
);
} catch (e) {
return throwError(e);
}
};
private cancel = (deps: SearchStrategyDependencies, id: string, options: ISearchOptions = {}) => {
const strategy = this.getSearchStrategy(options.strategy);
if (!strategy.cancel) {
throw new KbnServerError(
`Search strategy ${options.strategy} doesn't support cancellations`,
400
);
}
return strategy.cancel(id, options, deps);
};
private extend = (
deps: SearchStrategyDependencies,
id: string,
keepAlive: string,
options: ISearchOptions = {}
) => {
const strategy = this.getSearchStrategy(options.strategy);
if (!strategy.extend) {
throw new KbnServerError(`Search strategy ${options.strategy} does not support extend`, 400);
}
return strategy.extend(id, keepAlive, options, deps);
};
private cancelSession = async (deps: SearchStrategyDependencies, sessionId: string) => {
const searchIdMapping = await deps.searchSessionsClient.getSearchIdMapping(sessionId);
const response = await deps.searchSessionsClient.cancel(sessionId);
for (const [searchId, strategyName] of searchIdMapping.entries()) {
const searchOptions = {
sessionId,
strategy: strategyName,
isStored: true,
};
this.cancel(deps, searchId, searchOptions);
}
return response;
};
private extendSession = async (
deps: SearchStrategyDependencies,
sessionId: string,
expires: Date
) => {
const searchIdMapping = await deps.searchSessionsClient.getSearchIdMapping(sessionId);
const keepAlive = `${moment(expires).diff(moment())}ms`;
for (const [searchId, strategyName] of searchIdMapping.entries()) {
const searchOptions = {
sessionId,
strategy: strategyName,
isStored: true,
};
await this.extend(deps, searchId, keepAlive, searchOptions);
}
return deps.searchSessionsClient.extend(sessionId, expires);
};
private asScopedProvider = (core: CoreStart) => {
const { elasticsearch, savedObjects, uiSettings } = core;
const getSessionAsScoped = this.sessionService.asScopedProvider(core);
return (request: KibanaRequest): ISearchClient => {
const scopedSession = getSessionAsScoped(request);
return (request: KibanaRequest): IScopedSearchClient => {
const savedObjectsClient = savedObjects.getScopedClient(request);
const searchSessionsClient = getSessionAsScoped(request);
const deps = {
searchSessionsClient,
savedObjectsClient,
esClient: elasticsearch.client.asScoped(request),
uiSettingsClient: uiSettings.asScopedToClient(savedObjectsClient),
};
return {
search: (searchRequest, options = {}) =>
this.search(scopedSession, searchRequest, options, deps),
cancel: (id, options = {}) => this.cancel(id, options, deps),
extend: (id, keepAlive, options = {}) => this.extend(id, keepAlive, options, deps),
search: <
SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest,
SearchStrategyResponse extends IKibanaSearchResponse = IEsSearchResponse
>(
searchRequest: SearchStrategyRequest,
options: ISearchOptions = {}
) =>
this.search<SearchStrategyRequest, SearchStrategyResponse>(deps, searchRequest, options),
cancel: this.cancel.bind(this, deps),
extend: this.extend.bind(this, deps),
saveSession: searchSessionsClient.save,
getSession: searchSessionsClient.get,
findSessions: searchSessionsClient.find,
updateSession: searchSessionsClient.update,
extendSession: this.extendSession.bind(this, deps),
cancelSession: this.cancelSession.bind(this, deps),
};
};
};

View file

@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* and the Server Side Public License, v 1; you may not use this file except in
* compliance with, at your election, the Elastic License or the Server Side
* Public License, v 1.
*/
import { IScopedSearchSessionsClient } from './types';
export function createSearchSessionsClientMock<T = unknown>(): jest.Mocked<
IScopedSearchSessionsClient<T>
> {
return {
getId: jest.fn(),
trackId: jest.fn(),
getSearchIdMapping: jest.fn(),
save: jest.fn(),
get: jest.fn(),
find: jest.fn(),
update: jest.fn(),
cancel: jest.fn(),
extend: jest.fn(),
};
}

View file

@ -1,27 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* and the Server Side Public License, v 1; you may not use this file except in
* compliance with, at your election, the Elastic License or the Server Side
* Public License, v 1.
*/
import { of } from 'rxjs';
import { SearchStrategyDependencies } from '../types';
import { SessionService } from './session_service';
describe('SessionService', () => {
it('search invokes `strategy.search`', async () => {
const service = new SessionService();
const mockSearch = jest.fn().mockReturnValue(of({}));
const mockStrategy = { search: mockSearch };
const mockRequest = { id: 'bar' };
const mockOptions = { sessionId: '1234' };
const mockDeps = { savedObjectsClient: {} } as SearchStrategyDependencies;
await service.search(mockStrategy, mockRequest, mockOptions, mockDeps);
expect(mockSearch).toHaveBeenCalled();
expect(mockSearch).toHaveBeenCalledWith(mockRequest, mockOptions, mockDeps);
});
});

View file

@ -6,27 +6,41 @@
* Public License, v 1.
*/
import { CoreStart, KibanaRequest } from 'kibana/server';
import { IKibanaSearchRequest, IKibanaSearchResponse } from '../../../common';
import { ISearchStrategy } from '../types';
import { ISessionService } from './types';
import { ISearchSessionService } from './types';
/**
* The OSS session service. See data_enhanced in X-Pack for the search session service.
* The OSS session service, which leaves most search session-related logic unimplemented.
* @see x-pack/plugins/data_enhanced/server/search/session/session_service.ts
* @internal
*/
export class SessionService implements ISessionService {
export class SearchSessionService implements ISearchSessionService {
constructor() {}
public search<Request extends IKibanaSearchRequest, Response extends IKibanaSearchResponse>(
strategy: ISearchStrategy<Request, Response>,
...args: Parameters<ISearchStrategy<Request, Response>['search']>
) {
return strategy.search(...args);
}
public asScopedProvider(core: CoreStart) {
return (request: KibanaRequest) => ({
search: this.search,
public asScopedProvider() {
return () => ({
getId: () => {
throw new Error('getId not implemented in OSS search session service');
},
trackId: async () => {},
getSearchIdMapping: async () => new Map<string, string>(),
save: async () => {
throw new Error('save not implemented in OSS search session service');
},
get: async () => {
throw new Error('get not implemented in OSS search session service');
},
find: async () => {
throw new Error('find not implemented in OSS search session service');
},
update: async () => {
throw new Error('update not implemented in OSS search session service');
},
extend: async () => {
throw new Error('extend not implemented in OSS search session service');
},
cancel: async () => {
throw new Error('cancel not implemented in OSS search session service');
},
});
}
}

View file

@ -6,19 +6,32 @@
* Public License, v 1.
*/
import { Observable } from 'rxjs';
import { CoreStart, KibanaRequest } from 'kibana/server';
import { ISearchStrategy } from '../types';
import { IKibanaSearchRequest, IKibanaSearchResponse } from '../../../common/search';
import {
CoreStart,
KibanaRequest,
SavedObject,
SavedObjectsFindOptions,
SavedObjectsFindResponse,
SavedObjectsUpdateResponse,
} from 'kibana/server';
import { IKibanaSearchRequest, ISearchOptions } from '../../../common/search';
export interface IScopedSessionService {
search: <Request extends IKibanaSearchRequest, Response extends IKibanaSearchResponse>(
strategy: ISearchStrategy<Request, Response>,
...args: Parameters<ISearchStrategy<Request, Response>['search']>
) => Observable<Response>;
[prop: string]: any;
export interface IScopedSearchSessionsClient<T = unknown> {
getId: (request: IKibanaSearchRequest, options: ISearchOptions) => Promise<string>;
trackId: (
request: IKibanaSearchRequest,
searchId: string,
options: ISearchOptions
) => Promise<void>;
getSearchIdMapping: (sessionId: string) => Promise<Map<string, string>>;
save: (sessionId: string, attributes: Partial<T>) => Promise<SavedObject<T>>;
get: (sessionId: string) => Promise<SavedObject<T>>;
find: (options: Omit<SavedObjectsFindOptions, 'type'>) => Promise<SavedObjectsFindResponse<T>>;
update: (sessionId: string, attributes: Partial<T>) => Promise<SavedObjectsUpdateResponse<T>>;
cancel: (sessionId: string) => Promise<{}>;
extend: (sessionId: string, expires: Date) => Promise<SavedObjectsUpdateResponse<T>>;
}
export interface ISessionService {
asScopedProvider: (core: CoreStart) => (request: KibanaRequest) => IScopedSessionService;
export interface ISearchSessionService<T = unknown> {
asScopedProvider: (core: CoreStart) => (request: KibanaRequest) => IScopedSearchSessionsClient<T>;
}

View file

@ -25,17 +25,18 @@ import {
import { AggsSetup, AggsStart } from './aggs';
import { SearchUsage } from './collectors';
import { IEsSearchRequest, IEsSearchResponse } from './es_search';
import { ISessionService, IScopedSessionService } from './session';
import { IScopedSearchSessionsClient, ISearchSessionService } from './session';
export interface SearchEnhancements {
defaultStrategy: string;
sessionService: ISessionService;
sessionService: ISearchSessionService;
}
export interface SearchStrategyDependencies {
savedObjectsClient: SavedObjectsClientContract;
esClient: IScopedClusterClient;
uiSettingsClient: IUiSettingsClient;
searchSessionsClient: IScopedSearchSessionsClient;
}
export interface ISearchSetup {
@ -85,6 +86,15 @@ export interface ISearchStrategy<
) => Promise<void>;
}
export interface IScopedSearchClient extends ISearchClient {
saveSession: IScopedSearchSessionsClient['save'];
getSession: IScopedSearchSessionsClient['get'];
findSessions: IScopedSearchSessionsClient['find'];
updateSession: IScopedSearchSessionsClient['update'];
cancelSession: IScopedSearchSessionsClient['cancel'];
extendSession: IScopedSearchSessionsClient['extend'];
}
export interface ISearchStart<
SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest,
SearchStrategyResponse extends IKibanaSearchResponse = IEsSearchResponse
@ -98,21 +108,19 @@ export interface ISearchStart<
getSearchStrategy: (
name?: string // Name of the search strategy (defaults to the Elasticsearch strategy)
) => ISearchStrategy<SearchStrategyRequest, SearchStrategyResponse>;
asScoped: (request: KibanaRequest) => ISearchClient;
asScoped: (request: KibanaRequest) => IScopedSearchClient;
searchSource: {
asScoped: (request: KibanaRequest) => Promise<ISearchStartSearchSource>;
};
}
export interface DataApiRequestHandlerContext extends ISearchClient {
session: IScopedSessionService;
}
export type SearchRequestHandlerContext = IScopedSearchClient;
/**
* @internal
*/
export interface DataRequestHandlerContext extends RequestHandlerContext {
search: DataApiRequestHandlerContext;
search: SearchRequestHandlerContext;
}
export type DataPluginRouter = IRouter<DataRequestHandlerContext>;

View file

@ -55,9 +55,13 @@ import { RecursiveReadonly } from '@kbn/utility-types';
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 { SavedObject } from 'kibana/server';
import { SavedObject as SavedObject_2 } from 'src/core/server';
import { SavedObjectsClientContract } from 'src/core/server';
import { SavedObjectsClientContract as SavedObjectsClientContract_2 } from 'kibana/server';
import { SavedObjectsFindOptions } from 'kibana/server';
import { SavedObjectsFindResponse } from 'kibana/server';
import { SavedObjectsUpdateResponse } from 'kibana/server';
import { Search } from '@elastic/elasticsearch/api/requestParams';
import { SearchResponse } from 'elasticsearch';
import { SerializedFieldFormat as SerializedFieldFormat_2 } from 'src/plugins/expressions/common';
@ -305,19 +309,10 @@ export const castEsToKbnFieldTypeName: (esType: ES_FIELD_TYPES | string) => KBN_
// @public (undocumented)
export const config: PluginConfigDescriptor<ConfigSchema>;
// Warning: (ae-forgotten-export) The symbol "ISearchClient" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "DataApiRequestHandlerContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface DataApiRequestHandlerContext extends ISearchClient {
// (undocumented)
session: IScopedSessionService;
}
// @internal (undocumented)
export interface DataRequestHandlerContext extends RequestHandlerContext {
// (undocumented)
search: DataApiRequestHandlerContext;
search: SearchRequestHandlerContext;
}
// @public (undocumented)
@ -910,16 +905,6 @@ export class IndexPatternsService implements Plugin_3<void, IndexPatternsService
};
}
// Warning: (ae-missing-release-tag) "IScopedSessionService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface IScopedSessionService {
// (undocumented)
[prop: string]: any;
// (undocumented)
search: <Request extends IKibanaSearchRequest, Response extends IKibanaSearchResponse>(strategy: ISearchStrategy<Request, Response>, ...args: Parameters<ISearchStrategy<Request, Response>['search']>) => Observable<Response>;
}
// Warning: (ae-missing-release-tag) "ISearchOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@ -932,6 +917,16 @@ export interface ISearchOptions {
strategy?: string;
}
// Warning: (ae-missing-release-tag) "ISearchSessionService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface ISearchSessionService<T = unknown> {
// Warning: (ae-forgotten-export) The symbol "IScopedSearchSessionsClient" needs to be exported by the entry point index.d.ts
//
// (undocumented)
asScopedProvider: (core: CoreStart) => (request: KibanaRequest) => IScopedSearchSessionsClient<T>;
}
// Warning: (ae-missing-release-tag) "ISearchSetup" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@ -956,8 +951,10 @@ export interface ISearchStart<SearchStrategyRequest extends IKibanaSearchRequest
//
// (undocumented)
aggs: AggsStart;
// Warning: (ae-forgotten-export) The symbol "IScopedSearchClient" needs to be exported by the entry point index.d.ts
//
// (undocumented)
asScoped: (request: KibanaRequest_2) => ISearchClient;
asScoped: (request: KibanaRequest_2) => IScopedSearchClient;
getSearchStrategy: (name?: string) => ISearchStrategy<SearchStrategyRequest, SearchStrategyResponse>;
// (undocumented)
searchSource: {
@ -977,14 +974,6 @@ export interface ISearchStrategy<SearchStrategyRequest extends IKibanaSearchRequ
search: (request: SearchStrategyRequest, options: ISearchOptions, deps: SearchStrategyDependencies) => Observable<SearchStrategyResponse>;
}
// Warning: (ae-missing-release-tag) "ISessionService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface ISessionService {
// (undocumented)
asScopedProvider: (core: CoreStart) => (request: KibanaRequest) => IScopedSessionService;
}
// @public (undocumented)
export enum KBN_FIELD_TYPES {
// (undocumented)
@ -1246,6 +1235,28 @@ export const search: {
tabifyGetColumns: typeof tabifyGetColumns;
};
// Warning: (ae-missing-release-tag) "SearchRequestHandlerContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type SearchRequestHandlerContext = IScopedSearchClient;
// @internal
export class SearchSessionService implements ISearchSessionService {
constructor();
// (undocumented)
asScopedProvider(): () => {
getId: () => never;
trackId: () => Promise<void>;
getSearchIdMapping: () => Promise<Map<string, string>>;
save: () => Promise<never>;
get: () => Promise<never>;
find: () => Promise<never>;
update: () => Promise<never>;
extend: () => Promise<never>;
cancel: () => Promise<never>;
};
}
// Warning: (ae-missing-release-tag) "SearchStrategyDependencies" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@ -1255,6 +1266,8 @@ export interface SearchStrategyDependencies {
// (undocumented)
savedObjectsClient: SavedObjectsClientContract;
// (undocumented)
searchSessionsClient: IScopedSearchSessionsClient;
// (undocumented)
uiSettingsClient: IUiSettingsClient;
}
@ -1276,19 +1289,6 @@ export function searchUsageObserver(logger: Logger_2, usage?: SearchUsage): {
error(): void;
};
// Warning: (ae-missing-release-tag) "SessionService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export class SessionService implements ISessionService {
constructor();
// (undocumented)
asScopedProvider(core: CoreStart): (request: KibanaRequest) => {
search: <Request_1 extends IKibanaSearchRequest<any>, Response_1 extends IKibanaSearchResponse<any>>(strategy: ISearchStrategy<Request_1, Response_1>, request: Request_1, options: import("../../../common").ISearchOptions, deps: import("../types").SearchStrategyDependencies) => import("rxjs").Observable<Response_1>;
};
// (undocumented)
search<Request extends IKibanaSearchRequest, Response extends IKibanaSearchResponse>(strategy: ISearchStrategy<Request, Response>, ...args: Parameters<ISearchStrategy<Request, Response>['search']>): import("rxjs").Observable<Response>;
}
// @internal
export const shimAbortSignal: <T>(promise: TransportRequestPromise<T>, signal?: AbortSignal | undefined) => TransportRequestPromise<T>;
@ -1414,23 +1414,23 @@ export function usageProvider(core: CoreSetup_2): SearchUsage;
// src/plugins/data/server/index.ts:100:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:126:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:126:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:240:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:240:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:240:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:240:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:242:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:243:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:252:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:253:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:254:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:258:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:259:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:263:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:266:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:267:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:239:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:239:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:239:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:239:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:241:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:242:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:251:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:252:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:253:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:257:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:258:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:262:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:265:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:266:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index_patterns/index_patterns_service.ts:59:14 - (ae-forgotten-export) The symbol "IndexPatternsService" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/plugin.ts:79:74 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/search/types.ts:103:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/search/types.ts:113:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts
// (No @packageDocumentation comment for this package)

View file

@ -11,12 +11,8 @@ import { first } from 'rxjs/operators';
import { TypeOf, schema } from '@kbn/config-schema';
import { RecursiveReadonly } from '@kbn/utility-types';
import { deepFreeze } from '@kbn/std';
import type { RequestHandlerContext } from 'src/core/server';
import type {
PluginStart,
DataApiRequestHandlerContext,
} from '../../../../src/plugins/data/server';
import type { PluginStart, DataRequestHandlerContext } from '../../../../src/plugins/data/server';
import { CoreSetup, PluginInitializerContext } from '../../../../src/core/server';
import { configSchema } from '../config';
import loadFunctions from './lib/load_functions';
@ -73,9 +69,7 @@ export class Plugin {
const logger = this.initializerContext.logger.get('timelion');
const router = core.http.createRouter<
RequestHandlerContext & { search: DataApiRequestHandlerContext }
>();
const router = core.http.createRouter<DataRequestHandlerContext>();
const deps = {
configManager,

View file

@ -7,12 +7,10 @@
*/
import _ from 'lodash';
import { IRouter, RequestHandlerContext } from 'kibana/server';
import type { DataApiRequestHandlerContext } from '../../../data/server';
import { IRouter } from 'kibana/server';
import type { DataRequestHandlerContext } from '../../../data/server';
export function validateEsRoute(
router: IRouter<RequestHandlerContext & { search: DataApiRequestHandlerContext }>
) {
export function validateEsRoute(router: IRouter<DataRequestHandlerContext>) {
router.get(
{
path: '/api/timelion/validate/es',

View file

@ -6,11 +6,8 @@
* Public License, v 1.
*/
import type { IRouter, RequestHandlerContext } from 'src/core/server';
import type { DataApiRequestHandlerContext } from '../../data/server';
export interface VisTypeTimeseriesRequestHandlerContext extends RequestHandlerContext {
search: DataApiRequestHandlerContext;
}
import type { IRouter } from 'src/core/server';
import type { DataRequestHandlerContext } from '../../data/server';
export type VisTypeTimeseriesRequestHandlerContext = DataRequestHandlerContext;
export type VisTypeTimeseriesRouter = IRouter<VisTypeTimeseriesRequestHandlerContext>;

View file

@ -1,26 +0,0 @@
/*
* 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 type { DataRequestHandlerContext } from '../../../../../src/plugins/data/server';
import { coreMock } from '../../../../../src/core/server/mocks';
export function createSearchRequestHandlerContext() {
return ({
core: coreMock.createRequestHandlerContext(),
search: {
search: jest.fn(),
cancel: jest.fn(),
session: {
search: jest.fn(),
save: jest.fn(),
get: jest.fn(),
find: jest.fn(),
delete: jest.fn(),
update: jest.fn(),
},
},
} as unknown) as jest.Mocked<DataRequestHandlerContext>;
}

View file

@ -12,7 +12,7 @@ import type {
PluginStart as DataPluginStart,
DataRequestHandlerContext,
} from '../../../../../src/plugins/data/server';
import { createSearchRequestHandlerContext } from './mocks';
import { dataPluginMock } from '../../../../../src/plugins/data/server/mocks';
import { registerSessionRoutes } from './session';
describe('registerSessionRoutes', () => {
@ -23,11 +23,11 @@ describe('registerSessionRoutes', () => {
beforeEach(() => {
mockCoreSetup = coreMock.createSetup();
mockLogger = coreMock.createPluginInitializerContext().logger.get();
mockContext = createSearchRequestHandlerContext();
mockContext = dataPluginMock.createRequestHandlerContext();
registerSessionRoutes(mockCoreSetup.http.createRouter(), mockLogger);
});
it('save calls session.save with sessionId and attributes', async () => {
it('save calls saveSession with sessionId and attributes', async () => {
const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489';
const name = 'my saved background search session';
const body = { sessionId, name };
@ -40,10 +40,10 @@ describe('registerSessionRoutes', () => {
saveHandler(mockContext, mockRequest, mockResponse);
expect(mockContext.search!.session.save).toHaveBeenCalledWith(sessionId, { name });
expect(mockContext.search!.saveSession).toHaveBeenCalledWith(sessionId, { name });
});
it('get calls session.get with sessionId', async () => {
it('get calls getSession with sessionId', async () => {
const id = 'd7170a35-7e2c-48d6-8dec-9a056721b489';
const params = { id };
@ -55,10 +55,10 @@ describe('registerSessionRoutes', () => {
getHandler(mockContext, mockRequest, mockResponse);
expect(mockContext.search!.session.get).toHaveBeenCalledWith(id);
expect(mockContext.search!.getSession).toHaveBeenCalledWith(id);
});
it('find calls session.find with options', async () => {
it('find calls findSession with options', async () => {
const page = 1;
const perPage = 5;
const sortField = 'my_field';
@ -74,10 +74,10 @@ describe('registerSessionRoutes', () => {
findHandler(mockContext, mockRequest, mockResponse);
expect(mockContext.search!.session.find).toHaveBeenCalledWith(body);
expect(mockContext.search!.findSessions).toHaveBeenCalledWith(body);
});
it('update calls session.update with id and attributes', async () => {
it('update calls updateSession with id and attributes', async () => {
const id = 'd7170a35-7e2c-48d6-8dec-9a056721b489';
const name = 'my saved background search session';
const expires = new Date().toISOString();
@ -92,10 +92,10 @@ describe('registerSessionRoutes', () => {
updateHandler(mockContext, mockRequest, mockResponse);
expect(mockContext.search!.session.update).toHaveBeenCalledWith(id, body);
expect(mockContext.search!.updateSession).toHaveBeenCalledWith(id, body);
});
it('delete calls session.delete with id', async () => {
it('delete calls cancelSession with id', async () => {
const id = 'd7170a35-7e2c-48d6-8dec-9a056721b489';
const params = { id };
@ -107,6 +107,23 @@ describe('registerSessionRoutes', () => {
deleteHandler(mockContext, mockRequest, mockResponse);
expect(mockContext.search!.session.delete).toHaveBeenCalledWith(id);
expect(mockContext.search!.cancelSession).toHaveBeenCalledWith(id);
});
it('extend calls extendSession with id', async () => {
const id = 'd7170a35-7e2c-48d6-8dec-9a056721b489';
const expires = new Date().toISOString();
const params = { id };
const body = { expires };
const mockRequest = httpServerMock.createKibanaRequest({ params, body });
const mockResponse = httpServerMock.createResponseFactory();
const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value;
const [, , [, extendHandler]] = mockRouter.post.mock.calls;
extendHandler(mockContext, mockRequest, mockResponse);
expect(mockContext.search.extendSession).toHaveBeenCalledWith(id, new Date(expires));
});
});

View file

@ -37,7 +37,7 @@ export function registerSessionRoutes(router: DataEnhancedPluginRouter, logger:
} = request.body;
try {
const response = await context.search!.session.save(sessionId, {
const response = await context.search!.saveSession(sessionId, {
name,
appId,
expires,
@ -68,7 +68,7 @@ export function registerSessionRoutes(router: DataEnhancedPluginRouter, logger:
async (context, request, res) => {
const { id } = request.params;
try {
const response = await context.search!.session.get(id);
const response = await context.search!.getSession(id);
return res.ok({
body: response,
@ -97,7 +97,7 @@ export function registerSessionRoutes(router: DataEnhancedPluginRouter, logger:
async (context, request, res) => {
const { page, perPage, sortField, sortOrder, filter } = request.body;
try {
const response = await context.search!.session.find({
const response = await context.search!.findSessions({
page,
perPage,
sortField,
@ -127,7 +127,7 @@ export function registerSessionRoutes(router: DataEnhancedPluginRouter, logger:
async (context, request, res) => {
const { id } = request.params;
try {
await context.search!.session.delete(id);
await context.search!.cancelSession(id);
return res.ok();
} catch (e) {
@ -155,7 +155,7 @@ export function registerSessionRoutes(router: DataEnhancedPluginRouter, logger:
const { id } = request.params;
const { name, expires } = request.body;
try {
const response = await context.search!.session.update(id, { name, expires });
const response = await context.search!.updateSession(id, { name, expires });
return res.ok({
body: response,
@ -166,4 +166,33 @@ export function registerSessionRoutes(router: DataEnhancedPluginRouter, logger:
}
}
);
router.post(
{
path: '/internal/session/{id}/_extend',
validate: {
params: schema.object({
id: schema.string(),
}),
body: schema.object({
expires: schema.string(),
}),
},
},
async (context, request, res) => {
const { id } = request.params;
const { expires } = request.body;
try {
const response = await context.search!.extendSession(id, new Date(expires));
return res.ok({
body: response,
});
} catch (e) {
const err = e.output?.payload || e;
logger.error(err);
return reportServerError(res, err);
}
}
);
}

View file

@ -4,20 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { BehaviorSubject, of } from 'rxjs';
import { BehaviorSubject } from 'rxjs';
import type { SavedObject, SavedObjectsClientContract } from 'kibana/server';
import type { SearchStrategyDependencies } from '../../../../../../src/plugins/data/server';
import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks';
import { SearchSessionStatus } from '../../../common';
import { SEARCH_SESSION_TYPE } from '../../saved_objects';
import { SearchSessionDependencies, SearchSessionService, SessionInfo } from './session_service';
import { SearchSessionService, SessionInfo } from './session_service';
import { createRequestHash } from './utils';
import moment from 'moment';
import { coreMock } from 'src/core/server/mocks';
import { ConfigSchema } from '../../../config';
// @ts-ignore
import { taskManagerMock } from '../../../../task_manager/server/mocks';
import { SearchStatus } from './types';
const INMEM_TRACKING_INTERVAL = 10000;
const MAX_UPDATE_RETRIES = 3;
@ -139,13 +137,13 @@ describe('SearchSessionService', () => {
});
it('search throws if `name` is not provided', () => {
expect(() => service.save(sessionId, {}, { savedObjectsClient })).rejects.toMatchInlineSnapshot(
expect(() => service.save({ savedObjectsClient }, sessionId, {})).rejects.toMatchInlineSnapshot(
`[Error: Name is required]`
);
});
it('save throws if `name` is not provided', () => {
expect(() => service.save(sessionId, {}, { savedObjectsClient })).rejects.toMatchInlineSnapshot(
expect(() => service.save({ savedObjectsClient }, sessionId, {})).rejects.toMatchInlineSnapshot(
`[Error: Name is required]`
);
});
@ -153,7 +151,7 @@ describe('SearchSessionService', () => {
it('get calls saved objects client', async () => {
savedObjectsClient.get.mockResolvedValue(mockSavedObject);
const response = await service.get(sessionId, { savedObjectsClient });
const response = await service.get({ savedObjectsClient }, sessionId);
expect(response).toBe(mockSavedObject);
expect(savedObjectsClient.get).toHaveBeenCalledWith(SEARCH_SESSION_TYPE, sessionId);
@ -173,7 +171,7 @@ describe('SearchSessionService', () => {
savedObjectsClient.find.mockResolvedValue(mockResponse);
const options = { page: 0, perPage: 5 };
const response = await service.find(options, { savedObjectsClient });
const response = await service.find({ savedObjectsClient }, options);
expect(response).toBe(mockResponse);
expect(savedObjectsClient.find).toHaveBeenCalledWith({
@ -190,7 +188,7 @@ describe('SearchSessionService', () => {
savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject);
const attributes = { name: 'new_name' };
const response = await service.update(sessionId, attributes, { savedObjectsClient });
const response = await service.update({ savedObjectsClient }, sessionId, attributes);
expect(response).toBe(mockUpdateSavedObject);
expect(savedObjectsClient.update).toHaveBeenCalledWith(
@ -200,93 +198,11 @@ describe('SearchSessionService', () => {
);
});
it('delete calls saved objects client', async () => {
savedObjectsClient.delete.mockResolvedValue({});
it('cancel updates object status', async () => {
await service.cancel({ savedObjectsClient }, sessionId);
const response = await service.delete(sessionId, { savedObjectsClient });
expect(response).toEqual({});
expect(savedObjectsClient.delete).toHaveBeenCalledWith(SEARCH_SESSION_TYPE, sessionId);
});
describe('search', () => {
const mockSearch = jest.fn().mockReturnValue(of({}));
const mockStrategy = { search: mockSearch };
const mockSearchDeps = {} as SearchStrategyDependencies;
const mockDeps = {} as SearchSessionDependencies;
beforeEach(() => {
mockSearch.mockClear();
});
it('searches using the original request if not restoring', async () => {
const searchRequest = { params: {} };
const options = { sessionId, isStored: false, isRestore: false };
await service
.search(mockStrategy, searchRequest, options, mockSearchDeps, mockDeps)
.toPromise();
expect(mockSearch).toBeCalledWith(searchRequest, options, mockSearchDeps);
});
it('searches using the original request if `id` is provided', async () => {
const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0';
const searchRequest = { id: searchId, params: {} };
const options = { sessionId, isStored: true, isRestore: true };
await service
.search(mockStrategy, searchRequest, options, mockSearchDeps, mockDeps)
.toPromise();
expect(mockSearch).toBeCalledWith(searchRequest, options, mockSearchDeps);
});
it('searches by looking up an `id` if restoring and `id` is not provided', async () => {
const searchRequest = { params: {} };
const options = { sessionId, isStored: true, isRestore: true };
const spyGetId = jest.spyOn(service, 'getId').mockResolvedValueOnce('my_id');
await service
.search(mockStrategy, searchRequest, options, mockSearchDeps, mockDeps)
.toPromise();
expect(mockSearch).toBeCalledWith({ ...searchRequest, id: 'my_id' }, options, mockSearchDeps);
spyGetId.mockRestore();
});
it('calls `trackId` once if the response contains an `id` and not restoring', async () => {
const searchRequest = { params: {} };
const options = { sessionId, isStored: false, isRestore: false };
const spyTrackId = jest.spyOn(service, 'trackId').mockResolvedValue();
mockSearch.mockReturnValueOnce(of({ id: 'my_id' }, { id: 'my_id' }));
await service
.search(mockStrategy, searchRequest, options, mockSearchDeps, mockDeps)
.toPromise();
expect(spyTrackId).toBeCalledTimes(1);
expect(spyTrackId).toBeCalledWith(searchRequest, 'my_id', options, {});
spyTrackId.mockRestore();
});
it('does not call `trackId` if restoring', async () => {
const searchRequest = { params: {} };
const options = { sessionId, isStored: true, isRestore: true };
const spyGetId = jest.spyOn(service, 'getId').mockResolvedValueOnce('my_id');
const spyTrackId = jest.spyOn(service, 'trackId').mockResolvedValue();
mockSearch.mockReturnValueOnce(of({ id: 'my_id' }));
await service
.search(mockStrategy, searchRequest, options, mockSearchDeps, mockDeps)
.toPromise();
expect(spyTrackId).not.toBeCalled();
spyGetId.mockRestore();
spyTrackId.mockRestore();
expect(savedObjectsClient.update).toHaveBeenCalledWith(SEARCH_SESSION_TYPE, sessionId, {
status: SearchSessionStatus.CANCELLED,
});
});
@ -309,20 +225,21 @@ describe('SearchSessionService', () => {
get: () => mockIdMapping,
});
await service.trackId(
searchRequest,
searchId,
{ sessionId, isStored, strategy: MOCK_STRATEGY },
{ savedObjectsClient }
);
await service.trackId({ savedObjectsClient }, searchRequest, searchId, {
sessionId,
isStored,
strategy: MOCK_STRATEGY,
});
expect(savedObjectsClient.update).not.toHaveBeenCalled();
await service.save(
sessionId,
{ name, created, expires, appId, urlGeneratorId },
{ savedObjectsClient }
);
await service.save({ savedObjectsClient }, sessionId, {
name,
created,
expires,
appId,
urlGeneratorId,
});
expect(savedObjectsClient.create).toHaveBeenCalledWith(
SEARCH_SESSION_TYPE,
@ -346,30 +263,6 @@ describe('SearchSessionService', () => {
expect(setParams.ids.get(requestHash).strategy).toBe(MOCK_STRATEGY);
expect(setSessionId).toBe(sessionId);
});
it('updates saved object when `isStored` is `true`', async () => {
const searchRequest = { params: {} };
const requestHash = createRequestHash(searchRequest.params);
const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0';
const isStored = true;
await service.trackId(
searchRequest,
searchId,
{ sessionId, isStored, strategy: MOCK_STRATEGY },
{ savedObjectsClient }
);
expect(savedObjectsClient.update).toHaveBeenCalledWith(SEARCH_SESSION_TYPE, sessionId, {
idMapping: {
[requestHash]: {
id: searchId,
strategy: MOCK_STRATEGY,
status: SearchStatus.IN_PROGRESS,
},
},
});
});
});
describe('getId', () => {
@ -377,7 +270,7 @@ describe('SearchSessionService', () => {
const searchRequest = { params: {} };
expect(() =>
service.getId(searchRequest, {}, { savedObjectsClient })
service.getId({ savedObjectsClient }, searchRequest, {})
).rejects.toMatchInlineSnapshot(`[Error: Session ID is required]`);
});
@ -385,7 +278,7 @@ describe('SearchSessionService', () => {
const searchRequest = { params: {} };
expect(() =>
service.getId(searchRequest, { sessionId, isStored: false }, { savedObjectsClient })
service.getId({ savedObjectsClient }, searchRequest, { sessionId, isStored: false })
).rejects.toMatchInlineSnapshot(
`[Error: Cannot get search ID from a session that is not stored]`
);
@ -395,11 +288,11 @@ describe('SearchSessionService', () => {
const searchRequest = { params: {} };
expect(() =>
service.getId(
searchRequest,
{ sessionId, isStored: true, isRestore: false },
{ savedObjectsClient }
)
service.getId({ savedObjectsClient }, searchRequest, {
sessionId,
isStored: true,
isRestore: false,
})
).rejects.toMatchInlineSnapshot(
`[Error: Get search ID is only supported when restoring a session]`
);
@ -427,16 +320,47 @@ describe('SearchSessionService', () => {
};
savedObjectsClient.get.mockResolvedValue(mockSession);
const id = await service.getId(
searchRequest,
{ sessionId, isStored: true, isRestore: true },
{ savedObjectsClient }
);
const id = await service.getId({ savedObjectsClient }, searchRequest, {
sessionId,
isStored: true,
isRestore: true,
});
expect(id).toBe(searchId);
});
});
describe('getSearchIdMapping', () => {
it('retrieves the search IDs and strategies from the saved object', async () => {
const mockSession = {
id: 'd7170a35-7e2c-48d6-8dec-9a056721b489',
type: SEARCH_SESSION_TYPE,
attributes: {
name: 'my_name',
appId: 'my_app_id',
urlGeneratorId: 'my_url_generator_id',
idMapping: {
foo: {
id: 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0',
strategy: MOCK_STRATEGY,
},
},
},
references: [],
};
savedObjectsClient.get.mockResolvedValue(mockSession);
const searchIdMapping = await service.getSearchIdMapping(
{ savedObjectsClient },
mockSession.id
);
expect(searchIdMapping).toMatchInlineSnapshot(`
Map {
"FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0" => "ese",
}
`);
});
});
describe('Monitor', () => {
it('schedules the next iteration', async () => {
const findSpy = jest.fn().mockResolvedValue({ saved_objects: [] });

View file

@ -5,39 +5,33 @@
*/
import moment, { Moment } from 'moment';
import { from, Observable } from 'rxjs';
import { first, switchMap } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { first } from 'rxjs/operators';
import {
CoreSetup,
CoreStart,
KibanaRequest,
SavedObjectsClient,
SavedObjectsClientContract,
Logger,
SavedObject,
CoreSetup,
SavedObjectsBulkUpdateObject,
SavedObjectsClient,
SavedObjectsClientContract,
SavedObjectsFindOptions,
} from '../../../../../../src/core/server';
import {
IKibanaSearchRequest,
IKibanaSearchResponse,
ISearchOptions,
KueryNode,
nodeBuilder,
tapFirst,
} from '../../../../../../src/plugins/data/common';
import {
ISearchStrategy,
ISessionService,
SearchStrategyDependencies,
} from '../../../../../../src/plugins/data/server';
import { ISearchSessionService } from '../../../../../../src/plugins/data/server';
import {
TaskManagerSetupContract,
TaskManagerStartContract,
} from '../../../../task_manager/server';
import {
SearchSessionSavedObjectAttributes,
SearchSessionRequestInfo,
SearchSessionSavedObjectAttributes,
SearchSessionStatus,
} from '../../../common';
import { SEARCH_SESSION_TYPE } from '../../saved_objects';
@ -66,7 +60,11 @@ interface StartDependencies {
type SearchSessionsConfig = ConfigSchema['search']['sessions'];
export class SearchSessionService implements ISessionService {
/**
* @internal
*/
export class SearchSessionService
implements ISearchSessionService<SearchSessionSavedObjectAttributes> {
/**
* Map of sessionId to { [requestHash]: searchId }
* @private
@ -228,33 +226,9 @@ export class SearchSessionService implements ISessionService {
return updateResults.saved_objects;
}
public search<Request extends IKibanaSearchRequest, Response extends IKibanaSearchResponse>(
strategy: ISearchStrategy<Request, Response>,
searchRequest: Request,
options: ISearchOptions,
searchDeps: SearchStrategyDependencies,
deps: SearchSessionDependencies
): Observable<Response> {
// If this is a restored background search session, look up the ID using the provided sessionId
const getSearchRequest = async () =>
!options.isRestore || searchRequest.id
? searchRequest
: {
...searchRequest,
id: await this.getId(searchRequest, options, deps),
};
return from(getSearchRequest()).pipe(
switchMap((request) => strategy.search(request, options, searchDeps)),
tapFirst((response) => {
if (searchRequest.id || !options.sessionId || !response.id || options.isRestore) return;
this.trackId(searchRequest, response.id, options, deps);
})
);
}
// TODO: Generate the `userId` from the realm type/realm name/username
public save = async (
{ savedObjectsClient }: SearchSessionDependencies,
sessionId: string,
{
name,
@ -265,8 +239,7 @@ export class SearchSessionService implements ISessionService {
urlGeneratorId,
initialState = {},
restoreState = {},
}: Partial<SearchSessionSavedObjectAttributes>,
{ savedObjectsClient }: SearchSessionDependencies
}: Partial<SearchSessionSavedObjectAttributes>
) => {
if (!name) throw new Error('Name is required');
if (!appId) throw new Error('AppId is required');
@ -296,7 +269,7 @@ export class SearchSessionService implements ISessionService {
};
// TODO: Throw an error if this session doesn't belong to this user
public get = (sessionId: string, { savedObjectsClient }: SearchSessionDependencies) => {
public get = ({ savedObjectsClient }: SearchSessionDependencies, sessionId: string) => {
this.logger.debug(`get | ${sessionId}`);
return savedObjectsClient.get<SearchSessionSavedObjectAttributes>(
SEARCH_SESSION_TYPE,
@ -306,8 +279,8 @@ export class SearchSessionService implements ISessionService {
// TODO: Throw an error if this session doesn't belong to this user
public find = (
options: Omit<SavedObjectsFindOptions, 'type'>,
{ savedObjectsClient }: SearchSessionDependencies
{ savedObjectsClient }: SearchSessionDependencies,
options: Omit<SavedObjectsFindOptions, 'type'>
) => {
return savedObjectsClient.find<SearchSessionSavedObjectAttributes>({
...options,
@ -317,9 +290,9 @@ export class SearchSessionService implements ISessionService {
// TODO: Throw an error if this session doesn't belong to this user
public update = (
{ savedObjectsClient }: SearchSessionDependencies,
sessionId: string,
attributes: Partial<SearchSessionSavedObjectAttributes>,
{ savedObjectsClient }: SearchSessionDependencies
attributes: Partial<SearchSessionSavedObjectAttributes>
) => {
this.logger.debug(`update | ${sessionId}`);
return savedObjectsClient.update<SearchSessionSavedObjectAttributes>(
@ -329,9 +302,17 @@ export class SearchSessionService implements ISessionService {
);
};
public extend(deps: SearchSessionDependencies, sessionId: string, expires: Date) {
this.logger.debug(`extend | ${sessionId}`);
return this.update(deps, sessionId, { expires: expires.toISOString() });
}
// TODO: Throw an error if this session doesn't belong to this user
public delete = (sessionId: string, { savedObjectsClient }: SearchSessionDependencies) => {
return savedObjectsClient.delete(SEARCH_SESSION_TYPE, sessionId);
public cancel = (deps: SearchSessionDependencies, sessionId: string) => {
return this.update(deps, sessionId, {
status: SearchSessionStatus.CANCELLED,
});
};
/**
@ -340,10 +321,10 @@ export class SearchSessionService implements ISessionService {
* @internal
*/
public trackId = async (
deps: SearchSessionDependencies,
searchRequest: IKibanaSearchRequest,
searchId: string,
{ sessionId, isStored, strategy }: ISearchOptions,
deps: SearchSessionDependencies
{ sessionId, strategy }: ISearchOptions
) => {
if (!sessionId || !searchId) return;
this.logger.debug(`trackId | ${sessionId} | ${searchId}`);
@ -354,33 +335,34 @@ export class SearchSessionService implements ISessionService {
status: SearchStatus.IN_PROGRESS,
};
// If there is already a saved object for this session, update it to include this request/ID.
// Otherwise, just update the in-memory mapping for this session for when the session is saved.
if (isStored) {
const attributes = {
idMapping: { [requestHash]: searchInfo },
};
await this.update(sessionId, attributes, deps);
} else {
const map = this.sessionSearchMap.get(sessionId) ?? {
insertTime: moment(),
retryCount: 0,
ids: new Map<string, SearchSessionRequestInfo>(),
};
map.ids.set(requestHash, searchInfo);
this.sessionSearchMap.set(sessionId, map);
}
// Update the in-memory mapping for this session for when the session is saved.
const map = this.sessionSearchMap.get(sessionId) ?? {
insertTime: moment(),
retryCount: 0,
ids: new Map<string, SearchSessionRequestInfo>(),
};
map.ids.set(requestHash, searchInfo);
this.sessionSearchMap.set(sessionId, map);
};
public async getSearchIdMapping(deps: SearchSessionDependencies, sessionId: string) {
const searchSession = await this.get(deps, sessionId);
const searchIdMapping = new Map<string, string>();
Object.values(searchSession.attributes.idMapping).forEach((requestInfo) => {
searchIdMapping.set(requestInfo.id, requestInfo.strategy);
});
return searchIdMapping;
}
/**
* Look up an existing search ID that matches the given request in the given session so that the
* request can continue rather than restart.
* @internal
*/
public getId = async (
deps: SearchSessionDependencies,
searchRequest: IKibanaSearchRequest,
{ sessionId, isStored, isRestore }: ISearchOptions,
deps: SearchSessionDependencies
{ sessionId, isStored, isRestore }: ISearchOptions
) => {
if (!sessionId) {
throw new Error('Session ID is required');
@ -390,7 +372,7 @@ export class SearchSessionService implements ISessionService {
throw new Error('Get search ID is only supported when restoring a session');
}
const session = await this.get(sessionId, deps);
const session = await this.get(deps, sessionId);
const requestHash = createRequestHash(searchRequest.params);
if (!session.attributes.idMapping.hasOwnProperty(requestHash)) {
throw new Error('No search ID in this session matching the given search request');
@ -406,17 +388,15 @@ export class SearchSessionService implements ISessionService {
});
const deps = { savedObjectsClient };
return {
search: <Request extends IKibanaSearchRequest, Response extends IKibanaSearchResponse>(
strategy: ISearchStrategy<Request, Response>,
...args: Parameters<ISearchStrategy<Request, Response>['search']>
) => this.search(strategy, ...args, deps),
save: (sessionId: string, attributes: Partial<SearchSessionSavedObjectAttributes>) =>
this.save(sessionId, attributes, deps),
get: (sessionId: string) => this.get(sessionId, deps),
find: (options: SavedObjectsFindOptions) => this.find(options, deps),
update: (sessionId: string, attributes: Partial<SearchSessionSavedObjectAttributes>) =>
this.update(sessionId, attributes, deps),
delete: (sessionId: string) => this.delete(sessionId, deps),
getId: this.getId.bind(this, deps),
trackId: this.trackId.bind(this, deps),
getSearchIdMapping: this.getSearchIdMapping.bind(this, deps),
save: this.save.bind(this, deps),
get: this.get.bind(this, deps),
find: this.find.bind(this, deps),
update: this.update.bind(this, deps),
extend: this.extend.bind(this, deps),
cancel: this.cancel.bind(this, deps),
};
};
};

View file

@ -17,6 +17,7 @@ import {
ISearchStrategy,
SearchStrategyDependencies,
} from 'src/plugins/data/server';
import { createSearchSessionsClientMock } from '../../../../../../src/plugins/data/server/search/mocks';
import { InfraSource } from '../../lib/sources';
import { createInfraSourcesMock } from '../../lib/sources/mocks';
import {
@ -307,6 +308,7 @@ const createSearchStrategyDependenciesMock = (): SearchStrategyDependencies => (
uiSettingsClient: uiSettingsServiceMock.createClient(),
esClient: elasticsearchServiceMock.createScopedClusterClient(),
savedObjectsClient: savedObjectsClientMock.create(),
searchSessionsClient: createSearchSessionsClientMock(),
});
// using the official data mock from within x-pack doesn't type-check successfully,

View file

@ -22,6 +22,7 @@ import {
logEntrySearchRequestStateRT,
logEntrySearchStrategyProvider,
} from './log_entry_search_strategy';
import { createSearchSessionsClientMock } from '../../../../../../src/plugins/data/server/search/mocks';
describe('LogEntry search strategy', () => {
it('handles initial search requests', async () => {
@ -244,6 +245,7 @@ const createSearchStrategyDependenciesMock = (): SearchStrategyDependencies => (
uiSettingsClient: uiSettingsServiceMock.createClient(),
esClient: elasticsearchServiceMock.createScopedClusterClient(),
savedObjectsClient: savedObjectsClientMock.create(),
searchSessionsClient: createSearchSessionsClientMock(),
});
// using the official data mock from within x-pack doesn't type-check successfully,

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import type { RequestHandlerContext } from 'src/core/server';
import type { DataApiRequestHandlerContext } from '../../../../src/plugins/data/server';
import type { SearchRequestHandlerContext } from '../../../../src/plugins/data/server';
import { MlPluginSetup } from '../../ml/server';
export type MlSystem = ReturnType<MlPluginSetup['mlSystemProvider']>;
@ -27,5 +27,5 @@ export type InfraRequestHandlerContext = InfraMlRequestHandlerContext &
*/
export interface InfraPluginRequestHandlerContext extends RequestHandlerContext {
infra: InfraRequestHandlerContext;
search: DataApiRequestHandlerContext;
search: SearchRequestHandlerContext;
}

View file

@ -8,8 +8,8 @@
import geojsonvt from 'geojson-vt';
// @ts-expect-error
import vtpbf from 'vt-pbf';
import { Logger, RequestHandlerContext } from 'src/core/server';
import type { DataApiRequestHandlerContext } from 'src/plugins/data/server';
import { Logger } from 'src/core/server';
import type { DataRequestHandlerContext } from 'src/plugins/data/server';
import { Feature, FeatureCollection, Polygon } from 'geojson';
import {
ES_GEO_FIELD_TYPE,
@ -45,7 +45,7 @@ export async function getGridTile({
z: number;
geometryFieldName: string;
index: string;
context: RequestHandlerContext & { search: DataApiRequestHandlerContext };
context: DataRequestHandlerContext;
logger: Logger;
requestBody: any;
requestType: RENDER_AS;
@ -125,7 +125,7 @@ export async function getTile({
z: number;
geometryFieldName: string;
index: string;
context: RequestHandlerContext & { search: DataApiRequestHandlerContext };
context: DataRequestHandlerContext;
logger: Logger;
requestBody: any;
geoFieldType: ES_GEO_FIELD_TYPE;

View file

@ -6,14 +6,9 @@
import rison from 'rison-node';
import { schema } from '@kbn/config-schema';
import {
KibanaRequest,
KibanaResponseFactory,
Logger,
RequestHandlerContext,
} from 'src/core/server';
import { KibanaRequest, KibanaResponseFactory, Logger } from 'src/core/server';
import { IRouter } from 'src/core/server';
import type { DataApiRequestHandlerContext } from 'src/plugins/data/server';
import type { DataRequestHandlerContext } from 'src/plugins/data/server';
import {
MVT_GETTILE_API_PATH,
API_ROOT_PATH,
@ -29,7 +24,7 @@ export function initMVTRoutes({
router,
logger,
}: {
router: IRouter<RequestHandlerContext & { search: DataApiRequestHandlerContext }>;
router: IRouter<DataRequestHandlerContext>;
logger: Logger;
}) {
router.get(
@ -49,7 +44,7 @@ export function initMVTRoutes({
},
},
async (
context: RequestHandlerContext & { search: DataApiRequestHandlerContext },
context: DataRequestHandlerContext,
request: KibanaRequest<unknown, Record<string, any>, unknown>,
response: KibanaResponseFactory
) => {
@ -91,7 +86,7 @@ export function initMVTRoutes({
},
},
async (
context: RequestHandlerContext & { search: DataApiRequestHandlerContext },
context: DataRequestHandlerContext,
request: KibanaRequest<unknown, Record<string, any>, unknown>,
response: KibanaResponseFactory
) => {

View file

@ -6,6 +6,7 @@
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
import { SearchSessionStatus } from '../../../../plugins/data_enhanced/common';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
@ -29,11 +30,11 @@ export default function ({ getService }: FtrProviderContext) {
await supertest.get(`/internal/session/${sessionId}`).set('kbn-xsrf', 'foo').expect(200);
});
it('should fail to delete an unknown session', async () => {
it('should fail to cancel an unknown session', async () => {
await supertest.delete(`/internal/session/123`).set('kbn-xsrf', 'foo').expect(404);
});
it('should create and delete a session', async () => {
it('should create and cancel a session', async () => {
const sessionId = `my-session-${Math.random()}`;
await supertest
.post(`/internal/session`)
@ -49,7 +50,13 @@ export default function ({ getService }: FtrProviderContext) {
await supertest.delete(`/internal/session/${sessionId}`).set('kbn-xsrf', 'foo').expect(200);
await supertest.get(`/internal/session/${sessionId}`).set('kbn-xsrf', 'foo').expect(404);
const resp = await supertest
.get(`/internal/session/${sessionId}`)
.set('kbn-xsrf', 'foo')
.expect(200);
const { status } = resp.body.attributes;
expect(status).to.equal(SearchSessionStatus.CANCELLED);
});
it('should sync search ids into session', async () => {
@ -123,6 +130,39 @@ export default function ({ getService }: FtrProviderContext) {
expect(idMappings).to.contain(id1);
expect(idMappings).to.contain(id2);
});
it('should create and extend a session', async () => {
const sessionId = `my-session-${Math.random()}`;
await supertest
.post(`/internal/session`)
.set('kbn-xsrf', 'foo')
.send({
sessionId,
name: 'My Session',
appId: 'discover',
expires: '123',
urlGeneratorId: 'discover',
})
.expect(200);
await supertest
.post(`/internal/session/${sessionId}/_extend`)
.set('kbn-xsrf', 'foo')
.send({
expires: '2021-02-26T21:02:43.742Z',
})
.expect(200);
});
});
it('should fail to extend a nonexistent session', async () => {
await supertest
.post(`/internal/session/123/_extend`)
.set('kbn-xsrf', 'foo')
.send({
expires: '2021-02-26T21:02:43.742Z',
})
.expect(404);
});
});
}