Skip to content

Commit

Permalink
handling references for kibana_context and get_index_pattern expressi…
Browse files Browse the repository at this point in the history
…on functions (#95224) (#95401)

# Conflicts:
#	src/plugins/data/server/search/search_service.ts
  • Loading branch information
ppisljar authored Mar 25, 2021
1 parent daea8eb commit 8a89d6c
Show file tree
Hide file tree
Showing 15 changed files with 234 additions and 130 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export interface ExecutionContext<InspectorAdapters extends Adapters = Adapters,
| --- | --- | --- |
| [abortSignal](./kibana-plugin-plugins-expressions-public.executioncontext.abortsignal.md) | <code>AbortSignal</code> | Adds ability to abort current execution. |
| [getKibanaRequest](./kibana-plugin-plugins-expressions-public.executioncontext.getkibanarequest.md) | <code>() =&gt; KibanaRequest</code> | Getter to retrieve the <code>KibanaRequest</code> object inside an expression function. Useful for functions which are running on the server and need to perform operations that are scoped to a specific user. |
| [getSavedObject](./kibana-plugin-plugins-expressions-public.executioncontext.getsavedobject.md) | <code>&lt;T extends SavedObjectAttributes = SavedObjectAttributes&gt;(type: string, id: string) =&gt; Promise&lt;SavedObject&lt;T&gt;&gt;</code> | Allows to fetch saved objects from ElasticSearch. In browser <code>getSavedObject</code> function is provided automatically by the Expressions plugin. On the server the caller of the expression has to provide this context function. The reason is because on the browser we always know the user who tries to fetch a saved object, thus saved object client is scoped automatically to that user. However, on the server we can scope that saved object client to any user, or even not scope it at all and execute it as an "internal" user. |
| [getSearchContext](./kibana-plugin-plugins-expressions-public.executioncontext.getsearchcontext.md) | <code>() =&gt; ExecutionContextSearch</code> | Get search context of the expression. |
| [getSearchSessionId](./kibana-plugin-plugins-expressions-public.executioncontext.getsearchsessionid.md) | <code>() =&gt; string &#124; undefined</code> | Search context in which expression should operate. |
| [inspectorAdapters](./kibana-plugin-plugins-expressions-public.executioncontext.inspectoradapters.md) | <code>InspectorAdapters</code> | Adapters for <code>inspector</code> plugin. |
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export interface ExecutionContext<InspectorAdapters extends Adapters = Adapters,
| --- | --- | --- |
| [abortSignal](./kibana-plugin-plugins-expressions-server.executioncontext.abortsignal.md) | <code>AbortSignal</code> | Adds ability to abort current execution. |
| [getKibanaRequest](./kibana-plugin-plugins-expressions-server.executioncontext.getkibanarequest.md) | <code>() =&gt; KibanaRequest</code> | Getter to retrieve the <code>KibanaRequest</code> object inside an expression function. Useful for functions which are running on the server and need to perform operations that are scoped to a specific user. |
| [getSavedObject](./kibana-plugin-plugins-expressions-server.executioncontext.getsavedobject.md) | <code>&lt;T extends SavedObjectAttributes = SavedObjectAttributes&gt;(type: string, id: string) =&gt; Promise&lt;SavedObject&lt;T&gt;&gt;</code> | Allows to fetch saved objects from ElasticSearch. In browser <code>getSavedObject</code> function is provided automatically by the Expressions plugin. On the server the caller of the expression has to provide this context function. The reason is because on the browser we always know the user who tries to fetch a saved object, thus saved object client is scoped automatically to that user. However, on the server we can scope that saved object client to any user, or even not scope it at all and execute it as an "internal" user. |
| [getSearchContext](./kibana-plugin-plugins-expressions-server.executioncontext.getsearchcontext.md) | <code>() =&gt; ExecutionContextSearch</code> | Get search context of the expression. |
| [getSearchSessionId](./kibana-plugin-plugins-expressions-server.executioncontext.getsearchsessionid.md) | <code>() =&gt; string &#124; undefined</code> | Search context in which expression should operate. |
| [inspectorAdapters](./kibana-plugin-plugins-expressions-server.executioncontext.inspectoradapters.md) | <code>InspectorAdapters</code> | Adapters for <code>inspector</code> plugin. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n';
import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
import { IndexPatternsContract } from '../index_patterns';
import { IndexPatternSpec } from '..';
import { SavedObjectReference } from '../../../../../core/types';

const name = 'indexPatternLoad';
const type = 'index_pattern';
Expand Down Expand Up @@ -57,4 +58,29 @@ export const getIndexPatternLoadMeta = (): Omit<
}),
},
},
extract(state) {
const refName = 'indexPatternLoad.id';
const references: SavedObjectReference[] = [
{
name: refName,
type: 'search',
id: state.id[0] as string,
},
];
return {
state: {
...state,
id: [refName],
},
references,
};
},

