diff --git a/x-pack/plugins/reporting/export_types/csv_from_savedobject/index.d.ts b/x-pack/plugins/reporting/export_types/csv_from_savedobject/index.d.ts index ddd46e4031b21..3f35f7e237f5b 100644 --- a/x-pack/plugins/reporting/export_types/csv_from_savedobject/index.d.ts +++ b/x-pack/plugins/reporting/export_types/csv_from_savedobject/index.d.ts @@ -29,7 +29,7 @@ export interface SavedSearchObjectAttributesJSON { export interface SavedSearchObjectAttributes { title: string; sort: any[]; - columns: string[]; + columns?: string[]; kibanaSavedObjectMeta: SavedObjectMeta; uiState: any; } @@ -93,8 +93,8 @@ export interface IndexPatternSavedObject { export interface TimeRangeParams { timezone: string; - min: Date; - max: Date; + min: Date | string | number; + max: Date | string | number; } export interface VisPanel { @@ -110,8 +110,12 @@ export interface SearchPanel { timerange: TimeRangeParams; } +export interface SearchSourceQuery { + isSearchSourceQuery: boolean; +} + export interface SearchSource { - query: any; + query: SearchSourceQuery; filter: any[]; } diff --git a/x-pack/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts b/x-pack/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts index 7f3d7c11748b0..a17aa8d46b65b 100644 --- a/x-pack/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts +++ b/x-pack/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts @@ -4,12 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore +// @ts-ignore no module definition import { buildEsQuery } from '@kbn/es-query'; import { Request } from 'hapi'; -import moment from 'moment'; import { KbnServer, Logger } from '../../../../types'; -// @ts-ignore +// @ts-ignore no module definition import { createGenerateCsv } from '../../../csv/server/lib/generate_csv'; import { IndexPatternSavedObject, @@ -17,40 +16,18 @@ import { SearchPanel, SearchRequest, SearchSource, - TimeRangeParams, + SearchSourceQuery, } from '../../'; +import { + CsvResultFromSearch, + ESQueryConfig, + GenerateCsvParams, + Filter, + ReqPayload, + IndexPatternField, +} from './'; import { getDataSource } from './get_data_source'; - -interface SavedSearchGeneratorResult { - content: string; - maxSizeReached: boolean; - size: number; -} - -interface CsvResultFromSearch { - type: string; - result: SavedSearchGeneratorResult | null; -} - -type EndpointCaller = (method: string, params: any) => Promise; -type FormatsMap = Map; // this is required for generateCsv, but formatting is not done for this API - -interface GenerateCsvParams { - searchRequest: SearchRequest; - callEndpoint: EndpointCaller; - fields: string[]; - formatsMap: FormatsMap; - metaFields: string[]; // FIXME not sure what this is for - conflictedTypesFields: string[]; // FIXME not sure what this is for - cancellationToken: any; - settings: { - separator: string; - quoteValues: boolean; - timezone: string | null; - maxSizeBytes: number; - scroll: { duration: string; size: number }; - }; -} +import { getFilters } from './get_filters'; const getEsQueryConfig = async (config: any) => { const configs = await Promise.all([ @@ -68,47 +45,6 @@ const getUiSettings = async (config: any) => { return { separator, quoteValues }; }; -const getTimebasedParts = ( - indexPatternSavedObject: IndexPatternSavedObject, - timerange: TimeRangeParams, - savedSearchObjectAttr: SavedSearchObjectAttributes, - searchSourceFilter: any[] -) => { - let includes: string[]; - let timeFilter: any | null; - let timezone: string | null; - - const { timeFieldName: indexPatternTimeField } = indexPatternSavedObject; - if (indexPatternTimeField) { - includes = [indexPatternTimeField, ...savedSearchObjectAttr.columns]; - timeFilter = { - range: { - [indexPatternTimeField]: { - format: 'epoch_millis', - gte: moment(timerange.min).valueOf(), - lte: moment(timerange.max).valueOf(), - }, - }, - }; - timezone = timerange.timezone; - } else { - includes = savedSearchObjectAttr.columns; - timeFilter = null; - timezone = null; - } - - const combinedFilter = timeFilter ? [timeFilter].concat(searchSourceFilter) : searchSourceFilter; - - return { combinedFilter, includes, timezone }; -}; - -interface IndexPatternField { - scripted: boolean; - lang?: string; - script?: string; - name: string; -} - export async function generateCsvSearch( req: Request, server: KbnServer, @@ -124,21 +60,40 @@ export async function generateCsvSearch( indexPatternSavedObjectId ); const uiConfig = uiSettingsServiceFactory({ savedObjectsClient }); + const esQueryConfig = await getEsQueryConfig(uiConfig); - const kibanaSavedObjectMeta = savedSearchObjectAttr.kibanaSavedObjectMeta; - const { searchSource } = kibanaSavedObjectMeta as { searchSource: SearchSource }; + const { + kibanaSavedObjectMeta: { + searchSource: { + filter: [searchSourceFilter], + query: searchSourceQuery, + }, + }, + } = savedSearchObjectAttr as { kibanaSavedObjectMeta: { searchSource: SearchSource } }; - const { filter: searchSourceFilter, query: searchSourceQuery } = searchSource; - const { combinedFilter, includes, timezone } = getTimebasedParts( - indexPatternSavedObject, + const { + timeFieldName: indexPatternTimeField, + title: esIndex, + fields: indexPatternFields, + } = indexPatternSavedObject; + + const { + state: { query: payloadQuery, sort: payloadSort = [] }, + } = req.payload as ReqPayload; + + const { includes, timezone, combinedFilter } = getFilters( + indexPatternSavedObjectId, + indexPatternTimeField, timerange, savedSearchObjectAttr, - searchSourceFilter + searchSourceFilter, + payloadQuery ); - const esQueryConfig = await getEsQueryConfig(uiConfig); - const [sortField, sortOrder] = savedSearchObjectAttr.sort; - const scriptFields = indexPatternSavedObject.fields + const [savedSortField, savedSortOrder] = savedSearchObjectAttr.sort; + const sortConfig = [...payloadSort, { [savedSortField]: { order: savedSortOrder } }]; + + const scriptFieldsConfig = indexPatternFields .filter((f: IndexPatternField) => f.scripted) .reduce((accum: any, curr: IndexPatternField) => { return { @@ -151,21 +106,24 @@ export async function generateCsvSearch( }, }; }, {}); - const { timeFieldName: indexPatternTimeField } = indexPatternSavedObject; const docValueFields = indexPatternTimeField ? [indexPatternTimeField] : undefined; + + // this array helps ensure the params are passed to buildEsQuery (non-Typescript) in the right order + const buildCsvParams: [IndexPatternSavedObject, SearchSourceQuery, Filter[], ESQueryConfig] = [ + indexPatternSavedObject, + searchSourceQuery, + combinedFilter, + esQueryConfig, + ]; + const searchRequest: SearchRequest = { - index: indexPatternSavedObject.title, + index: esIndex, body: { _source: { includes }, docvalue_fields: docValueFields, - query: buildEsQuery( - indexPatternSavedObject, - searchSourceQuery, - combinedFilter, - esQueryConfig - ), - script_fields: scriptFields, - sort: [{ [sortField]: { order: sortOrder } }], + query: buildEsQuery(...buildCsvParams), + script_fields: scriptFieldsConfig, + sort: sortConfig, }, }; diff --git a/x-pack/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_filters.test.ts b/x-pack/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_filters.test.ts new file mode 100644 index 0000000000000..e87cfc212d404 --- /dev/null +++ b/x-pack/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_filters.test.ts @@ -0,0 +1,207 @@ +/* + * 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 { SavedSearchObjectAttributes, TimeRangeParams } from '../../'; +import { QueryFilter, SearchSourceFilter } from './'; +import { getFilters } from './get_filters'; + +interface Args { + indexPatternId: string; + indexPatternTimeField: string | null; + timerange: TimeRangeParams | null; + savedSearchObjectAttr: SavedSearchObjectAttributes; + searchSourceFilter: SearchSourceFilter; + queryFilter: QueryFilter; +} + +describe('CSV from Saved Object: get_filters', () => { + let args: Args; + beforeEach(() => { + args = { + indexPatternId: 'logs-test-*', + indexPatternTimeField: 'testtimestamp', + timerange: { + timezone: 'UTC', + min: '1901-01-01T00:00:00.000Z', + max: '1902-01-01T00:00:00.000Z', + }, + savedSearchObjectAttr: { + title: 'test', + sort: [{ sortField: { order: 'asc' } }], + kibanaSavedObjectMeta: { + searchSource: { + query: { isSearchSourceQuery: true }, + filter: ['hello searchSource filter 1'], + }, + }, + columns: ['larry'], + uiState: null, + }, + searchSourceFilter: { isSearchSourceFilter: true, isFilter: true }, + queryFilter: { isQueryFilter: true, isFilter: true }, + }; + }); + + describe('search', () => { + it('for timebased search', () => { + const filters = getFilters( + args.indexPatternId, + args.indexPatternTimeField, + args.timerange, + args.savedSearchObjectAttr, + args.searchSourceFilter, + args.queryFilter + ); + + expect(filters).toEqual({ + combinedFilter: [ + { + range: { + testtimestamp: { + format: 'strict_date_time', + gte: '1901-01-01T00:00:00Z', + lte: '1902-01-01T00:00:00Z', + }, + }, + }, + { isFilter: true, isSearchSourceFilter: true }, + { isFilter: true, isQueryFilter: true }, + ], + includes: ['testtimestamp', 'larry'], + timezone: 'UTC', + }); + }); + + it('for non-timebased search', () => { + args.indexPatternTimeField = null; + args.timerange = null; + + const filters = getFilters( + args.indexPatternId, + args.indexPatternTimeField, + args.timerange, + args.savedSearchObjectAttr, + args.searchSourceFilter, + args.queryFilter + ); + + expect(filters).toEqual({ + combinedFilter: [ + { isFilter: true, isSearchSourceFilter: true }, + { isFilter: true, isQueryFilter: true }, + ], + includes: ['larry'], + timezone: null, + }); + }); + }); + + describe('errors', () => { + it('throw if timebased and timerange is missing', () => { + args.timerange = null; + + const throwFn = () => + getFilters( + args.indexPatternId, + args.indexPatternTimeField, + args.timerange, + args.savedSearchObjectAttr, + args.searchSourceFilter, + args.queryFilter + ); + + expect(throwFn).toThrow( + 'Time range params are required for index pattern [logs-test-*], using time field [testtimestamp]' + ); + }); + }); + + it('composes the defined filters', () => { + expect( + getFilters( + args.indexPatternId, + args.indexPatternTimeField, + args.timerange, + args.savedSearchObjectAttr, + undefined, + undefined + ) + ).toEqual({ + combinedFilter: [ + { + range: { + testtimestamp: { + format: 'strict_date_time', + gte: '1901-01-01T00:00:00Z', + lte: '1902-01-01T00:00:00Z', + }, + }, + }, + ], + includes: ['testtimestamp', 'larry'], + timezone: 'UTC', + }); + + expect( + getFilters( + args.indexPatternId, + args.indexPatternTimeField, + args.timerange, + args.savedSearchObjectAttr, + undefined, + args.queryFilter + ) + ).toEqual({ + combinedFilter: [ + { + range: { + testtimestamp: { + format: 'strict_date_time', + gte: '1901-01-01T00:00:00Z', + lte: '1902-01-01T00:00:00Z', + }, + }, + }, + { isFilter: true, isQueryFilter: true }, + ], + includes: ['testtimestamp', 'larry'], + timezone: 'UTC', + }); + }); + + describe('timefilter', () => { + it('formats the datetime to the provided timezone', () => { + args.timerange = { + timezone: 'MST', + min: '1901-01-01T00:00:00Z', + max: '1902-01-01T00:00:00Z', + }; + + expect( + getFilters( + args.indexPatternId, + args.indexPatternTimeField, + args.timerange, + args.savedSearchObjectAttr + ) + ).toEqual({ + combinedFilter: [ + { + range: { + testtimestamp: { + format: 'strict_date_time', + gte: '1900-12-31T17:00:00-07:00', + lte: '1901-12-31T17:00:00-07:00', + }, + }, + }, + ], + includes: ['testtimestamp', 'larry'], + timezone: 'MST', + }); + }); + }); +}); diff --git a/x-pack/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_filters.ts b/x-pack/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_filters.ts new file mode 100644 index 0000000000000..bb3c6b8283f33 --- /dev/null +++ b/x-pack/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_filters.ts @@ -0,0 +1,55 @@ +/* + * 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 { badRequest } from 'boom'; +import moment from 'moment-timezone'; + +import { SavedSearchObjectAttributes, TimeRangeParams } from '../../'; +import { QueryFilter, Filter, SearchSourceFilter } from './'; + +export function getFilters( + indexPatternId: string, + indexPatternTimeField: string | null, + timerange: TimeRangeParams | null, + savedSearchObjectAttr: SavedSearchObjectAttributes, + searchSourceFilter?: SearchSourceFilter, + queryFilter?: QueryFilter +) { + let includes: string[]; + let timeFilter: any | null; + let timezone: string | null; + + if (indexPatternTimeField) { + if (!timerange) { + throw badRequest( + `Time range params are required for index pattern [${indexPatternId}], using time field [${indexPatternTimeField}]` + ); + } + + timezone = timerange.timezone; + const { min: gte, max: lte } = timerange; + timeFilter = { + range: { + [indexPatternTimeField]: { + format: 'strict_date_time', + gte: moment.tz(moment(gte), timezone).format(), + lte: moment.tz(moment(lte), timezone).format(), + }, + }, + }; + + const savedSearchCols = savedSearchObjectAttr.columns || []; + includes = [indexPatternTimeField, ...savedSearchCols]; + } else { + includes = savedSearchObjectAttr.columns || []; + timeFilter = null; + timezone = null; + } + + const combinedFilter: Filter[] = [timeFilter, searchSourceFilter, queryFilter].filter(Boolean); // builds an array of defined filters + + return { timezone, combinedFilter, includes }; +} diff --git a/x-pack/plugins/reporting/export_types/csv_from_savedobject/server/lib/index.d.ts b/x-pack/plugins/reporting/export_types/csv_from_savedobject/server/lib/index.d.ts new file mode 100644 index 0000000000000..7220cbc666083 --- /dev/null +++ b/x-pack/plugins/reporting/export_types/csv_from_savedobject/server/lib/index.d.ts @@ -0,0 +1,96 @@ +/* + * 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 { + SavedSearchObjectAttributes, + SearchPanel, + SearchRequest, + SearchSource, + TimeRangeParams, +} from '../../'; + +export interface SavedSearchGeneratorResult { + content: string; + maxSizeReached: boolean; + size: number; +} + +export interface CsvResultFromSearch { + type: string; + result: SavedSearchGeneratorResult | null; +} + +type EndpointCaller = (method: string, params: any) => Promise; +type FormatsMap = Map< + string, + { + id: string; + params: { + pattern: string; + }; + } +>; + +interface ReqPayload { + state: { + sort: Array<{ + [sortKey: string]: { + order: string; + }; + }>; + docvalue_fields: any; + query: any; + }; +} + +export interface GenerateCsvParams { + searchRequest: SearchRequest; + callEndpoint: EndpointCaller; + fields: string[]; + formatsMap: FormatsMap; + metaFields: string[]; // FIXME not sure what this is for + conflictedTypesFields: string[]; // FIXME not sure what this is for + cancellationToken: any; // FIXME not sure how to do anything with this + settings: { + separator: string; + quoteValues: boolean; + timezone: string | null; + maxSizeBytes: number; + scroll: { duration: string; size: number }; + }; +} + +/* + * These filter types are stub types to help ensure things get passed to + * non-Typescript functions in the right order. An actual structure is not + * needed because the code doesn't look into the properties; just combines them + * and passes them through to other non-TS modules. + */ +export interface Filter { + isFilter: boolean; +} +export interface TimeFilter extends Filter { + isTimeFilter: boolean; +} +export interface QueryFilter extends Filter { + isQueryFilter: boolean; +} +export interface SearchSourceFilter extends Filter { + isSearchSourceFilter: boolean; +} + +export interface ESQueryConfig { + allowLeadingWildcards: boolean; + queryStringOptions: boolean; + ignoreFilterIfFieldNotInIndex: boolean; +} + +export interface IndexPatternField { + scripted: boolean; + lang?: string; + script?: string; + name: string; +} diff --git a/x-pack/test/reporting/api/generate/csv_saved_search.ts b/x-pack/test/reporting/api/generate/csv_saved_search.ts index b0c4431df7617..18cb791537bfa 100644 --- a/x-pack/test/reporting/api/generate/csv_saved_search.ts +++ b/x-pack/test/reporting/api/generate/csv_saved_search.ts @@ -6,7 +6,13 @@ import expect from '@kbn/expect'; import supertest from 'supertest'; -import { CSV_RESULT_SCRIPTED, CSV_RESULT_TIMEBASED, CSV_RESULT_TIMELESS } from './fixtures'; +import { + CSV_RESULT_SCRIPTED, + CSV_RESULT_SCRIPTED_REQUERY, + CSV_RESULT_SCRIPTED_RESORTED, + CSV_RESULT_TIMEBASED, + CSV_RESULT_TIMELESS, +} from './fixtures'; interface GenerateOpts { timerange?: { @@ -31,88 +37,200 @@ export default function({ getService }: { getService: any }) { }; describe('Generation from Saved Search ID', () => { - it('With filters and timebased data', async () => { - // load test data that contains a saved search and documents - await esArchiver.load('reporting/logs'); - await esArchiver.load('logstash_functional'); - - // TODO: check headers for inline filename - const { - status: resStatus, - text: resText, - type: resType, - } = (await generateAPI.getCsvFromSavedSearch('search:d7a79750-3edd-11e9-99cc-4d80163ee9e7', { - timerange: { - timezone: 'UTC', - min: '2015-09-19T10:00:00.000Z', - max: '2015-09-21T10:00:00.000Z', - }, - state: {}, - })) as supertest.Response; - - expect(resStatus).to.eql(200); - expect(resType).to.eql('text/csv'); - expect(resText).to.eql(CSV_RESULT_TIMEBASED); - - await esArchiver.unload('reporting/logs'); - await esArchiver.unload('logstash_functional'); + describe('Saved Search Features', () => { + it('With filters and timebased data', async () => { + // load test data that contains a saved search and documents + await esArchiver.load('reporting/logs'); + await esArchiver.load('logstash_functional'); + + // TODO: check headers for inline filename + const { + status: resStatus, + text: resText, + type: resType, + } = (await generateAPI.getCsvFromSavedSearch( + 'search:d7a79750-3edd-11e9-99cc-4d80163ee9e7', + { + timerange: { + timezone: 'UTC', + min: '2015-09-19T10:00:00.000Z', + max: '2015-09-21T10:00:00.000Z', + }, + state: {}, + } + )) as supertest.Response; + + expect(resStatus).to.eql(200); + expect(resType).to.eql('text/csv'); + expect(resText).to.eql(CSV_RESULT_TIMEBASED); + + await esArchiver.unload('reporting/logs'); + await esArchiver.unload('logstash_functional'); + }); + + it('With filters and non-timebased data', async () => { + // load test data that contains a saved search and documents + await esArchiver.load('reporting/sales'); + + const { + status: resStatus, + text: resText, + type: resType, + } = (await generateAPI.getCsvFromSavedSearch( + 'search:71e3ee20-3f99-11e9-b8ee-6b9604f2f877', + { + state: {}, + } + )) as supertest.Response; + + expect(resStatus).to.eql(200); + expect(resType).to.eql('text/csv'); + expect(resText).to.eql(CSV_RESULT_TIMELESS); + + await esArchiver.unload('reporting/sales'); + }); + + it('With scripted fields and field formatters', async () => { + // load test data that contains a saved search and documents + await esArchiver.load('reporting/scripted'); + + const { + status: resStatus, + text: resText, + type: resType, + } = (await generateAPI.getCsvFromSavedSearch( + 'search:f34bf440-5014-11e9-bce7-4dabcb8bef24', + { + timerange: { + timezone: 'UTC', + min: '1979-01-01T10:00:00Z', + max: '1981-01-01T10:00:00Z', + }, + state: {}, + } + )) as supertest.Response; + + expect(resStatus).to.eql(200); + expect(resType).to.eql('text/csv'); + expect(resText).to.eql(CSV_RESULT_SCRIPTED); + + await esArchiver.unload('reporting/scripted'); + }); }); - it('With filters and non-timebased data', async () => { - // load test data that contains a saved search and documents - await esArchiver.load('reporting/sales'); - - // TODO: check headers for inline filename - const { - status: resStatus, - text: resText, - type: resType, - } = (await generateAPI.getCsvFromSavedSearch('search:71e3ee20-3f99-11e9-b8ee-6b9604f2f877', { - state: {}, - })) as supertest.Response; - - expect(resStatus).to.eql(200); - expect(resType).to.eql('text/csv'); - expect(resText).to.eql(CSV_RESULT_TIMELESS); - - await esArchiver.unload('reporting/sales'); - }); - - it('With scripted fields and field formatters', async () => { - // load test data that contains a saved search and documents - await esArchiver.load('reporting/scripted'); - - const { - status: resStatus, - text: resText, - type: resType, - } = (await generateAPI.getCsvFromSavedSearch('search:f34bf440-5014-11e9-bce7-4dabcb8bef24', { - timerange: { - timezone: 'UTC', - min: '1979-01-01T10:00:00Z', - max: '1981-01-01T10:00:00Z', - }, - state: {}, - })) as supertest.Response; - - expect(resStatus).to.eql(200); - expect(resType).to.eql('text/csv'); - expect(resText).to.eql(CSV_RESULT_SCRIPTED); - - await esArchiver.unload('reporting/scripted'); + describe('API Features', () => { + it('Return a 404', async () => { + const { body } = (await generateAPI.getCsvFromSavedSearch('search:gobbledygook', { + timerange: { timezone: 'UTC', min: 63097200000, max: 126255599999 }, + state: {}, + })) as supertest.Response; + const expectedBody = { + error: 'Not Found', + message: 'Saved object [search/gobbledygook] not found', + statusCode: 404, + }; + expect(body).to.eql(expectedBody); + }); + + it('Return 400 if time range param is needed but missing', async () => { + // load test data that contains a saved search and documents + await esArchiver.load('reporting/logs'); + await esArchiver.load('logstash_functional'); + + const { + status: resStatus, + text: resText, + type: resType, + } = (await generateAPI.getCsvFromSavedSearch( + 'search:d7a79750-3edd-11e9-99cc-4d80163ee9e7', + { state: {} } + )) as supertest.Response; + + expect(resStatus).to.eql(400); + expect(resType).to.eql('application/json'); + const { message: errorMessage } = JSON.parse(resText); + expect(errorMessage).to.eql( + 'Time range params are required for index pattern [logstash-*], using time field [@timestamp]' + ); + + await esArchiver.unload('reporting/logs'); + await esArchiver.unload('logstash_functional'); + }); }); - it('Return a 404', async () => { - const { body } = (await generateAPI.getCsvFromSavedSearch('search:gobbledygook', { - timerange: { timezone: 'UTC', min: 63097200000, max: 126255599999 }, - state: {}, - })) as supertest.Response; - const expectedBody = { - error: 'Not Found', - message: 'Saved object [search/gobbledygook] not found', - statusCode: 404, - }; - expect(body).to.eql(expectedBody); + describe('Merge user state into the query', () => { + it('for query', async () => { + // load test data that contains a saved search and documents + await esArchiver.load('reporting/scripted'); + + const { + status: resStatus, + text: resText, + type: resType, + } = (await generateAPI.getCsvFromSavedSearch( + 'search:f34bf440-5014-11e9-bce7-4dabcb8bef24', + { + timerange: { + timezone: 'UTC', + min: '1979-01-01T10:00:00Z', + max: '1981-01-01T10:00:00Z', + }, + state: { + query: { + bool: { + filter: [ + { + bool: { + filter: [ + { + bool: { + minimum_should_match: 1, + should: [{ query_string: { fields: ['name'], query: 'Fe*' } }], + }, + }, + ], + }, + }, + ], + }, + }, + }, + } + )) as supertest.Response; + + expect(resStatus).to.eql(200); + expect(resType).to.eql('text/csv'); + expect(resText).to.eql(CSV_RESULT_SCRIPTED_REQUERY); + + await esArchiver.unload('reporting/scripted'); + }); + + it('for sort', async () => { + // load test data that contains a saved search and documents + await esArchiver.load('reporting/scripted'); + + const { + status: resStatus, + text: resText, + type: resType, + } = (await generateAPI.getCsvFromSavedSearch( + 'search:f34bf440-5014-11e9-bce7-4dabcb8bef24', + { + timerange: { + timezone: 'UTC', + min: '1979-01-01T10:00:00Z', + max: '1981-01-01T10:00:00Z', + }, + state: { sort: [{ name: { order: 'asc', unmapped_type: 'boolean' } }] }, + } + )) as supertest.Response; + + expect(resStatus).to.eql(200); + expect(resType).to.eql('text/csv'); + expect(resText).to.eql(CSV_RESULT_SCRIPTED_RESORTED); + + await esArchiver.unload('reporting/scripted'); + }); }); }); } diff --git a/x-pack/test/reporting/api/generate/fixtures.ts b/x-pack/test/reporting/api/generate/fixtures.ts index da447333626f4..03d9fbf0bfb69 100644 --- a/x-pack/test/reporting/api/generate/fixtures.ts +++ b/x-pack/test/reporting/api/generate/fixtures.ts @@ -98,3 +98,33 @@ export const CSV_RESULT_SCRIPTED = `date,year,name,value,"years_ago" "1980-01-01T00:00:00.000Z",1980,Feverly,2249,39 "1980-01-01T00:00:00.000Z",1980,Fecky,2071,39 `; + +export const CSV_RESULT_SCRIPTED_REQUERY = `date,year,name,value,"years_ago" +"1981-01-01T00:00:00.000Z",1981,Fetty,1763,38 +"1981-01-01T00:00:00.000Z",1981,Felinda,1886,38 +"1981-01-01T00:00:00.000Z",1981,Feth,3685,38 +"1981-01-01T00:00:00.000Z",1981,Feverly,1987,38 +"1981-01-01T00:00:00.000Z",1981,Fecky,1930,38 +"1980-01-01T00:00:00.000Z",1980,Fetty,1967,39 +"1980-01-01T00:00:00.000Z",1980,Feth,4246,39 +"1980-01-01T00:00:00.000Z",1980,Feverly,2249,39 +"1980-01-01T00:00:00.000Z",1980,Fecky,2071,39 +`; + +export const CSV_RESULT_SCRIPTED_RESORTED = `date,year,name,value,"years_ago" +"1981-01-01T00:00:00.000Z",1981,Farbara,6456,38 +"1980-01-01T00:00:00.000Z",1980,Farbara,8026,39 +"1981-01-01T00:00:00.000Z",1981,Fecky,1930,38 +"1980-01-01T00:00:00.000Z",1980,Fecky,2071,39 +"1981-01-01T00:00:00.000Z",1981,Felinda,1886,38 +"1981-01-01T00:00:00.000Z",1981,Feth,3685,38 +"1980-01-01T00:00:00.000Z",1980,Feth,4246,39 +"1981-01-01T00:00:00.000Z",1981,Fetty,1763,38 +"1980-01-01T00:00:00.000Z",1980,Fetty,1967,39 +"1981-01-01T00:00:00.000Z",1981,Feverly,1987,38 +"1980-01-01T00:00:00.000Z",1980,Feverly,2249,39 +"1981-01-01T00:00:00.000Z",1981,Fonnie,2330,38 +"1980-01-01T00:00:00.000Z",1980,Fonnie,2748,39 +"1981-01-01T00:00:00.000Z",1981,Frenda,7162,38 +"1980-01-01T00:00:00.000Z",1980,Frenda,8335,39 +`;