[Canvas] Improvements to datasource expressions including SQL parameter support and array leniency (#99549)
* Remove es sql strategy from behind Labs project, remove legacy essql code, remove last spot of legacy elasticsearch client from canvas * clean up test * fix es field test * remove comment Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
b51af01adc
commit
9c2605398f
|
@ -9,11 +9,10 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const LABS_PROJECT_PREFIX = 'labs:';
|
||||
export const USE_DATA_SERVICE = `${LABS_PROJECT_PREFIX}canvas:useDataService` as const;
|
||||
export const TIME_TO_PRESENT = `${LABS_PROJECT_PREFIX}presentation:timeToPresent` as const;
|
||||
export const DEFER_BELOW_FOLD = `${LABS_PROJECT_PREFIX}dashboard:deferBelowFold` as const;
|
||||
|
||||
export const projectIDs = [TIME_TO_PRESENT, USE_DATA_SERVICE, DEFER_BELOW_FOLD] as const;
|
||||
export const projectIDs = [TIME_TO_PRESENT, DEFER_BELOW_FOLD] as const;
|
||||
export const environmentNames = ['kibana', 'browser', 'session'] as const;
|
||||
export const solutionNames = ['canvas', 'dashboard', 'presentation'] as const;
|
||||
|
||||
|
@ -35,22 +34,6 @@ export const projects: { [ID in ProjectID]: ProjectConfig & { id: ID } } = {
|
|||
}),
|
||||
solutions: ['canvas'],
|
||||
},
|
||||
[USE_DATA_SERVICE]: {
|
||||
id: USE_DATA_SERVICE,
|
||||
isActive: true,
|
||||
isDisplayed: true,
|
||||
environments: ['kibana', 'browser', 'session'],
|
||||
name: i18n.translate('presentationUtil.experiments.enableUseDataServiceExperimentName', {
|
||||
defaultMessage: 'Use data service',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'presentationUtil.experiments.enableUseDataServiceExperimentDescription',
|
||||
{
|
||||
defaultMessage: 'An experiment of using the new data.search service for Canvas datasources',
|
||||
}
|
||||
),
|
||||
solutions: ['canvas'],
|
||||
},
|
||||
[DEFER_BELOW_FOLD]: {
|
||||
id: DEFER_BELOW_FOLD,
|
||||
isActive: false,
|
||||
|
|
|
@ -1,79 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
ExpressionFunctionDefinition,
|
||||
ExpressionValueFilter,
|
||||
} from 'src/plugins/expressions/common';
|
||||
// @ts-expect-error untyped local
|
||||
import { buildESRequest } from '../../../common/lib/request/build_es_request';
|
||||
|
||||
import { getFunctionHelp } from '../../../i18n';
|
||||
|
||||
interface Arguments {
|
||||
index: string | null;
|
||||
query: string;
|
||||
}
|
||||
|
||||
export function escount(): ExpressionFunctionDefinition<
|
||||
'escount',
|
||||
ExpressionValueFilter,
|
||||
Arguments,
|
||||
any
|
||||
> {
|
||||
const { help, args: argHelp } = getFunctionHelp().escount;
|
||||
|
||||
return {
|
||||
name: 'escount',
|
||||
type: 'number',
|
||||
context: {
|
||||
types: ['filter'],
|
||||
},
|
||||
help,
|
||||
args: {
|
||||
query: {
|
||||
types: ['string'],
|
||||
aliases: ['_', 'q'],
|
||||
help: argHelp.query,
|
||||
default: '"-_index:.kibana"',
|
||||
},
|
||||
index: {
|
||||
types: ['string'],
|
||||
default: '_all',
|
||||
help: argHelp.index,
|
||||
},
|
||||
},
|
||||
fn: (input, args, handlers) => {
|
||||
input.and = input.and.concat([
|
||||
{
|
||||
type: 'filter',
|
||||
filterType: 'luceneQueryString',
|
||||
query: args.query,
|
||||
and: [],
|
||||
},
|
||||
]);
|
||||
|
||||
const esRequest = buildESRequest(
|
||||
{
|
||||
index: args.index,
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
must: [{ match_all: {} }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
input
|
||||
);
|
||||
|
||||
return ((handlers as any) as { elasticsearchClient: any })
|
||||
.elasticsearchClient('count', esRequest)
|
||||
.then((resp: { count: number }) => resp.count);
|
||||
},
|
||||
};
|
||||
}
|
|
@ -1,114 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import squel from 'safe-squel';
|
||||
import { ExpressionFunctionDefinition } from 'src/plugins/expressions';
|
||||
/* eslint-disable */
|
||||
import { queryEsSQL } from '../../../server/lib/query_es_sql';
|
||||
/* eslint-enable */
|
||||
import { ExpressionValueFilter } from '../../../types';
|
||||
import { getFunctionHelp } from '../../../i18n';
|
||||
|
||||
interface Arguments {
|
||||
index: string;
|
||||
query: string;
|
||||
sort: string;
|
||||
fields: string;
|
||||
metaFields: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export function esdocs(): ExpressionFunctionDefinition<
|
||||
'esdocs',
|
||||
ExpressionValueFilter,
|
||||
Arguments,
|
||||
any
|
||||
> {
|
||||
const { help, args: argHelp } = getFunctionHelp().esdocs;
|
||||
|
||||
return {
|
||||
name: 'esdocs',
|
||||
type: 'datatable',
|
||||
context: {
|
||||
types: ['filter'],
|
||||
},
|
||||
help,
|
||||
args: {
|
||||
query: {
|
||||
types: ['string'],
|
||||
aliases: ['_', 'q'],
|
||||
help: argHelp.query,
|
||||
default: '-_index:.kibana',
|
||||
},
|
||||
count: {
|
||||
types: ['number'],
|
||||
default: 1000,
|
||||
help: argHelp.count,
|
||||
},
|
||||
fields: {
|
||||
help: argHelp.fields,
|
||||
types: ['string'],
|
||||
},
|
||||
index: {
|
||||
types: ['string'],
|
||||
default: '_all',
|
||||
help: argHelp.index,
|
||||
},
|
||||
// TODO: This arg isn't being used in the function.
|
||||
// We need to restore this functionality or remove it as an arg.
|
||||
metaFields: {
|
||||
help: argHelp.metaFields,
|
||||
types: ['string'],
|
||||
},
|
||||
sort: {
|
||||
types: ['string'],
|
||||
help: argHelp.sort,
|
||||
},
|
||||
},
|
||||
fn: (input, args, context) => {
|
||||
const { count, index, fields, sort } = args;
|
||||
|
||||
input.and = input.and.concat([
|
||||
{
|
||||
type: 'filter',
|
||||
filterType: 'luceneQueryString',
|
||||
query: args.query,
|
||||
and: [],
|
||||
},
|
||||
]);
|
||||
|
||||
let query = squel.select({
|
||||
autoQuoteTableNames: true,
|
||||
autoQuoteFieldNames: true,
|
||||
autoQuoteAliasNames: true,
|
||||
nameQuoteCharacter: '"',
|
||||
});
|
||||
|
||||
if (index) {
|
||||
query.from(index);
|
||||
}
|
||||
|
||||
if (fields) {
|
||||
const allFields = fields.split(',').map((field) => field.trim());
|
||||
allFields.forEach((field) => (query = query.field(field)));
|
||||
}
|
||||
|
||||
if (sort) {
|
||||
const [sortField, sortOrder] = sort.split(',').map((str) => str.trim());
|
||||
if (sortField) {
|
||||
query.order(`"${sortField}"`, sortOrder === 'asc');
|
||||
}
|
||||
}
|
||||
|
||||
return queryEsSQL(((context as any) as { elasticsearchClient: any }).elasticsearchClient, {
|
||||
count,
|
||||
query: query.toString(),
|
||||
filter: input.and,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
|
@ -1,61 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
|
||||
/* eslint-disable */
|
||||
import { queryEsSQL } from '../../../server/lib/query_es_sql';
|
||||
/* eslint-enable */
|
||||
import { ExpressionValueFilter } from '../../../types';
|
||||
import { getFunctionHelp } from '../../../i18n';
|
||||
|
||||
interface Arguments {
|
||||
query: string;
|
||||
count: number;
|
||||
timezone: string;
|
||||
}
|
||||
|
||||
export function essql(): ExpressionFunctionDefinition<
|
||||
'essql',
|
||||
ExpressionValueFilter,
|
||||
Arguments,
|
||||
any
|
||||
> {
|
||||
const { help, args: argHelp } = getFunctionHelp().essql;
|
||||
|
||||
return {
|
||||
name: 'essql',
|
||||
type: 'datatable',
|
||||
context: {
|
||||
types: ['filter'],
|
||||
},
|
||||
help,
|
||||
args: {
|
||||
query: {
|
||||
aliases: ['_', 'q'],
|
||||
types: ['string'],
|
||||
help: argHelp.query,
|
||||
},
|
||||
count: {
|
||||
types: ['number'],
|
||||
help: argHelp.count,
|
||||
default: 1000,
|
||||
},
|
||||
timezone: {
|
||||
aliases: ['tz'],
|
||||
types: ['string'],
|
||||
default: 'UTC',
|
||||
help: argHelp.timezone,
|
||||
},
|
||||
},
|
||||
fn: (input, args, context) => {
|
||||
return queryEsSQL(((context as any) as { elasticsearchClient: any }).elasticsearchClient, {
|
||||
...args,
|
||||
filter: input.and,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
|
@ -6,9 +6,6 @@
|
|||
*/
|
||||
|
||||
import { demodata } from './demodata';
|
||||
import { escount } from './escount';
|
||||
import { esdocs } from './esdocs';
|
||||
import { pointseries } from './pointseries';
|
||||
import { essql } from './essql';
|
||||
|
||||
export const functions = [demodata, esdocs, escount, essql, pointseries];
|
||||
export const functions = [demodata, pointseries];
|
||||
|
|
|
@ -24,10 +24,6 @@ export const expressionsServiceFactory: CanvasServiceFactory<ExpressionsService>
|
|||
const loadServerFunctionWrappers = async () => {
|
||||
if (!cached) {
|
||||
cached = (async () => {
|
||||
const labService = startPlugins.presentationUtil.labsService;
|
||||
const hasDataSearch = labService.isProjectEnabled('labs:canvas:useDataService');
|
||||
const dataSearchFns = ['essql', 'esdocs', 'escount'];
|
||||
|
||||
const serverFunctionList = await coreSetup.http.get(API_ROUTE_FUNCTIONS);
|
||||
const batchedFunction = bfetch.batchedFunction({ url: API_ROUTE_FUNCTIONS });
|
||||
const { serialize } = serializeProvider(expressions.getTypes());
|
||||
|
@ -36,13 +32,7 @@ export const expressionsServiceFactory: CanvasServiceFactory<ExpressionsService>
|
|||
// function that matches its definition, but which simply
|
||||
// calls the server-side function endpoint.
|
||||
Object.keys(serverFunctionList).forEach((functionName) => {
|
||||
// Allow function to be overwritten if we want to use
|
||||
// the server-hosted essql, esdocs, and escount functions
|
||||
if (dataSearchFns.includes(functionName)) {
|
||||
if (hasDataSearch && expressions.getFunction(functionName)) {
|
||||
return;
|
||||
}
|
||||
} else if (expressions.getFunction(functionName)) {
|
||||
if (expressions.getFunction(functionName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
165
x-pack/plugins/canvas/server/lib/essql_strategy.test.ts
Normal file
165
x-pack/plugins/canvas/server/lib/essql_strategy.test.ts
Normal file
|
@ -0,0 +1,165 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { essqlSearchStrategyProvider } from './essql_strategy';
|
||||
import { EssqlSearchStrategyRequest } from '../../types';
|
||||
import { zipObject } from 'lodash';
|
||||
|
||||
const getMockEssqlResponse = () => ({
|
||||
body: {
|
||||
columns: [
|
||||
{ name: 'One', type: 'keyword' },
|
||||
{ name: 'Two', type: 'keyword' },
|
||||
],
|
||||
rows: [
|
||||
['foo', 'bar'],
|
||||
['buz', 'baz'],
|
||||
['beep', 'boop'],
|
||||
],
|
||||
cursor: 'cursor-value',
|
||||
},
|
||||
statusCode: 200,
|
||||
});
|
||||
|
||||
const basicReq: EssqlSearchStrategyRequest = {
|
||||
query: 'SELECT * FROM my_index;',
|
||||
count: 3,
|
||||
params: ['my_var'],
|
||||
filter: [
|
||||
{
|
||||
type: 'filter',
|
||||
filterType: 'exactly',
|
||||
value: 'Test Value',
|
||||
column: 'One',
|
||||
and: [],
|
||||
},
|
||||
],
|
||||
timezone: 'UTC',
|
||||
};
|
||||
|
||||
describe('ESSQL search strategy', () => {
|
||||
describe('strategy interface', () => {
|
||||
it('returns a strategy with a `search` function', async () => {
|
||||
const essqlSearch = await essqlSearchStrategyProvider();
|
||||
expect(typeof essqlSearch.search).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('search()', () => {
|
||||
let mockQuery: jest.Mock;
|
||||
let mockClearCursor: jest.Mock;
|
||||
let mockDeps: any;
|
||||
|
||||
beforeEach(() => {
|
||||
mockQuery = jest.fn().mockResolvedValueOnce(getMockEssqlResponse());
|
||||
mockClearCursor = jest.fn();
|
||||
mockDeps = ({
|
||||
esClient: {
|
||||
asCurrentUser: {
|
||||
sql: {
|
||||
query: mockQuery,
|
||||
clearCursor: mockClearCursor,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown) as any;
|
||||
});
|
||||
|
||||
describe('query functionality', () => {
|
||||
it('performs a simple query', async () => {
|
||||
const sqlSearch = await essqlSearchStrategyProvider();
|
||||
const result = await sqlSearch.search(basicReq, {}, mockDeps).toPromise();
|
||||
const [[request]] = mockQuery.mock.calls;
|
||||
|
||||
expect(request.format).toEqual('json');
|
||||
expect(request.body).toEqual(
|
||||
expect.objectContaining({
|
||||
query: basicReq.query,
|
||||
client_id: 'canvas',
|
||||
fetch_size: basicReq.count,
|
||||
time_zone: basicReq.timezone,
|
||||
field_multi_value_leniency: true,
|
||||
params: ['my_var'],
|
||||
})
|
||||
);
|
||||
|
||||
const expectedColumns = getMockEssqlResponse().body.columns.map((c) => ({
|
||||
id: c.name,
|
||||
name: c.name,
|
||||
meta: { type: 'string' },
|
||||
}));
|
||||
const columnNames = expectedColumns.map((c) => c.name);
|
||||
const expectedRows = getMockEssqlResponse().body.rows.map((r) => zipObject(columnNames, r));
|
||||
|
||||
expect(result.columns).toEqual(expectedColumns);
|
||||
expect(result.rows).toEqual(expectedRows);
|
||||
});
|
||||
|
||||
it('iterates over cursor to retrieve for records query', async () => {
|
||||
const pageOne = {
|
||||
body: {
|
||||
columns: [
|
||||
{ name: 'One', type: 'keyword' },
|
||||
{ name: 'Two', type: 'keyword' },
|
||||
],
|
||||
rows: [['foo', 'bar']],
|
||||
cursor: 'cursor-value',
|
||||
},
|
||||
};
|
||||
|
||||
const pageTwo = {
|
||||
body: {
|
||||
rows: [['buz', 'baz']],
|
||||
},
|
||||
};
|
||||
|
||||
mockQuery.mockReset().mockReturnValueOnce(pageOne).mockReturnValueOnce(pageTwo);
|
||||
|
||||
const sqlSearch = await essqlSearchStrategyProvider();
|
||||
const result = await sqlSearch.search({ ...basicReq, count: 2 }, {}, mockDeps).toPromise();
|
||||
|
||||
expect(result.rows).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('closes any cursors that remain open', async () => {
|
||||
const sqlSearch = await essqlSearchStrategyProvider();
|
||||
await sqlSearch.search(basicReq, {}, mockDeps).toPromise();
|
||||
|
||||
const [[cursorReq]] = mockClearCursor.mock.calls;
|
||||
expect(cursorReq.body.cursor).toEqual('cursor-value');
|
||||
});
|
||||
|
||||
it('emits an error if the client throws', async () => {
|
||||
const req: EssqlSearchStrategyRequest = {
|
||||
query: 'SELECT * FROM my_index;',
|
||||
count: 1,
|
||||
params: [],
|
||||
filter: [
|
||||
{
|
||||
type: 'filter',
|
||||
filterType: 'exactly',
|
||||
value: 'Test Value',
|
||||
column: 'category.keyword',
|
||||
and: [],
|
||||
},
|
||||
],
|
||||
timezone: 'UTC',
|
||||
};
|
||||
|
||||
expect.assertions(1);
|
||||
mockQuery.mockReset().mockRejectedValueOnce(new Error('client error'));
|
||||
const eqlSearch = await essqlSearchStrategyProvider();
|
||||
eqlSearch.search(req, {}, mockDeps).subscribe(
|
||||
() => {},
|
||||
(err) => {
|
||||
expect(err).toEqual(new Error('client error'));
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -8,7 +8,7 @@
|
|||
import { from } from 'rxjs';
|
||||
import { map, zipObject } from 'lodash';
|
||||
|
||||
import { ISearchStrategy, PluginStart } from 'src/plugins/data/server';
|
||||
import { ISearchStrategy } from 'src/plugins/data/server';
|
||||
|
||||
import { getKbnServerError } from '../../../../../src/plugins/kibana_utils/server';
|
||||
import { EssqlSearchStrategyRequest, EssqlSearchStrategyResponse } from '../../types';
|
||||
|
@ -17,9 +17,10 @@ import { buildBoolArray } from '../../common/lib/request/build_bool_array';
|
|||
import { sanitizeName } from '../../common/lib/request/sanitize_name';
|
||||
import { normalizeType } from '../../common/lib/request/normalize_type';
|
||||
|
||||
export const essqlSearchStrategyProvider = (
|
||||
data: PluginStart
|
||||
): ISearchStrategy<EssqlSearchStrategyRequest, EssqlSearchStrategyResponse> => {
|
||||
export const essqlSearchStrategyProvider = (): ISearchStrategy<
|
||||
EssqlSearchStrategyRequest,
|
||||
EssqlSearchStrategyResponse
|
||||
> => {
|
||||
return {
|
||||
search: (request, options, { esClient }) => {
|
||||
const { count, query, filter, timezone, params } = request;
|
||||
|
|
|
@ -1,111 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { zipObject } from 'lodash';
|
||||
import { queryEsSQL } from './query_es_sql';
|
||||
// @ts-expect-error
|
||||
import { buildBoolArray } from './build_bool_array';
|
||||
|
||||
const response = {
|
||||
columns: [
|
||||
{ name: 'One', type: 'keyword' },
|
||||
{ name: 'Two', type: 'keyword' },
|
||||
],
|
||||
rows: [
|
||||
['foo', 'bar'],
|
||||
['buz', 'baz'],
|
||||
],
|
||||
cursor: 'cursor-value',
|
||||
};
|
||||
|
||||
const baseArgs = {
|
||||
count: 1,
|
||||
query: 'query',
|
||||
filter: [],
|
||||
timezone: 'timezone',
|
||||
};
|
||||
|
||||
const getApi = (resp = response) => {
|
||||
const api = jest.fn();
|
||||
api.mockResolvedValue(resp);
|
||||
return api;
|
||||
};
|
||||
|
||||
describe('query_es_sql', () => {
|
||||
it('should call the api with the given args', async () => {
|
||||
const api = getApi();
|
||||
|
||||
queryEsSQL(api, baseArgs);
|
||||
|
||||
expect(api).toHaveBeenCalled();
|
||||
const givenArgs = api.mock.calls[0][1];
|
||||
|
||||
expect(givenArgs.body.fetch_size).toBe(baseArgs.count);
|
||||
expect(givenArgs.body.query).toBe(baseArgs.query);
|
||||
expect(givenArgs.body.time_zone).toBe(baseArgs.timezone);
|
||||
});
|
||||
|
||||
it('formats the response', async () => {
|
||||
const api = getApi();
|
||||
|
||||
const result = await queryEsSQL(api, baseArgs);
|
||||
|
||||
const expectedColumns = response.columns.map((c) => ({
|
||||
id: c.name,
|
||||
name: c.name,
|
||||
meta: { type: 'string' },
|
||||
}));
|
||||
const columnNames = expectedColumns.map((c) => c.name);
|
||||
const expectedRows = response.rows.map((r) => zipObject(columnNames, r));
|
||||
|
||||
expect(result.type).toBe('datatable');
|
||||
expect(result.columns).toEqual(expectedColumns);
|
||||
expect(result.rows).toEqual(expectedRows);
|
||||
});
|
||||
|
||||
it('fetches pages until it has the requested count', async () => {
|
||||
const pageOne = {
|
||||
columns: [
|
||||
{ name: 'One', type: 'keyword' },
|
||||
{ name: 'Two', type: 'keyword' },
|
||||
],
|
||||
rows: [['foo', 'bar']],
|
||||
cursor: 'cursor-value',
|
||||
};
|
||||
|
||||
const pageTwo = {
|
||||
rows: [['buz', 'baz']],
|
||||
};
|
||||
|
||||
const api = getApi(pageOne);
|
||||
api.mockReturnValueOnce(pageOne).mockReturnValueOnce(pageTwo);
|
||||
|
||||
const result = await queryEsSQL(api, { ...baseArgs, count: 2 });
|
||||
expect(result.rows).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('closes any cursors that remain open', async () => {
|
||||
const api = getApi();
|
||||
|
||||
await queryEsSQL(api, baseArgs);
|
||||
expect(api.mock.calls[1][1].body.cursor).toBe(response.cursor);
|
||||
});
|
||||
|
||||
it('throws on errors', async () => {
|
||||
const api = getApi();
|
||||
api.mockRejectedValueOnce(new Error('parsing_exception'));
|
||||
api.mockRejectedValueOnce(new Error('generic es error'));
|
||||
|
||||
expect(queryEsSQL(api, baseArgs)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Couldn't parse Elasticsearch SQL query. You may need to add double quotes to names containing special characters. Check your query and try again. Error: parsing_exception"`
|
||||
);
|
||||
|
||||
expect(queryEsSQL(api, baseArgs)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unexpected error from Elasticsearch: generic es error"`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,105 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { map, zipObject } from 'lodash';
|
||||
import { buildBoolArray } from '../../common/lib/request/build_bool_array';
|
||||
import { sanitizeName } from '../../common/lib/request/sanitize_name';
|
||||
import { normalizeType } from '../../common/lib/request/normalize_type';
|
||||
import { LegacyAPICaller } from '../../../../../src/core/server';
|
||||
import { ExpressionValueFilter } from '../../types';
|
||||
|
||||
interface Args {
|
||||
count: number;
|
||||
query: string;
|
||||
timezone?: string;
|
||||
filter: ExpressionValueFilter[];
|
||||
}
|
||||
|
||||
interface CursorResponse {
|
||||
cursor?: string;
|
||||
rows: string[][];
|
||||
}
|
||||
|
||||
type QueryResponse = CursorResponse & {
|
||||
columns: Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
}>;
|
||||
cursor?: string;
|
||||
rows: string[][];
|
||||
};
|
||||
|
||||
export const queryEsSQL = async (
|
||||
elasticsearchClient: LegacyAPICaller,
|
||||
{ count, query, filter, timezone }: Args
|
||||
) => {
|
||||
try {
|
||||
let response: QueryResponse = await elasticsearchClient<QueryResponse>('transport.request', {
|
||||
path: '/_sql?format=json',
|
||||
method: 'POST',
|
||||
body: {
|
||||
query,
|
||||
time_zone: timezone,
|
||||
fetch_size: count,
|
||||
client_id: 'canvas',
|
||||
filter: {
|
||||
bool: {
|
||||
must: [{ match_all: {} }, ...buildBoolArray(filter)],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const columns = response.columns.map(({ name, type }) => {
|
||||
return {
|
||||
id: sanitizeName(name),
|
||||
name: sanitizeName(name),
|
||||
meta: { type: normalizeType(type) },
|
||||
};
|
||||
});
|
||||
const columnNames = map(columns, 'name');
|
||||
let rows = response.rows.map((row) => zipObject(columnNames, row));
|
||||
|
||||
while (rows.length < count && response.cursor !== undefined) {
|
||||
response = await elasticsearchClient<QueryResponse>('transport.request', {
|
||||
path: '/_sql?format=json',
|
||||
method: 'POST',
|
||||
body: {
|
||||
cursor: response.cursor,
|
||||
},
|
||||
});
|
||||
|
||||
rows = [...rows, ...response.rows.map((row) => zipObject(columnNames, row))];
|
||||
}
|
||||
|
||||
if (response.cursor !== undefined) {
|
||||
elasticsearchClient('transport.request', {
|
||||
path: '/_sql/close',
|
||||
method: 'POST',
|
||||
body: {
|
||||
cursor: response.cursor,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'datatable',
|
||||
meta: {
|
||||
type: 'essql',
|
||||
},
|
||||
columns,
|
||||
rows,
|
||||
};
|
||||
} catch (e) {
|
||||
if (e.message.indexOf('parsing_exception') > -1) {
|
||||
throw new Error(
|
||||
`Couldn't parse Elasticsearch SQL query. You may need to add double quotes to names containing special characters. Check your query and try again. Error: ${e.message}`
|
||||
);
|
||||
}
|
||||
throw new Error(`Unexpected error from Elasticsearch: ${e.message}`);
|
||||
}
|
||||
};
|
|
@ -61,7 +61,6 @@ export class CanvasPlugin implements Plugin {
|
|||
router: canvasRouter,
|
||||
expressions: plugins.expressions,
|
||||
bfetch: plugins.bfetch,
|
||||
elasticsearch: coreSetup.elasticsearch,
|
||||
logger: this.logger,
|
||||
});
|
||||
|
||||
|
@ -77,7 +76,7 @@ export class CanvasPlugin implements Plugin {
|
|||
setupInterpreter(plugins.expressions);
|
||||
|
||||
coreSetup.getStartServices().then(([_, depsStart]) => {
|
||||
const strategy = essqlSearchStrategyProvider(depsStart.data);
|
||||
const strategy = essqlSearchStrategyProvider();
|
||||
plugins.data.search.registerSearchStrategy(ESSQL_SEARCH_STRATEGY, strategy);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import { getMockedRouterDeps } from '../test_helpers';
|
|||
const mockRouteContext = ({
|
||||
core: {
|
||||
elasticsearch: {
|
||||
legacy: { client: elasticsearchServiceMock.createLegacyScopedClusterClient() },
|
||||
client: elasticsearchServiceMock.createScopedClusterClient(),
|
||||
},
|
||||
},
|
||||
} as unknown) as RequestHandlerContext;
|
||||
|
@ -33,27 +33,29 @@ describe('Retrieve ES Fields', () => {
|
|||
it(`returns 200 with fields from existing index/index pattern`, async () => {
|
||||
const index = 'test';
|
||||
const mockResults = {
|
||||
indices: ['test'],
|
||||
fields: {
|
||||
'@timestamp': {
|
||||
date: {
|
||||
type: 'date',
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
body: {
|
||||
indices: ['test'],
|
||||
fields: {
|
||||
'@timestamp': {
|
||||
date: {
|
||||
type: 'date',
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
name: {
|
||||
text: {
|
||||
type: 'text',
|
||||
searchable: true,
|
||||
aggregatable: false,
|
||||
name: {
|
||||
text: {
|
||||
type: 'text',
|
||||
searchable: true,
|
||||
aggregatable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
products: {
|
||||
object: {
|
||||
type: 'object',
|
||||
searchable: false,
|
||||
aggregatable: false,
|
||||
products: {
|
||||
object: {
|
||||
type: 'object',
|
||||
searchable: false,
|
||||
aggregatable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -66,10 +68,10 @@ describe('Retrieve ES Fields', () => {
|
|||
},
|
||||
});
|
||||
|
||||
const callAsCurrentUserMock = mockRouteContext.core.elasticsearch.legacy.client
|
||||
.callAsCurrentUser as jest.Mock;
|
||||
const fieldCapsMock = mockRouteContext.core.elasticsearch.client.asCurrentUser
|
||||
.fieldCaps as jest.Mock;
|
||||
|
||||
callAsCurrentUserMock.mockResolvedValueOnce(mockResults);
|
||||
fieldCapsMock.mockResolvedValueOnce(mockResults);
|
||||
|
||||
const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory);
|
||||
|
||||
|
@ -85,7 +87,7 @@ describe('Retrieve ES Fields', () => {
|
|||
|
||||
it(`returns 200 with empty object when index/index pattern has no fields`, async () => {
|
||||
const index = 'test';
|
||||
const mockResults = { indices: [index], fields: {} };
|
||||
const mockResults = { body: { indices: [index], fields: {} } };
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
method: 'get',
|
||||
path,
|
||||
|
@ -94,10 +96,10 @@ describe('Retrieve ES Fields', () => {
|
|||
},
|
||||
});
|
||||
|
||||
const callAsCurrentUserMock = mockRouteContext.core.elasticsearch.legacy.client
|
||||
.callAsCurrentUser as jest.Mock;
|
||||
const fieldCapsMock = mockRouteContext.core.elasticsearch.client.asCurrentUser
|
||||
.fieldCaps as jest.Mock;
|
||||
|
||||
callAsCurrentUserMock.mockResolvedValueOnce(mockResults);
|
||||
fieldCapsMock.mockResolvedValueOnce(mockResults);
|
||||
|
||||
const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory);
|
||||
|
||||
|
@ -109,8 +111,10 @@ describe('Retrieve ES Fields', () => {
|
|||
const index = 'test';
|
||||
|
||||
const mockResults = {
|
||||
indices: [index],
|
||||
fields: {},
|
||||
body: {
|
||||
indices: [index],
|
||||
fields: {},
|
||||
},
|
||||
};
|
||||
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
|
@ -122,10 +126,10 @@ describe('Retrieve ES Fields', () => {
|
|||
},
|
||||
});
|
||||
|
||||
const callAsCurrentUserMock = mockRouteContext.core.elasticsearch.legacy.client
|
||||
.callAsCurrentUser as jest.Mock;
|
||||
const fieldCapsMock = mockRouteContext.core.elasticsearch.client.asCurrentUser
|
||||
.fieldCaps as jest.Mock;
|
||||
|
||||
callAsCurrentUserMock.mockResolvedValueOnce(mockResults);
|
||||
fieldCapsMock.mockResolvedValueOnce(mockResults);
|
||||
|
||||
const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory);
|
||||
|
||||
|
@ -142,10 +146,10 @@ describe('Retrieve ES Fields', () => {
|
|||
},
|
||||
});
|
||||
|
||||
const callAsCurrentUserMock = mockRouteContext.core.elasticsearch.legacy.client
|
||||
.callAsCurrentUser as jest.Mock;
|
||||
const fieldCapsMock = mockRouteContext.core.elasticsearch.client.asCurrentUser
|
||||
.fieldCaps as jest.Mock;
|
||||
|
||||
callAsCurrentUserMock.mockRejectedValueOnce(new Error('Index not found'));
|
||||
fieldCapsMock.mockRejectedValueOnce(new Error('Index not found'));
|
||||
|
||||
await expect(
|
||||
routeHandler(mockRouteContext, request, kibanaResponseFactory)
|
||||
|
|
|
@ -28,7 +28,7 @@ export function initializeESFieldsRoute(deps: RouteInitializerDeps) {
|
|||
},
|
||||
},
|
||||
catchErrorHandler(async (context, request, response) => {
|
||||
const { callAsCurrentUser } = context.core.elasticsearch.legacy.client;
|
||||
const client = context.core.elasticsearch.client.asCurrentUser;
|
||||
const { index, fields } = request.query;
|
||||
|
||||
const config = {
|
||||
|
@ -36,8 +36,8 @@ export function initializeESFieldsRoute(deps: RouteInitializerDeps) {
|
|||
fields: fields || '*',
|
||||
};
|
||||
|
||||
const esFields = await callAsCurrentUser('fieldCaps', config).then((resp) => {
|
||||
return mapValues(resp.fields, (types) => {
|
||||
const esFields = await client.fieldCaps(config).then((resp) => {
|
||||
return mapValues(resp.body.fields, (types) => {
|
||||
if (keys(types).length > 1) {
|
||||
return 'conflict';
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { LegacyAPICaller } from 'src/core/server';
|
||||
import { serializeProvider } from '../../../../../../src/plugins/expressions/common';
|
||||
import { RouteInitializerDeps } from '../';
|
||||
import { API_ROUTE_FUNCTIONS } from '../../../common/lib/constants';
|
||||
|
@ -34,12 +33,9 @@ export function initializeGetFunctionsRoute(deps: RouteInitializerDeps) {
|
|||
}
|
||||
|
||||
export function initializeBatchFunctionsRoute(deps: RouteInitializerDeps) {
|
||||
const { bfetch, elasticsearch, expressions } = deps;
|
||||
const { bfetch, expressions } = deps;
|
||||
|
||||
async function runFunction(
|
||||
handlers: { environment: string; elasticsearchClient: LegacyAPICaller },
|
||||
fnCall: FunctionCall
|
||||
) {
|
||||
async function runFunction(handlers: { environment: string }, fnCall: FunctionCall) {
|
||||
const { functionName, args, context } = fnCall;
|
||||
const { deserialize } = serializeProvider(expressions.getTypes());
|
||||
|
||||
|
@ -61,7 +57,6 @@ export function initializeBatchFunctionsRoute(deps: RouteInitializerDeps) {
|
|||
onBatchItem: async (fnCall: FunctionCall) => {
|
||||
const handlers = {
|
||||
environment: 'server',
|
||||
elasticsearchClient: elasticsearch.legacy.client.asScoped(request).callAsCurrentUser,
|
||||
};
|
||||
const result = await runFunction(handlers, fnCall);
|
||||
if (typeof result === 'undefined') {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { IRouter, Logger, ElasticsearchServiceSetup } from 'src/core/server';
|
||||
import { IRouter, Logger } from 'src/core/server';
|
||||
import { ExpressionsServerSetup } from 'src/plugins/expressions/server';
|
||||
import { BfetchServerSetup } from 'src/plugins/bfetch/server';
|
||||
import { initCustomElementsRoutes } from './custom_elements';
|
||||
|
@ -20,7 +20,6 @@ export interface RouteInitializerDeps {
|
|||
logger: Logger;
|
||||
expressions: ExpressionsServerSetup;
|
||||
bfetch: BfetchServerSetup;
|
||||
elasticsearch: ElasticsearchServiceSetup;
|
||||
}
|
||||
|
||||
export function initRoutes(deps: RouteInitializerDeps) {
|
||||
|
|
Loading…
Reference in a new issue