inject(state, references) {
const reference = references.find((ref) => ref.name === 'indexPatternLoad.id');
if (reference) {
state.id[0] = reference.id;
}
return state;
},
});
167 changes: 104 additions & 63 deletions src/plugins/data/common/search/expressions/kibana_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ import { Query, uniqFilters } from '../../query';
import { ExecutionContextSearch, KibanaContext, KibanaFilter } from './kibana_context_type';
import { KibanaQueryOutput } from './kibana_context_type';
import { KibanaTimerangeOutput } from './timerange';
import { SavedObjectReference } from '../../../../../core/types';
import { SavedObjectsClientCommon } from '../../index_patterns';
import { Filter } from '../../es_query/filters';

/** @internal */
export interface KibanaContextStartDependencies {
savedObjectsClient: SavedObjectsClientCommon;
}

interface Arguments {
q?: KibanaQueryOutput | null;
Expand All @@ -40,75 +48,108 @@ const mergeQueries = (first: Query | Query[] = [], second: Query | Query[]) =>
(n: any) => JSON.stringify(n.query)
);

export const kibanaContextFunction: ExpressionFunctionKibanaContext = {
name: 'kibana_context',
type: 'kibana_context',
inputTypes: ['kibana_context', 'null'],
help: i18n.translate('data.search.functions.kibana_context.help', {
defaultMessage: 'Updates kibana global context',
}),
args: {
q: {
types: ['kibana_query', 'null'],
aliases: ['query', '_'],
default: null,
help: i18n.translate('data.search.functions.kibana_context.q.help', {
defaultMessage: 'Specify Kibana free form text query',
}),
},
filters: {
types: ['kibana_filter', 'null'],
multi: true,
help: i18n.translate('data.search.functions.kibana_context.filters.help', {
defaultMessage: 'Specify Kibana generic filters',
}),
export const getKibanaContextFn = (
getStartDependencies: (
getKibanaRequest: ExecutionContext['getKibanaRequest']
) => Promise<KibanaContextStartDependencies>
) => {
const kibanaContextFunction: ExpressionFunctionKibanaContext = {
name: 'kibana_context',
type: 'kibana_context',
inputTypes: ['kibana_context', 'null'],
help: i18n.translate('data.search.functions.kibana_context.help', {
defaultMessage: 'Updates kibana global context',
}),
args: {
q: {
types: ['kibana_query', 'null'],
aliases: ['query', '_'],
default: null,
help: i18n.translate('data.search.functions.kibana_context.q.help', {
defaultMessage: 'Specify Kibana free form text query',
}),
},
filters: {
types: ['kibana_filter', 'null'],
multi: true,
help: i18n.translate('data.search.functions.kibana_context.filters.help', {
defaultMessage: 'Specify Kibana generic filters',
}),
},
timeRange: {
types: ['timerange', 'null'],
default: null,
help: i18n.translate('data.search.functions.kibana_context.timeRange.help', {
defaultMessage: 'Specify Kibana time range filter',
}),
},
savedSearchId: {
types: ['string', 'null'],
default: null,
help: i18n.translate('data.search.functions.kibana_context.savedSearchId.help', {
defaultMessage: 'Specify saved search ID to be used for queries and filters',
}),
},
},
timeRange: {
types: ['timerange', 'null'],
default: null,
help: i18n.translate('data.search.functions.kibana_context.timeRange.help', {
defaultMessage: 'Specify Kibana time range filter',
}),

extract(state) {
const references: SavedObjectReference[] = [];
if (state.savedSearchId.length && typeof state.savedSearchId[0] === 'string') {
const refName = 'kibana_context.savedSearchId';
references.push({
name: refName,
type: 'search',
id: state.savedSearchId[0] as string,
});
return {
state: {
...state,
savedSearchId: [refName],
},
references,
};
}
return { state, references };
},
savedSearchId: {
types: ['string', 'null'],
default: null,
help: i18n.translate('data.search.functions.kibana_context.savedSearchId.help', {
defaultMessage: 'Specify saved search ID to be used for queries and filters',
}),

inject(state, references) {
const reference = references.find((r) => r.name === 'kibana_context.savedSearchId');
if (reference) {
state.savedSearchId[0] = reference.id;
}
return state;
},
},

async fn(input, args, { getSavedObject }) {
const timeRange = args.timeRange || input?.timeRange;
let queries = mergeQueries(input?.query, args?.q || []);
let filters = [...(input?.filters || []), ...(args?.filters?.map(unboxExpressionValue) || [])];
async fn(input, args, { getKibanaRequest }) {
const { savedObjectsClient } = await getStartDependencies(getKibanaRequest);

if (args.savedSearchId) {
if (typeof getSavedObject !== 'function') {
throw new Error(
'"getSavedObject" function not available in execution context. ' +
'When you execute expression you need to add extra execution context ' +
'as the third argument and provide "getSavedObject" implementation.'
);
}
const obj = await getSavedObject('search', args.savedSearchId);
const search = obj.attributes.kibanaSavedObjectMeta as { searchSourceJSON: string };
const { query, filter } = getParsedValue(search.searchSourceJSON, {});
const timeRange = args.timeRange || input?.timeRange;
let queries = mergeQueries(input?.query, args?.q || []);
let filters = [
...(input?.filters || []),
...((args?.filters?.map(unboxExpressionValue) || []) as Filter[]),
];

if (query) {
queries = mergeQueries(queries, query);
}
if (filter) {
filters = [...filters, ...(Array.isArray(filter) ? filter : [filter])];
if (args.savedSearchId) {
const obj = await savedObjectsClient.get('search', args.savedSearchId);
const search = (obj.attributes as any).kibanaSavedObjectMeta.searchSourceJSON as string;
const { query, filter } = getParsedValue(search, {});

if (query) {
queries = mergeQueries(queries, query);
}
if (filter) {
filters = [...filters, ...(Array.isArray(filter) ? filter : [filter])];
}
}
}

return {
type: 'kibana_context',
query: queries,
filters: uniqFilters(filters).filter((f: any) => !f.meta?.disabled),
timeRange,
};
},
return {
type: 'kibana_context',
query: queries,
filters: uniqFilters(filters).filter((f: any) => !f.meta?.disabled),
timeRange,
};
},
};
return kibanaContextFunction;
};
39 changes: 39 additions & 0 deletions src/plugins/data/public/search/expressions/kibana_context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { StartServicesAccessor } from 'src/core/public';
import { getKibanaContextFn } from '../../../common/search/expressions';
import { DataPublicPluginStart, DataStartDependencies } from '../../types';
import { SavedObjectsClientCommon } from '../../../common/index_patterns';

/**
* This is some glue code that takes in `core.getStartServices`, extracts the dependencies
* needed for this function, and wraps them behind a `getStartDependencies` function that
* is then called at runtime.
*
* We do this so that we can be explicit about exactly which dependencies the function
* requires, without cluttering up the top-level `plugin.ts` with this logic. It also
* makes testing the expression function a bit easier since `getStartDependencies` is
* the only thing you should need to mock.
*
* @param getStartServices - core's StartServicesAccessor for this plugin
*
* @internal
*/
export function getKibanaContext({
getStartServices,
}: {
getStartServices: StartServicesAccessor<DataStartDependencies, DataPublicPluginStart>;
}) {
return getKibanaContextFn(async () => {
const [core] = await getStartServices();
return {
savedObjectsClient: (core.savedObjects.client as unknown) as SavedObjectsClientCommon,
};
});
}
8 changes: 6 additions & 2 deletions src/plugins/data/public/search/search_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import { handleResponse } from './fetch';
import {
kibana,
kibanaContext,
kibanaContextFunction,
ISearchGeneric,
SearchSourceDependencies,
SearchSourceService,
Expand Down Expand Up @@ -52,6 +51,7 @@ import {
import { aggShardDelay } from '../../common/search/aggs/buckets/shard_delay_fn';
import { DataPublicPluginStart, DataStartDependencies } from '../types';
import { NowProviderInternalContract } from '../now_provider';
import { getKibanaContext } from './expressions/kibana_context';

/** @internal */
export interface SearchServiceSetupDependencies {
Expand Down Expand Up @@ -110,7 +110,11 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
})
);
expressions.registerFunction(kibana);
expressions.registerFunction(kibanaContextFunction);
expressions.registerFunction(
getKibanaContext({ getStartServices } as {
getStartServices: StartServicesAccessor<DataStartDependencies, DataPublicPluginStart>;
})
);
expressions.registerFunction(luceneFunction);
expressions.registerFunction(kqlFunction);
expressions.registerFunction(kibanaTimerangeFunction);
Expand Down
46 changes: 46 additions & 0 deletions src/plugins/data/server/search/expressions/kibana_context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { StartServicesAccessor } from 'src/core/server';
import { getKibanaContextFn } from '../../../common/search/expressions';
import { DataPluginStart, DataPluginStartDependencies } from '../../plugin';
import { SavedObjectsClientCommon } from '../../../common/index_patterns';

/**
* This is some glue code that takes in `core.getStartServices`, extracts the dependencies
* needed for this function, and wraps them behind a `getStartDependencies` function that
* is then called at runtime.
*
* We do this so that we can be explicit about exactly which dependencies the function
* requires, without cluttering up the top-level `plugin.ts` with this logic. It also
* makes testing the expression function a bit easier since `getStartDependencies` is
* the only thing you should need to mock.
*
* @param getStartServices - core's StartServicesAccessor for this plugin
*
* @internal
*/
export function getKibanaContext({
getStartServices,
}: {
getStartServices: StartServicesAccessor<DataPluginStartDependencies, DataPluginStart>;
}) {
return getKibanaContextFn(async (getKibanaRequest) => {
const request = getKibanaRequest && getKibanaRequest();
if (!request) {
throw new Error('KIBANA_CONTEXT_KIBANA_REQUEST_MISSING');
}

const [{ savedObjects }] = await getStartServices();
return {
savedObjectsClient: (savedObjects.getScopedClient(
request
) as any) as SavedObjectsClientCommon,
};
});
}
Loading

0 comments on commit 8a89d6c

Please sign in to comment.