diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.createsearchsource.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.createsearchsource.md new file mode 100644 index 0000000000000..5c5aa348eecdf --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.createsearchsource.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [createSearchSource](./kibana-plugin-plugins-data-public.createsearchsource.md) + +## createSearchSource variable + +Deserializes a json string and a set of referenced objects to a `SearchSource` instance. Use this method to re-create the search source serialized using `searchSource.serialize`. + +This function is a factory function that returns the actual utility when calling it with the required service dependency (index patterns contract). A pre-wired version is also exposed in the start contract of the data plugin as part of the search service + +Signature: + +```typescript +createSearchSource: (indexPatterns: Pick) => (searchSourceJson: string, references: SavedObjectReference[]) => Promise +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 6964c070097c5..fc0dab94a0f65 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -102,6 +102,7 @@ | [castEsToKbnFieldTypeName](./kibana-plugin-plugins-data-public.castestokbnfieldtypename.md) | Get the KbnFieldType name for an esType string | | [connectToQueryState](./kibana-plugin-plugins-data-public.connecttoquerystate.md) | Helper to setup two-way syncing of global data and a state container | | [createSavedQueryService](./kibana-plugin-plugins-data-public.createsavedqueryservice.md) | | +| [createSearchSource](./kibana-plugin-plugins-data-public.createsearchsource.md) | Deserializes a json string and a set of referenced objects to a SearchSource instance. Use this method to re-create the search source serialized using searchSource.serialize.This function is a factory function that returns the actual utility when calling it with the required service dependency (index patterns contract). A pre-wired version is also exposed in the start contract of the data plugin as part of the search service | | [ES\_SEARCH\_STRATEGY](./kibana-plugin-plugins-data-public.es_search_strategy.md) | | | [esFilters](./kibana-plugin-plugins-data-public.esfilters.md) | | | [esKuery](./kibana-plugin-plugins-data-public.eskuery.md) | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md index 8e1dbb6e2671d..5f2fc809a5590 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md @@ -38,6 +38,7 @@ export declare class SearchSource | [getParent()](./kibana-plugin-plugins-data-public.searchsource.getparent.md) | | Get the parent of this SearchSource {undefined\|searchSource} | | [getSearchRequestBody()](./kibana-plugin-plugins-data-public.searchsource.getsearchrequestbody.md) | | | | [onRequestStart(handler)](./kibana-plugin-plugins-data-public.searchsource.onrequeststart.md) | | Add a handler that will be notified whenever requests start | +| [serialize()](./kibana-plugin-plugins-data-public.searchsource.serialize.md) | | Serializes the instance to a JSON string and a set of referenced objects. Use this method to get a representation of the search source which can be stored in a saved object.The references returned by this function can be mixed with other references in the same object, however make sure there are no name-collisions. The references will be named kibanaSavedObjectMeta.searchSourceJSON.index and kibanaSavedObjectMeta.searchSourceJSON.filter[<number>].meta.index.Using createSearchSource, the instance can be re-created. | | [setField(field, value)](./kibana-plugin-plugins-data-public.searchsource.setfield.md) | | | | [setFields(newFields)](./kibana-plugin-plugins-data-public.searchsource.setfields.md) | | | | [setParent(parent, options)](./kibana-plugin-plugins-data-public.searchsource.setparent.md) | | Set a searchSource that this source should inherit from | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.serialize.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.serialize.md new file mode 100644 index 0000000000000..52d25dec01dfd --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.serialize.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [serialize](./kibana-plugin-plugins-data-public.searchsource.serialize.md) + +## SearchSource.serialize() method + +Serializes the instance to a JSON string and a set of referenced objects. Use this method to get a representation of the search source which can be stored in a saved object. + +The references returned by this function can be mixed with other references in the same object, however make sure there are no name-collisions. The references will be named `kibanaSavedObjectMeta.searchSourceJSON.index` and `kibanaSavedObjectMeta.searchSourceJSON.filter[].meta.index`. + +Using `createSearchSource`, the instance can be re-created. + +Signature: + +```typescript +serialize(): { + searchSourceJSON: string; + references: SavedObjectReference[]; + }; +``` +Returns: + +`{ + searchSourceJSON: string; + references: SavedObjectReference[]; + }` + diff --git a/src/legacy/core_plugins/kibana/public/discover/build_services.ts b/src/legacy/core_plugins/kibana/public/discover/build_services.ts index 180ff13cdddc0..a3a99a0ded523 100644 --- a/src/legacy/core_plugins/kibana/public/discover/build_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/build_services.ts @@ -72,6 +72,7 @@ export async function buildServices( const services = { savedObjectsClient: core.savedObjects.client, indexPatterns: plugins.data.indexPatterns, + search: plugins.data.search, chrome: core.chrome, overlays: core.overlays, }; diff --git a/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts b/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts index 7261b2ba03372..705be68a141e7 100644 --- a/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts +++ b/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts @@ -56,6 +56,7 @@ export const savedObjectManagementRegistry: ISavedObjectsManagementRegistry = { const services = { savedObjectsClient: npStart.core.savedObjects.client, indexPatterns: npStart.plugins.data.indexPatterns, + search: npStart.plugins.data.search, chrome: npStart.core.chrome, overlays: npStart.core.overlays, }; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/flyout.test.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/flyout.test.js index 5d14c4609b918..0d16e0ae35dd6 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/flyout.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/flyout.test.js @@ -519,7 +519,8 @@ describe('Flyout', () => { expect(resolveIndexPatternConflicts).toHaveBeenCalledWith( component.instance().resolutions, mockConflictedIndexPatterns, - true + true, + defaultProps.indexPatterns ); expect(saveObjects).toHaveBeenCalledWith( mockConflictedSavedObjectsLinkedToSavedSearches, diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js index 105c279218375..da2221bb54203 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js @@ -358,7 +358,8 @@ export class Flyout extends Component { importCount += await resolveIndexPatternConflicts( resolutions, conflictedIndexPatterns, - isOverwriteAllChecked + isOverwriteAllChecked, + this.props.indexPatterns ); } this.setState({ diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.test.ts b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.test.ts index 8243aa69ac082..dc6d2643145ff 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.test.ts +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.test.ts @@ -84,7 +84,7 @@ describe('resolveSavedObjects', () => { }, } as unknown) as IndexPatternsContract; - const services = [ + const services = ([ { type: 'search', get: async () => { @@ -124,7 +124,7 @@ describe('resolveSavedObjects', () => { }; }, }, - ] as SavedObjectLoader[]; + ] as unknown) as SavedObjectLoader[]; const overwriteAll = false; @@ -176,7 +176,7 @@ describe('resolveSavedObjects', () => { }, } as unknown) as IndexPatternsContract; - const services = [ + const services = ([ { type: 'search', get: async () => { @@ -217,7 +217,7 @@ describe('resolveSavedObjects', () => { }; }, }, - ] as SavedObjectLoader[]; + ] as unknown) as SavedObjectLoader[]; const overwriteAll = false; @@ -237,33 +237,38 @@ describe('resolveSavedObjects', () => { describe('resolveIndexPatternConflicts', () => { it('should resave resolutions', async () => { - const hydrateIndexPattern = jest.fn(); const save = jest.fn(); - const conflictedIndexPatterns = [ + const conflictedIndexPatterns = ([ { obj: { - searchSource: { - getOwnField: (field: string) => { - return field === 'index' ? '1' : undefined; + save, + }, + doc: { + _source: { + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: '1', + }), }, }, - hydrateIndexPattern, - save, }, }, { obj: { - searchSource: { - getOwnField: (field: string) => { - return field === 'index' ? '3' : undefined; + save, + }, + doc: { + _source: { + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: '3', + }), }, }, - hydrateIndexPattern, - save, }, }, - ]; + ] as unknown) as Array<{ obj: SavedObject; doc: any }>; const resolutions = [ { @@ -282,43 +287,49 @@ describe('resolveSavedObjects', () => { const overwriteAll = false; - await resolveIndexPatternConflicts(resolutions, conflictedIndexPatterns, overwriteAll); - expect(hydrateIndexPattern.mock.calls.length).toBe(2); + await resolveIndexPatternConflicts(resolutions, conflictedIndexPatterns, overwriteAll, ({ + get: (id: string) => Promise.resolve({ id }), + } as unknown) as IndexPatternsContract); + expect(conflictedIndexPatterns[0].obj.searchSource!.getField('index')!.id).toEqual('2'); + expect(conflictedIndexPatterns[1].obj.searchSource!.getField('index')!.id).toEqual('4'); expect(save.mock.calls.length).toBe(2); expect(save).toHaveBeenCalledWith({ confirmOverwrite: !overwriteAll }); - expect(hydrateIndexPattern).toHaveBeenCalledWith('2'); - expect(hydrateIndexPattern).toHaveBeenCalledWith('4'); }); it('should resolve filter index conflicts', async () => { - const hydrateIndexPattern = jest.fn(); const save = jest.fn(); - const conflictedIndexPatterns = [ + const conflictedIndexPatterns = ([ { obj: { - searchSource: { - getOwnField: (field: string) => { - return field === 'index' ? '1' : [{ meta: { index: 'filterIndex' } }]; + save, + }, + doc: { + _source: { + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: '1', + filter: [{ meta: { index: 'filterIndex' } }], + }), }, - setField: jest.fn(), }, - hydrateIndexPattern, - save, }, }, { obj: { - searchSource: { - getOwnField: (field: string) => { - return field === 'index' ? '3' : undefined; + save, + }, + doc: { + _source: { + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: '3', + }), }, }, - hydrateIndexPattern, - save, }, }, - ]; + ] as unknown) as Array<{ obj: SavedObject; doc: any }>; const resolutions = [ { @@ -337,9 +348,11 @@ describe('resolveSavedObjects', () => { const overwriteAll = false; - await resolveIndexPatternConflicts(resolutions, conflictedIndexPatterns, overwriteAll); + await resolveIndexPatternConflicts(resolutions, conflictedIndexPatterns, overwriteAll, ({ + get: (id: string) => Promise.resolve({ id }), + } as unknown) as IndexPatternsContract); - expect(conflictedIndexPatterns[0].obj.searchSource.setField).toHaveBeenCalledWith('filter', [ + expect(conflictedIndexPatterns[0].obj.searchSource!.getField('filter')).toEqual([ { meta: { index: 'newFilterIndex' } }, ]); expect(save.mock.calls.length).toBe(2); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.ts b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.ts index 902de654f5f85..d9473367f7502 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.ts +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.ts @@ -18,12 +18,17 @@ */ import { i18n } from '@kbn/i18n'; -import { OverlayStart } from 'src/core/public'; +import { cloneDeep } from 'lodash'; +import { OverlayStart, SavedObjectReference } from 'src/core/public'; import { SavedObject, SavedObjectLoader, } from '../../../../../../../../plugins/saved_objects/public'; -import { IndexPatternsContract, IIndexPattern } from '../../../../../../../../plugins/data/public'; +import { + IndexPatternsContract, + IIndexPattern, + createSearchSource, +} from '../../../../../../../../plugins/data/public'; type SavedObjectsRawDoc = Record; @@ -126,7 +131,7 @@ async function importIndexPattern( async function importDocument(obj: SavedObject, doc: SavedObjectsRawDoc, overwriteAll: boolean) { await obj.applyESResp({ references: doc._references || [], - ...doc, + ...cloneDeep(doc), }); return await obj.save({ confirmOverwrite: !overwriteAll }); } @@ -160,41 +165,57 @@ async function awaitEachItemInParallel(list: T[], op: (item: T) => R) { export async function resolveIndexPatternConflicts( resolutions: Array<{ oldId: string; newId: string }>, conflictedIndexPatterns: any[], - overwriteAll: boolean + overwriteAll: boolean, + indexPatterns: IndexPatternsContract ) { let importCount = 0; - await awaitEachItemInParallel(conflictedIndexPatterns, async ({ obj }) => { - // Resolve search index reference: - let oldIndexId = obj.searchSource.getOwnField('index'); - // Depending on the object, this can either be the raw id or the actual index pattern object - if (typeof oldIndexId !== 'string') { - oldIndexId = oldIndexId.id; - } - let resolution = resolutions.find(({ oldId }) => oldId === oldIndexId); - if (resolution) { - const newIndexId = resolution.newId; - await obj.hydrateIndexPattern(newIndexId); + await awaitEachItemInParallel(conflictedIndexPatterns, async ({ obj, doc }) => { + const serializedSearchSource = JSON.parse( + doc._source.kibanaSavedObjectMeta?.searchSourceJSON || '{}' + ); + const oldIndexId = serializedSearchSource.index; + let allResolved = true; + const inlineResolution = resolutions.find(({ oldId }) => oldId === oldIndexId); + if (inlineResolution) { + serializedSearchSource.index = inlineResolution.newId; + } else { + allResolved = false; } // Resolve filter index reference: - const filter = (obj.searchSource.getOwnField('filter') || []).map((f: any) => { + const filter = (serializedSearchSource.filter || []).map((f: any) => { if (!(f.meta && f.meta.index)) { return f; } - resolution = resolutions.find(({ oldId }) => oldId === f.meta.index); + const resolution = resolutions.find(({ oldId }) => oldId === f.meta.index); return resolution ? { ...f, ...{ meta: { ...f.meta, index: resolution.newId } } } : f; }); if (filter.length > 0) { - obj.searchSource.setField('filter', filter); + serializedSearchSource.filter = filter; } - if (!resolution) { + const replacedReferences = (doc._references || []).map((reference: SavedObjectReference) => { + const resolution = resolutions.find(({ oldId }) => oldId === reference.id); + if (resolution) { + return { ...reference, id: resolution.newId }; + } else { + allResolved = false; + } + + return reference; + }); + + if (!allResolved) { // The user decided to skip this conflict so do nothing return; } + obj.searchSource = await createSearchSource(indexPatterns)( + JSON.stringify(serializedSearchSource), + replacedReferences + ); if (await saveObject(obj, overwriteAll)) { importCount++; } diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js index 7c9ab32ab2f72..a710d3e318749 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js @@ -71,6 +71,7 @@ const getResolvedResults = deps => { return createSavedSearchesLoader({ savedObjectsClient: core.savedObjects.client, indexPatterns: data.indexPatterns, + search: data.search, chrome: core.chrome, overlays: core.overlays, }).get(results.vis.data.savedSearchId); diff --git a/src/legacy/core_plugins/timelion/public/services/saved_sheets.ts b/src/legacy/core_plugins/timelion/public/services/saved_sheets.ts index 201b21f932988..e7f431a178ea0 100644 --- a/src/legacy/core_plugins/timelion/public/services/saved_sheets.ts +++ b/src/legacy/core_plugins/timelion/public/services/saved_sheets.ts @@ -28,6 +28,7 @@ const savedObjectsClient = npStart.core.savedObjects.client; const services = { savedObjectsClient, indexPatterns: npStart.plugins.data.indexPatterns, + search: npStart.plugins.data.search, chrome: npStart.core.chrome, overlays: npStart.core.overlays, }; diff --git a/src/legacy/ui/public/new_platform/set_services.ts b/src/legacy/ui/public/new_platform/set_services.ts index 8cf015d5dff5c..400f31e73ffa1 100644 --- a/src/legacy/ui/public/new_platform/set_services.ts +++ b/src/legacy/ui/public/new_platform/set_services.ts @@ -72,9 +72,11 @@ export function setStartServices(npStart: NpStart) { visualizationsServices.setAggs(npStart.plugins.data.search.aggs); visualizationsServices.setOverlays(npStart.core.overlays); visualizationsServices.setChrome(npStart.core.chrome); + visualizationsServices.setSearch(npStart.plugins.data.search); const savedVisualizationsLoader = createSavedVisLoader({ savedObjectsClient: npStart.core.savedObjects.client, indexPatterns: npStart.plugins.data.indexPatterns, + search: npStart.plugins.data.search, chrome: npStart.core.chrome, overlays: npStart.core.overlays, visualizationTypes: visualizationsServices.getTypes(), diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index c98fa612dc7af..322d734d9f39f 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -284,7 +284,7 @@ export class DashboardPlugin const { notifications } = core; const { uiActions, - data: { indexPatterns }, + data: { indexPatterns, search }, } = plugins; const SavedObjectFinder = getSavedObjectFinder(core.savedObjects, core.uiSettings); @@ -300,6 +300,7 @@ export class DashboardPlugin const savedDashboardLoader = createSavedDashboardLoader({ savedObjectsClient: core.savedObjects.client, indexPatterns, + search, chrome: core.chrome, overlays: core.overlays, }); diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts b/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts index 2a1e64fa88a02..09357072a13a6 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts +++ b/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts @@ -18,13 +18,14 @@ */ import { SavedObjectsClientContract, ChromeStart, OverlayStart } from 'kibana/public'; -import { IndexPatternsContract } from '../../../../plugins/data/public'; +import { DataPublicPluginStart, IndexPatternsContract } from '../../../../plugins/data/public'; import { SavedObjectLoader } from '../../../../plugins/saved_objects/public'; import { createSavedDashboardClass } from './saved_dashboard'; interface Services { savedObjectsClient: SavedObjectsClientContract; indexPatterns: IndexPatternsContract; + search: DataPublicPluginStart['search']; chrome: ChromeStart; overlays: OverlayStart; } diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index efafea44167d4..06a46065baa84 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -366,6 +366,7 @@ export { SearchStrategyProvider, ISearchSource, SearchSource, + createSearchSource, SearchSourceFields, EsQuerySortValue, SortDirection, diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 15067077afc43..2ebe377b3b32f 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -155,7 +155,7 @@ export class DataPublicPlugin implements Plugin({ timefilter: { timefil // @public (undocumented) export const createSavedQueryService: (savedObjectsClient: Pick) => SavedQueryService; +// @public +export const createSearchSource: (indexPatterns: Pick) => (searchSourceJson: string, references: SavedObjectReference[]) => Promise; + // Warning: (ae-missing-release-tag) "CustomFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1667,6 +1671,10 @@ export class SearchSource { // (undocumented) history: SearchRequest[]; onRequestStart(handler: (searchSource: ISearchSource, options?: FetchOptions) => Promise): void; + serialize(): { + searchSourceJSON: string; + references: SavedObjectReference[]; + }; // (undocumented) setField(field: K, value: SearchSourceFields[K]): this; // (undocumented) @@ -1881,21 +1889,21 @@ export type TSearchStrategyProvider = (context: ISearc // src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "getRoutes" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:382:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:382:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:382:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:382:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:387:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:388:1 - (ae-forgotten-export) The symbol "convertDateRangeToString" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:390:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:404:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:405:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:408:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:409:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:383:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:383:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:383:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:383:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:388:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:389:1 - (ae-forgotten-export) The symbol "convertDateRangeToString" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:391:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:402:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:405:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:406:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:409:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:410:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:413:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:33:33 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:37:1 - (ae-forgotten-export) The symbol "QueryStateChange" needs to be exported by the entry point index.d.ts // src/plugins/data/public/types.ts:52:5 - (ae-forgotten-export) The symbol "createFiltersFromEvent" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts index 1687d749f46e2..cce973d632f41 100644 --- a/src/plugins/data/public/search/index.ts +++ b/src/plugins/data/public/search/index.ts @@ -54,6 +54,7 @@ export { SearchSourceFields, EsQuerySortValue, SortDirection, + createSearchSource, } from './search_source'; export { SearchInterceptor } from './search_interceptor'; diff --git a/src/plugins/data/public/search/mocks.ts b/src/plugins/data/public/search/mocks.ts index b70e889066a45..cb1c625a72959 100644 --- a/src/plugins/data/public/search/mocks.ts +++ b/src/plugins/data/public/search/mocks.ts @@ -33,6 +33,7 @@ export const searchStartMock: jest.Mocked = { aggs: searchAggsStartMock(), setInterceptor: jest.fn(), search: jest.fn(), + createSearchSource: jest.fn(), __LEGACY: { AggConfig: jest.fn() as any, AggType: jest.fn(), diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 42f31ef450d28..6124682184821 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -25,6 +25,8 @@ import { TStrategyTypes } from './strategy_types'; import { getEsClient, LegacyApiCaller } from './es_client'; import { ES_SEARCH_STRATEGY, DEFAULT_SEARCH_STRATEGY } from '../../common/search'; import { esSearchStrategyProvider } from './es_search/es_search_strategy'; +import { IndexPatternsContract } from '../index_patterns/index_patterns'; +import { createSearchSource } from './search_source'; import { QuerySetup } from '../query/query_service'; import { GetInternalStartServicesFn } from '../types'; import { SearchInterceptor } from './search_interceptor'; @@ -108,7 +110,7 @@ export class SearchService implements Plugin { }; } - public start(core: CoreStart): ISearchStart { + public start(core: CoreStart, indexPatterns: IndexPatternsContract): ISearchStart { /** * A global object that intercepts all searches and provides convenience methods for cancelling * all pending search requests, as well as getting the number of pending search requests. @@ -145,6 +147,7 @@ export class SearchService implements Plugin { // TODO: should an intercepror have a destroy method? this.searchInterceptor = searchInterceptor; }, + createSearchSource: createSearchSource(indexPatterns), __LEGACY: { esClient: this.esClient!, AggConfig, diff --git a/src/plugins/data/public/search/search_source/create_search_source.test.ts b/src/plugins/data/public/search/search_source/create_search_source.test.ts new file mode 100644 index 0000000000000..d49ce5a0d11f8 --- /dev/null +++ b/src/plugins/data/public/search/search_source/create_search_source.test.ts @@ -0,0 +1,151 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { createSearchSource as createSearchSourceFactory } from './create_search_source'; +import { IIndexPattern } from '../../../common/index_patterns'; +import { IndexPatternsContract } from '../../index_patterns/index_patterns'; +import { Filter } from '../../../common/es_query/filters'; + +describe('createSearchSource', function() { + let createSearchSource: ReturnType; + const indexPatternMock: IIndexPattern = {} as IIndexPattern; + let indexPatternContractMock: jest.Mocked; + + beforeEach(() => { + indexPatternContractMock = ({ + get: jest.fn().mockReturnValue(Promise.resolve(indexPatternMock)), + } as unknown) as jest.Mocked; + createSearchSource = createSearchSourceFactory(indexPatternContractMock); + }); + + it('should fail if JSON is invalid', () => { + expect(createSearchSource('{', [])).rejects.toThrow(); + expect(createSearchSource('0', [])).rejects.toThrow(); + expect(createSearchSource('"abcdefg"', [])).rejects.toThrow(); + }); + + it('should set fields', async () => { + const searchSource = await createSearchSource( + JSON.stringify({ + highlightAll: true, + query: { + query: '', + language: 'kuery', + }, + }), + [] + ); + expect(searchSource.getOwnField('highlightAll')).toBe(true); + expect(searchSource.getOwnField('query')).toEqual({ + query: '', + language: 'kuery', + }); + }); + + it('should resolve referenced index pattern', async () => { + const searchSource = await createSearchSource( + JSON.stringify({ + indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index', + }), + [ + { + id: '123-456', + type: 'index-pattern', + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + }, + ] + ); + expect(indexPatternContractMock.get).toHaveBeenCalledWith('123-456'); + expect(searchSource.getOwnField('index')).toBe(indexPatternMock); + }); + + it('should set filters and resolve referenced index patterns', async () => { + const searchSource = await createSearchSource( + JSON.stringify({ + filter: [ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Clothing", + }, + indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index', + }, + query: { + match_phrase: { + 'category.keyword': "Men's Clothing", + }, + }, + $state: { + store: 'appState', + }, + }, + ], + }), + [ + { + id: '123-456', + type: 'index-pattern', + name: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index', + }, + ] + ); + const filters = searchSource.getOwnField('filter') as Filter[]; + expect(filters[0]).toMatchInlineSnapshot(` + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "index": "123-456", + "key": "category.keyword", + "negate": false, + "params": Object { + "query": "Men's Clothing", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "category.keyword": "Men's Clothing", + }, + }, + } + `); + }); + + it('should migrate legacy queries on the fly', async () => { + const searchSource = await createSearchSource( + JSON.stringify({ + highlightAll: true, + query: 'a:b', + }), + [] + ); + expect(searchSource.getOwnField('query')).toEqual({ + query: 'a:b', + language: 'lucene', + }); + }); +}); diff --git a/src/plugins/data/public/search/search_source/create_search_source.ts b/src/plugins/data/public/search/search_source/create_search_source.ts new file mode 100644 index 0000000000000..35b7ac4eb9762 --- /dev/null +++ b/src/plugins/data/public/search/search_source/create_search_source.ts @@ -0,0 +1,113 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import _ from 'lodash'; +import { SavedObjectReference } from 'kibana/public'; +import { migrateLegacyQuery } from '../../../../kibana_legacy/public'; +import { InvalidJSONProperty } from '../../../../kibana_utils/public'; +import { SearchSource } from './search_source'; +import { IndexPatternsContract } from '../../index_patterns/index_patterns'; +import { SearchSourceFields } from './types'; + +/** + * Deserializes a json string and a set of referenced objects to a `SearchSource` instance. + * Use this method to re-create the search source serialized using `searchSource.serialize`. + * + * This function is a factory function that returns the actual utility when calling it with the + * required service dependency (index patterns contract). A pre-wired version is also exposed in + * the start contract of the data plugin as part of the search service + * + * @param indexPatterns The index patterns contract of the data plugin + * + * @return Wired utility function taking two parameters `searchSourceJson`, the json string + * returned by `serializeSearchSource` and `references`, a list of references including the ones + * returned by `serializeSearchSource`. + * + * @public */ +export const createSearchSource = (indexPatterns: IndexPatternsContract) => async ( + searchSourceJson: string, + references: SavedObjectReference[] +) => { + const searchSource = new SearchSource(); + + // if we have a searchSource, set its values based on the searchSourceJson field + let searchSourceValues: Record; + try { + searchSourceValues = JSON.parse(searchSourceJson); + } catch (e) { + throw new InvalidJSONProperty( + `Invalid JSON in search source. ${e.message} JSON: ${searchSourceJson}` + ); + } + + // This detects a scenario where documents with invalid JSON properties have been imported into the saved object index. + // (This happened in issue #20308) + if (!searchSourceValues || typeof searchSourceValues !== 'object') { + throw new InvalidJSONProperty('Invalid JSON in search source.'); + } + + // Inject index id if a reference is saved + if (searchSourceValues.indexRefName) { + const reference = references.find(ref => ref.name === searchSourceValues.indexRefName); + if (!reference) { + throw new Error(`Could not find reference for ${searchSourceValues.indexRefName}`); + } + searchSourceValues.index = reference.id; + delete searchSourceValues.indexRefName; + } + + if (searchSourceValues.filter && Array.isArray(searchSourceValues.filter)) { + searchSourceValues.filter.forEach((filterRow: any) => { + if (!filterRow.meta || !filterRow.meta.indexRefName) { + return; + } + const reference = references.find((ref: any) => ref.name === filterRow.meta.indexRefName); + if (!reference) { + throw new Error(`Could not find reference for ${filterRow.meta.indexRefName}`); + } + filterRow.meta.index = reference.id; + delete filterRow.meta.indexRefName; + }); + } + + if (searchSourceValues.index && typeof searchSourceValues.index === 'string') { + searchSourceValues.index = await indexPatterns.get(searchSourceValues.index); + } + + const searchSourceFields = searchSource.getFields(); + const fnProps = _.transform( + searchSourceFields, + function(dynamic, val, name) { + if (_.isFunction(val) && name) dynamic[name] = val; + }, + {} + ); + + // This assignment might hide problems because the type of values passed from the parsed JSON + // might not fit the SearchSourceFields interface. + const newFields: SearchSourceFields = _.defaults(searchSourceValues, fnProps); + + searchSource.setFields(newFields); + const query = searchSource.getOwnField('query'); + + if (typeof query !== 'undefined') { + searchSource.setField('query', migrateLegacyQuery(query)); + } + + return searchSource; +}; diff --git a/src/plugins/data/public/search/search_source/index.ts b/src/plugins/data/public/search/search_source/index.ts index 10f1b2bc332e1..0e9f530d0968a 100644 --- a/src/plugins/data/public/search/search_source/index.ts +++ b/src/plugins/data/public/search/search_source/index.ts @@ -18,4 +18,5 @@ */ export * from './search_source'; +export { createSearchSource } from './create_search_source'; export { SortDirection, EsQuerySortValue, SearchSourceFields } from './types'; diff --git a/src/plugins/data/public/search/search_source/mocks.ts b/src/plugins/data/public/search/search_source/mocks.ts index 700bea741bd6a..1ef7c1187a9e0 100644 --- a/src/plugins/data/public/search/search_source/mocks.ts +++ b/src/plugins/data/public/search/search_source/mocks.ts @@ -37,4 +37,5 @@ export const searchSourceMock: MockedKeys = { getSearchRequestBody: jest.fn(), destroy: jest.fn(), history: [], + serialize: jest.fn(), }; diff --git a/src/plugins/data/public/search/search_source/search_source.test.ts b/src/plugins/data/public/search/search_source/search_source.test.ts index fcd116a3f4121..6bad093d31402 100644 --- a/src/plugins/data/public/search/search_source/search_source.test.ts +++ b/src/plugins/data/public/search/search_source/search_source.test.ts @@ -18,7 +18,7 @@ */ import { SearchSource } from './search_source'; -import { IndexPattern } from '../..'; +import { IndexPattern, SortDirection } from '../..'; import { mockDataServices } from '../aggs/test_helpers'; jest.mock('../fetch', () => ({ @@ -150,4 +150,77 @@ describe('SearchSource', function() { expect(parentFn).toBeCalledWith(searchSource, options); }); }); + + describe('#serialize', function() { + it('should reference index patterns', () => { + const indexPattern123 = { id: '123' } as IndexPattern; + const searchSource = new SearchSource(); + searchSource.setField('index', indexPattern123); + const { searchSourceJSON, references } = searchSource.serialize(); + expect(references[0].id).toEqual('123'); + expect(references[0].type).toEqual('index-pattern'); + expect(JSON.parse(searchSourceJSON).indexRefName).toEqual(references[0].name); + }); + + it('should add other fields', () => { + const searchSource = new SearchSource(); + searchSource.setField('highlightAll', true); + searchSource.setField('from', 123456); + const { searchSourceJSON } = searchSource.serialize(); + expect(JSON.parse(searchSourceJSON).highlightAll).toEqual(true); + expect(JSON.parse(searchSourceJSON).from).toEqual(123456); + }); + + it('should omit sort and size', () => { + const searchSource = new SearchSource(); + searchSource.setField('highlightAll', true); + searchSource.setField('from', 123456); + searchSource.setField('sort', { field: SortDirection.asc }); + searchSource.setField('size', 200); + const { searchSourceJSON } = searchSource.serialize(); + expect(Object.keys(JSON.parse(searchSourceJSON))).toEqual(['highlightAll', 'from']); + }); + + it('should serialize filters', () => { + const searchSource = new SearchSource(); + const filter = [ + { + query: 'query', + meta: { + alias: 'alias', + disabled: false, + negate: false, + }, + }, + ]; + searchSource.setField('filter', filter); + const { searchSourceJSON } = searchSource.serialize(); + expect(JSON.parse(searchSourceJSON).filter).toEqual(filter); + }); + + it('should reference index patterns in filters separately from index field', () => { + const searchSource = new SearchSource(); + const indexPattern123 = { id: '123' } as IndexPattern; + searchSource.setField('index', indexPattern123); + const filter = [ + { + query: 'query', + meta: { + alias: 'alias', + disabled: false, + negate: false, + index: '456', + }, + }, + ]; + searchSource.setField('filter', filter); + const { searchSourceJSON, references } = searchSource.serialize(); + expect(references[0].id).toEqual('123'); + expect(references[0].type).toEqual('index-pattern'); + expect(JSON.parse(searchSourceJSON).indexRefName).toEqual(references[0].name); + expect(references[1].id).toEqual('456'); + expect(references[1].type).toEqual('index-pattern'); + expect(JSON.parse(searchSourceJSON).filter[0].meta.indexRefName).toEqual(references[1].name); + }); + }); }); diff --git a/src/plugins/data/public/search/search_source/search_source.ts b/src/plugins/data/public/search/search_source/search_source.ts index 0c3321f03dabc..c70db7bb82ef7 100644 --- a/src/plugins/data/public/search/search_source/search_source.ts +++ b/src/plugins/data/public/search/search_source/search_source.ts @@ -70,6 +70,7 @@ */ import _ from 'lodash'; +import { SavedObjectReference } from 'kibana/public'; import { normalizeSortRequest } from './normalize_sort_request'; import { filterDocvalueFields } from './filter_docvalue_fields'; import { fieldWildcardFilter } from '../../../../kibana_utils/public'; @@ -419,4 +420,85 @@ export class SearchSource { return searchRequest; } + + /** + * Serializes the instance to a JSON string and a set of referenced objects. + * Use this method to get a representation of the search source which can be stored in a saved object. + * + * The references returned by this function can be mixed with other references in the same object, + * however make sure there are no name-collisions. The references will be named `kibanaSavedObjectMeta.searchSourceJSON.index` + * and `kibanaSavedObjectMeta.searchSourceJSON.filter[].meta.index`. + * + * Using `createSearchSource`, the instance can be re-created. + * @param searchSource The search source to serialize + * @public */ + public serialize() { + const references: SavedObjectReference[] = []; + + const { + filter: originalFilters, + ...searchSourceFields + }: Omit = _.omit(this.getFields(), ['sort', 'size']); + let serializedSearchSourceFields: Omit & { + indexRefName?: string; + filter?: Array & { meta: Filter['meta'] & { indexRefName?: string } }>; + } = searchSourceFields; + if (searchSourceFields.index) { + const indexId = searchSourceFields.index.id!; + const refName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; + references.push({ + name: refName, + type: 'index-pattern', + id: indexId, + }); + serializedSearchSourceFields = { + ...serializedSearchSourceFields, + indexRefName: refName, + index: undefined, + }; + } + if (originalFilters) { + const filters = this.getFilters(originalFilters); + serializedSearchSourceFields = { + ...serializedSearchSourceFields, + filter: filters.map((filterRow, i) => { + if (!filterRow.meta || !filterRow.meta.index) { + return filterRow; + } + const refName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`; + references.push({ + name: refName, + type: 'index-pattern', + id: filterRow.meta.index, + }); + return { + ...filterRow, + meta: { + ...filterRow.meta, + indexRefName: refName, + index: undefined, + }, + }; + }), + }; + } + + return { searchSourceJSON: JSON.stringify(serializedSearchSourceFields), references }; + } + + private getFilters(filterField: SearchSourceFields['filter']): Filter[] { + if (!filterField) { + return []; + } + + if (Array.isArray(filterField)) { + return filterField; + } + + if (_.isFunction(filterField)) { + return this.getFilters(filterField()); + } + + return [filterField]; + } } diff --git a/src/plugins/data/public/search/types.ts b/src/plugins/data/public/search/types.ts index 03cbfa9f8ed84..ba6e44f47b75e 100644 --- a/src/plugins/data/public/search/types.ts +++ b/src/plugins/data/public/search/types.ts @@ -18,6 +18,7 @@ */ import { CoreStart } from 'kibana/public'; +import { createSearchSource } from './search_source'; import { SearchAggsSetup, SearchAggsStart, SearchAggsStartLegacy } from './aggs'; import { ISearch, ISearchGeneric } from './i_search'; import { TStrategyTypes } from './strategy_types'; @@ -89,5 +90,6 @@ export interface ISearchStart { aggs: SearchAggsStart; setInterceptor: (searchInterceptor: SearchInterceptor) => void; search: ISearchGeneric; + createSearchSource: ReturnType; __LEGACY: ISearchStartLegacy & SearchAggsStartLegacy; } diff --git a/src/plugins/saved_objects/public/plugin.ts b/src/plugins/saved_objects/public/plugin.ts index 0f5773c00283e..7927238e12066 100644 --- a/src/plugins/saved_objects/public/plugin.ts +++ b/src/plugins/saved_objects/public/plugin.ts @@ -39,6 +39,7 @@ export class SavedObjectsPublicPlugin SavedObjectClass: createSavedObjectClass({ indexPatterns: data.indexPatterns, savedObjectsClient: core.savedObjects.client, + search: data.search, chrome: core.chrome, overlays: core.overlays, }), diff --git a/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts b/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts index 2e965eaf1989b..9776887b6d741 100644 --- a/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts +++ b/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts @@ -18,9 +18,8 @@ */ import _ from 'lodash'; import { EsResponse, SavedObject, SavedObjectConfig } from '../../types'; -import { parseSearchSource } from './parse_search_source'; import { expandShorthand, SavedObjectNotFound } from '../../../../kibana_utils/public'; -import { IndexPattern } from '../../../../data/public'; +import { DataPublicPluginStart, IndexPattern } from '../../../../data/public'; /** * A given response of and ElasticSearch containing a plain saved object is applied to the given @@ -29,13 +28,13 @@ import { IndexPattern } from '../../../../data/public'; export async function applyESResp( resp: EsResponse, savedObject: SavedObject, - config: SavedObjectConfig + config: SavedObjectConfig, + createSearchSource: DataPublicPluginStart['search']['createSearchSource'] ) { const mapping = expandShorthand(config.mapping); const esType = config.type || ''; savedObject._source = _.cloneDeep(resp._source); const injectReferences = config.injectReferences; - const hydrateIndexPattern = savedObject.hydrateIndexPattern!; if (typeof resp.found === 'boolean' && !resp.found) { throw new SavedObjectNotFound(esType, savedObject.id || ''); } @@ -64,13 +63,34 @@ export async function applyESResp( _.assign(savedObject, savedObject._source); savedObject.lastSavedTitle = savedObject.title; - await parseSearchSource(savedObject, esType, meta.searchSourceJSON, resp.references); - await hydrateIndexPattern(); + if (config.searchSource) { + try { + savedObject.searchSource = await createSearchSource(meta.searchSourceJSON, resp.references); + } catch (error) { + if ( + error.constructor.name === 'SavedObjectNotFound' && + error.savedObjectType === 'index-pattern' + ) { + // if parsing the search source fails because the index pattern wasn't found, + // remember the reference - this is required for error handling on legacy imports + savedObject.unresolvedIndexPatternReference = { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + id: JSON.parse(meta.searchSourceJSON).index, + type: 'index-pattern', + }; + } + + throw error; + } + } + if (injectReferences && resp.references && resp.references.length > 0) { injectReferences(savedObject, resp.references); } + if (typeof config.afterESResp === 'function') { savedObject = await config.afterESResp(savedObject); } + return savedObject; } diff --git a/src/plugins/saved_objects/public/saved_object/helpers/build_saved_object.ts b/src/plugins/saved_objects/public/saved_object/helpers/build_saved_object.ts index b9043890e2775..e8faef4e9e040 100644 --- a/src/plugins/saved_objects/public/saved_object/helpers/build_saved_object.ts +++ b/src/plugins/saved_objects/public/saved_object/helpers/build_saved_object.ts @@ -81,7 +81,8 @@ export function buildSavedObject( */ savedObject.init = _.once(() => intializeSavedObject(savedObject, savedObjectsClient, config)); - savedObject.applyESResp = (resp: EsResponse) => applyESResp(resp, savedObject, config); + savedObject.applyESResp = (resp: EsResponse) => + applyESResp(resp, savedObject, config, services.search.createSearchSource); /** * Serialize this object diff --git a/src/plugins/saved_objects/public/saved_object/helpers/hydrate_index_pattern.ts b/src/plugins/saved_objects/public/saved_object/helpers/hydrate_index_pattern.ts index b55538e4073ba..84275cf35befb 100644 --- a/src/plugins/saved_objects/public/saved_object/helpers/hydrate_index_pattern.ts +++ b/src/plugins/saved_objects/public/saved_object/helpers/hydrate_index_pattern.ts @@ -31,25 +31,19 @@ export async function hydrateIndexPattern( indexPatterns: IndexPatternsContract, config: SavedObjectConfig ) { - const clearSavedIndexPattern = !!config.clearSavedIndexPattern; const indexPattern = config.indexPattern; if (!savedObject.searchSource) { return null; } - if (clearSavedIndexPattern) { - savedObject.searchSource!.setField('index', undefined); - return null; - } - - const index = id || indexPattern || savedObject.searchSource!.getOwnField('index'); + const index = id || indexPattern || savedObject.searchSource.getOwnField('index'); if (typeof index !== 'string' || !index) { return null; } const indexObj = await indexPatterns.get(index); - savedObject.searchSource!.setField('index', indexObj); + savedObject.searchSource.setField('index', indexObj); return indexObj; } diff --git a/src/plugins/saved_objects/public/saved_object/helpers/parse_search_source.ts b/src/plugins/saved_objects/public/saved_object/helpers/parse_search_source.ts deleted file mode 100644 index cdb191f9e7df8..0000000000000 --- a/src/plugins/saved_objects/public/saved_object/helpers/parse_search_source.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import _ from 'lodash'; -import { migrateLegacyQuery } from '../../../../kibana_legacy/public'; -import { SavedObject } from '../../types'; -import { InvalidJSONProperty } from '../../../../kibana_utils/public'; - -export function parseSearchSource( - savedObject: SavedObject, - esType: string, - searchSourceJson: string, - references: any[] -) { - if (!savedObject.searchSource) return; - - // if we have a searchSource, set its values based on the searchSourceJson field - let searchSourceValues: Record; - try { - searchSourceValues = JSON.parse(searchSourceJson); - } catch (e) { - throw new InvalidJSONProperty( - `Invalid JSON in ${esType} "${savedObject.id}". ${e.message} JSON: ${searchSourceJson}` - ); - } - - // This detects a scenario where documents with invalid JSON properties have been imported into the saved object index. - // (This happened in issue #20308) - if (!searchSourceValues || typeof searchSourceValues !== 'object') { - throw new InvalidJSONProperty(`Invalid searchSourceJSON in ${esType} "${savedObject.id}".`); - } - - // Inject index id if a reference is saved - if (searchSourceValues.indexRefName) { - const reference = references.find( - (ref: Record) => ref.name === searchSourceValues.indexRefName - ); - if (!reference) { - throw new Error( - `Could not find reference for ${ - searchSourceValues.indexRefName - } on ${savedObject.getEsType()} ${savedObject.id}` - ); - } - searchSourceValues.index = reference.id; - delete searchSourceValues.indexRefName; - } - - if (searchSourceValues.filter) { - searchSourceValues.filter.forEach((filterRow: any) => { - if (!filterRow.meta || !filterRow.meta.indexRefName) { - return; - } - const reference = references.find((ref: any) => ref.name === filterRow.meta.indexRefName); - if (!reference) { - throw new Error( - `Could not find reference for ${ - filterRow.meta.indexRefName - } on ${savedObject.getEsType()}` - ); - } - filterRow.meta.index = reference.id; - delete filterRow.meta.indexRefName; - }); - } - - const searchSourceFields = savedObject.searchSource.getFields(); - const fnProps = _.transform( - searchSourceFields, - function(dynamic: Record, val: any, name: string | undefined) { - if (_.isFunction(val) && name) dynamic[name] = val; - }, - {} - ); - - savedObject.searchSource.setFields(_.defaults(searchSourceValues, fnProps)); - const query = savedObject.searchSource.getOwnField('query'); - - if (typeof query !== 'undefined') { - savedObject.searchSource.setField('query', migrateLegacyQuery(query)); - } -} diff --git a/src/plugins/saved_objects/public/saved_object/helpers/serialize_saved_object.ts b/src/plugins/saved_objects/public/saved_object/helpers/serialize_saved_object.ts index 8a020ca03aea3..78f9eeb8b5fb1 100644 --- a/src/plugins/saved_objects/public/saved_object/helpers/serialize_saved_object.ts +++ b/src/plugins/saved_objects/public/saved_object/helpers/serialize_saved_object.ts @@ -17,7 +17,6 @@ * under the License. */ import _ from 'lodash'; -import angular from 'angular'; import { SavedObject, SavedObjectConfig } from '../../types'; import { expandShorthand } from '../../../../kibana_utils/public'; @@ -41,57 +40,16 @@ export function serializeSavedObject(savedObject: SavedObject, config: SavedObje }); if (savedObject.searchSource) { - let searchSourceFields: Record = _.omit(savedObject.searchSource.getFields(), [ - 'sort', - 'size', - ]); - if (searchSourceFields.index) { - // searchSourceFields.index will normally be an IndexPattern, but can be a string in two scenarios: - // (1) `init()` (and by extension `hydrateIndexPattern()`) hasn't been called on Saved Object - // (2) The IndexPattern doesn't exist, so we fail to resolve it in `hydrateIndexPattern()` - const indexId = - typeof searchSourceFields.index === 'string' - ? searchSourceFields.index - : searchSourceFields.index.id; - const refName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; - references.push({ - name: refName, - type: 'index-pattern', - id: indexId, - }); - searchSourceFields = { - ...searchSourceFields, - indexRefName: refName, - index: undefined, - }; - } - if (searchSourceFields.filter) { - searchSourceFields = { - ...searchSourceFields, - filter: searchSourceFields.filter.map((filterRow: any, i: number) => { - if (!filterRow.meta || !filterRow.meta.index) { - return filterRow; - } - const refName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`; - references.push({ - name: refName, - type: 'index-pattern', - id: filterRow.meta.index, - }); - return { - ...filterRow, - meta: { - ...filterRow.meta, - indexRefName: refName, - index: undefined, - }, - }; - }), - }; - } - attributes.kibanaSavedObjectMeta = { - searchSourceJSON: angular.toJson(searchSourceFields), - }; + const { + searchSourceJSON, + references: searchSourceReferences, + } = savedObject.searchSource.serialize(); + attributes.kibanaSavedObjectMeta = { searchSourceJSON }; + references.push(...searchSourceReferences); + } + + if (savedObject.unresolvedIndexPatternReference) { + references.push(savedObject.unresolvedIndexPatternReference); } return { attributes, references }; diff --git a/src/plugins/saved_objects/public/saved_object/saved_object.test.ts b/src/plugins/saved_objects/public/saved_object/saved_object.test.ts index 08389e9e3c97f..60c66f84080b2 100644 --- a/src/plugins/saved_objects/public/saved_object/saved_object.test.ts +++ b/src/plugins/saved_objects/public/saved_object/saved_object.test.ts @@ -103,9 +103,11 @@ describe('Saved Object', () => { } beforeEach(() => { + (dataStartMock.search.createSearchSource as jest.Mock).mockReset(); SavedObjectClass = createSavedObjectClass({ savedObjectsClient: savedObjectsClientStub, indexPatterns: dataStartMock.indexPatterns, + search: dataStartMock.search, } as SavedObjectKibanaServices); }); @@ -269,7 +271,7 @@ describe('Saved Object', () => { ); }); - it('when index exists in searchSourceJSON', () => { + it('when search source references saved object', () => { const id = '123'; stubESResponse(getMockedDocResponse(id)); return createInitializedSavedObject({ type: 'dashboard', searchSource: true }).then( @@ -409,18 +411,17 @@ describe('Saved Object', () => { }); }); - it('throws error invalid JSON is detected', async () => { + it('forwards thrown exceptions from createSearchSource', async () => { + (dataStartMock.search.createSearchSource as jest.Mock).mockImplementation(() => { + throw new InvalidJSONProperty(''); + }); const savedObject = await createInitializedSavedObject({ type: 'dashboard', searchSource: true, }); const response = { found: true, - _source: { - kibanaSavedObjectMeta: { - searchSourceJSON: '"{\\n \\"filter\\": []\\n}"', - }, - }, + _source: {}, }; try { @@ -586,23 +587,24 @@ describe('Saved Object', () => { }); }); - it('injects references from searchSourceJSON', async () => { + it('passes references to search source parsing function', async () => { const savedObject = new SavedObjectClass({ type: 'dashboard', searchSource: true }); return savedObject.init!().then(() => { + const searchSourceJSON = JSON.stringify({ + indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index', + filter: [ + { + meta: { + indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index', + }, + }, + ], + }); const response = { found: true, _source: { kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index', - filter: [ - { - meta: { - indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index', - }, - }, - ], - }), + searchSourceJSON, }, }, references: [ @@ -619,16 +621,10 @@ describe('Saved Object', () => { ], }; savedObject.applyESResp(response); - expect(savedObject.searchSource!.getFields()).toEqual({ - index: 'my-index-1', - filter: [ - { - meta: { - index: 'my-index-2', - }, - }, - ], - }); + expect(dataStartMock.search.createSearchSource).toBeCalledWith( + searchSourceJSON, + response.references + ); }); }); }); diff --git a/src/plugins/saved_objects/public/types.ts b/src/plugins/saved_objects/public/types.ts index 99088df84ec36..3184038040952 100644 --- a/src/plugins/saved_objects/public/types.ts +++ b/src/plugins/saved_objects/public/types.ts @@ -24,7 +24,12 @@ import { SavedObjectAttributes, SavedObjectReference, } from 'kibana/public'; -import { IIndexPattern, IndexPatternsContract, ISearchSource } from '../../data/public'; +import { + DataPublicPluginStart, + IIndexPattern, + IndexPatternsContract, + ISearchSource, +} from '../../data/public'; export interface SavedObject { _serialize: () => { attributes: SavedObjectAttributes; references: SavedObjectReference[] }; @@ -49,6 +54,7 @@ export interface SavedObject { searchSource?: ISearchSource; showInRecentlyAccessed: boolean; title: string; + unresolvedIndexPatternReference?: SavedObjectReference; } export interface SavedObjectSaveOpts { @@ -65,6 +71,7 @@ export interface SavedObjectCreationOpts { export interface SavedObjectKibanaServices { savedObjectsClient: SavedObjectsClientContract; indexPatterns: IndexPatternsContract; + search: DataPublicPluginStart['search']; chrome: ChromeStart; overlays: OverlayStart; } @@ -72,7 +79,6 @@ export interface SavedObjectKibanaServices { export interface SavedObjectConfig { // is only used by visualize afterESResp?: (savedObject: SavedObject) => Promise; - clearSavedIndexPattern?: boolean; defaults?: any; extractReferences?: (opts: { attributes: SavedObjectAttributes; diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index 216defcee9016..8fcb84b19a9be 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -26,6 +26,7 @@ import { setCapabilities, setHttp, setIndexPatterns, + setSearch, setSavedObjects, setUsageCollector, setFilterManager, @@ -140,6 +141,7 @@ export class VisualizationsPlugin setHttp(core.http); setSavedObjects(core.savedObjects); setIndexPatterns(data.indexPatterns); + setSearch(data.search); setFilterManager(data.query.filterManager); setExpressions(expressions); setUiActions(uiActions); @@ -150,6 +152,7 @@ export class VisualizationsPlugin const savedVisualizationsLoader = createSavedVisLoader({ savedObjectsClient: core.savedObjects.client, indexPatterns: data.indexPatterns, + search: data.search, chrome: core.chrome, overlays: core.overlays, visualizationTypes: types, diff --git a/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts b/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts index bc96e08f4b9da..c99c7a4c2caa1 100644 --- a/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts +++ b/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts @@ -35,7 +35,7 @@ import { extractReferences, injectReferences } from './saved_visualization_refer import { IIndexPattern, ISearchSource, SearchSource } from '../../../../plugins/data/public'; import { ISavedVis, SerializedVis } from '../types'; import { createSavedSearchesLoader } from '../../../../plugins/discover/public'; -import { getChrome, getOverlays, getIndexPatterns, getSavedObjects } from '../services'; +import { getChrome, getOverlays, getIndexPatterns, getSavedObjects, getSearch } from '../services'; export const convertToSerializedVis = async (savedVis: ISavedVis): Promise => { const { visState } = savedVis; @@ -87,6 +87,7 @@ const getSearchSource = async (inputSearchSource: ISearchSource, savedSearchId?: const savedSearch = await createSavedSearchesLoader({ savedObjectsClient: getSavedObjects().client, indexPatterns: getIndexPatterns(), + search: getSearch(), chrome: getChrome(), overlays: getOverlays(), }).get(savedSearchId); diff --git a/src/plugins/visualizations/public/services.ts b/src/plugins/visualizations/public/services.ts index c4668fa4b0c79..618c61dff176a 100644 --- a/src/plugins/visualizations/public/services.ts +++ b/src/plugins/visualizations/public/services.ts @@ -63,6 +63,8 @@ export const [getIndexPatterns, setIndexPatterns] = createGetterSetter('Search'); + export const [getUsageCollector, setUsageCollector] = createGetterSetter( 'UsageCollection' ); diff --git a/x-pack/legacy/plugins/maps/public/angular/services/gis_map_saved_object_loader.js b/x-pack/legacy/plugins/maps/public/angular/services/gis_map_saved_object_loader.js index 252d602e8f564..bc636c0b200f8 100644 --- a/x-pack/legacy/plugins/maps/public/angular/services/gis_map_saved_object_loader.js +++ b/x-pack/legacy/plugins/maps/public/angular/services/gis_map_saved_object_loader.js @@ -17,6 +17,7 @@ module.service('gisMapSavedObjectLoader', function() { const services = { savedObjectsClient, indexPatterns: npStart.plugins.data.indexPatterns, + search: npStart.plugins.data.search, chrome: npStart.core.chrome, overlays: npStart.core.overlays, }; diff --git a/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts b/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts index f5f9e98fe659c..feff17b813112 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts @@ -29,6 +29,7 @@ export const useSearchItems = (defaultSavedObjectId: string | undefined) => { const savedSearches = createSavedSearchesLoader({ savedObjectsClient, indexPatterns, + search: appDeps.data.search, chrome: appDeps.chrome, overlays: appDeps.overlays, });