Move search source parsing and serializing to data (#59919)

This commit is contained in:
Joe Reuter 2020-04-09 14:06:01 +02:00 committed by GitHub
parent 530732c9dd
commit 8d21b6b6f3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 688 additions and 276 deletions

View file

@ -0,0 +1,15 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [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
<b>Signature:</b>
```typescript
createSearchSource: (indexPatterns: Pick<import("../../index_patterns/index_patterns").IndexPatternsService, "get" | "clearCache" | "getFieldsForTimePattern" | "getFieldsForWildcard" | "getIds" | "getTitles" | "getFields" | "getCache" | "getDefault" | "make">) => (searchSourceJson: string, references: SavedObjectReference[]) => Promise<SearchSource>
```

View file

@ -102,6 +102,7 @@
| [castEsToKbnFieldTypeName](./kibana-plugin-plugins-data-public.castestokbnfieldtypename.md) | Get the KbnFieldType name for an esType string | | [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 | | [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) | | | [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 <code>SearchSource</code> instance. Use this method to re-create the search source serialized using <code>searchSource.serialize</code>.<!-- -->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) | | | [ES\_SEARCH\_STRATEGY](./kibana-plugin-plugins-data-public.es_search_strategy.md) | |
| [esFilters](./kibana-plugin-plugins-data-public.esfilters.md) | | | [esFilters](./kibana-plugin-plugins-data-public.esfilters.md) | |
| [esKuery](./kibana-plugin-plugins-data-public.eskuery.md) | | | [esKuery](./kibana-plugin-plugins-data-public.eskuery.md) | |

View file

@ -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<!-- -->} | | [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) | | | | [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 | | [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 <code>kibanaSavedObjectMeta.searchSourceJSON.index</code> and <code>kibanaSavedObjectMeta.searchSourceJSON.filter[&lt;number&gt;].meta.index</code>.<!-- -->Using <code>createSearchSource</code>, the instance can be re-created. |
| [setField(field, value)](./kibana-plugin-plugins-data-public.searchsource.setfield.md) | | | | [setField(field, value)](./kibana-plugin-plugins-data-public.searchsource.setfield.md) | | |
| [setFields(newFields)](./kibana-plugin-plugins-data-public.searchsource.setfields.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 | | [setParent(parent, options)](./kibana-plugin-plugins-data-public.searchsource.setparent.md) | | Set a searchSource that this source should inherit from |

View file

@ -0,0 +1,27 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) &gt; [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[<number>].meta.index`<!-- -->.
Using `createSearchSource`<!-- -->, the instance can be re-created.
<b>Signature:</b>
```typescript
serialize(): {
searchSourceJSON: string;
references: SavedObjectReference[];
};
```
<b>Returns:</b>
`{
searchSourceJSON: string;
references: SavedObjectReference[];
}`

View file

@ -72,6 +72,7 @@ export async function buildServices(
const services = { const services = {
savedObjectsClient: core.savedObjects.client, savedObjectsClient: core.savedObjects.client,
indexPatterns: plugins.data.indexPatterns, indexPatterns: plugins.data.indexPatterns,
search: plugins.data.search,
chrome: core.chrome, chrome: core.chrome,
overlays: core.overlays, overlays: core.overlays,
}; };

View file

@ -56,6 +56,7 @@ export const savedObjectManagementRegistry: ISavedObjectsManagementRegistry = {
const services = { const services = {
savedObjectsClient: npStart.core.savedObjects.client, savedObjectsClient: npStart.core.savedObjects.client,
indexPatterns: npStart.plugins.data.indexPatterns, indexPatterns: npStart.plugins.data.indexPatterns,
search: npStart.plugins.data.search,
chrome: npStart.core.chrome, chrome: npStart.core.chrome,
overlays: npStart.core.overlays, overlays: npStart.core.overlays,
}; };

View file

@ -519,7 +519,8 @@ describe('Flyout', () => {
expect(resolveIndexPatternConflicts).toHaveBeenCalledWith( expect(resolveIndexPatternConflicts).toHaveBeenCalledWith(
component.instance().resolutions, component.instance().resolutions,
mockConflictedIndexPatterns, mockConflictedIndexPatterns,
true true,
defaultProps.indexPatterns
); );
expect(saveObjects).toHaveBeenCalledWith( expect(saveObjects).toHaveBeenCalledWith(
mockConflictedSavedObjectsLinkedToSavedSearches, mockConflictedSavedObjectsLinkedToSavedSearches,

View file

@ -358,7 +358,8 @@ export class Flyout extends Component {
importCount += await resolveIndexPatternConflicts( importCount += await resolveIndexPatternConflicts(
resolutions, resolutions,
conflictedIndexPatterns, conflictedIndexPatterns,
isOverwriteAllChecked isOverwriteAllChecked,
this.props.indexPatterns
); );
} }
this.setState({ this.setState({

View file

@ -84,7 +84,7 @@ describe('resolveSavedObjects', () => {
}, },
} as unknown) as IndexPatternsContract; } as unknown) as IndexPatternsContract;
const services = [ const services = ([
{ {
type: 'search', type: 'search',
get: async () => { get: async () => {
@ -124,7 +124,7 @@ describe('resolveSavedObjects', () => {
}; };
}, },
}, },
] as SavedObjectLoader[]; ] as unknown) as SavedObjectLoader[];
const overwriteAll = false; const overwriteAll = false;
@ -176,7 +176,7 @@ describe('resolveSavedObjects', () => {
}, },
} as unknown) as IndexPatternsContract; } as unknown) as IndexPatternsContract;
const services = [ const services = ([
{ {
type: 'search', type: 'search',
get: async () => { get: async () => {
@ -217,7 +217,7 @@ describe('resolveSavedObjects', () => {
}; };
}, },
}, },
] as SavedObjectLoader[]; ] as unknown) as SavedObjectLoader[];
const overwriteAll = false; const overwriteAll = false;
@ -237,33 +237,38 @@ describe('resolveSavedObjects', () => {
describe('resolveIndexPatternConflicts', () => { describe('resolveIndexPatternConflicts', () => {
it('should resave resolutions', async () => { it('should resave resolutions', async () => {
const hydrateIndexPattern = jest.fn();
const save = jest.fn(); const save = jest.fn();
const conflictedIndexPatterns = [ const conflictedIndexPatterns = ([
{ {
obj: { obj: {
searchSource: { save,
getOwnField: (field: string) => { },
return field === 'index' ? '1' : undefined; doc: {
_source: {
kibanaSavedObjectMeta: {
searchSourceJSON: JSON.stringify({
index: '1',
}),
}, },
}, },
hydrateIndexPattern,
save,
}, },
}, },
{ {
obj: { obj: {
searchSource: {
getOwnField: (field: string) => {
return field === 'index' ? '3' : undefined;
},
},
hydrateIndexPattern,
save, save,
}, },
doc: {
_source: {
kibanaSavedObjectMeta: {
searchSourceJSON: JSON.stringify({
index: '3',
}),
},
},
},
}, },
]; ] as unknown) as Array<{ obj: SavedObject; doc: any }>;
const resolutions = [ const resolutions = [
{ {
@ -282,43 +287,49 @@ describe('resolveSavedObjects', () => {
const overwriteAll = false; const overwriteAll = false;
await resolveIndexPatternConflicts(resolutions, conflictedIndexPatterns, overwriteAll); await resolveIndexPatternConflicts(resolutions, conflictedIndexPatterns, overwriteAll, ({
expect(hydrateIndexPattern.mock.calls.length).toBe(2); 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.mock.calls.length).toBe(2);
expect(save).toHaveBeenCalledWith({ confirmOverwrite: !overwriteAll }); expect(save).toHaveBeenCalledWith({ confirmOverwrite: !overwriteAll });
expect(hydrateIndexPattern).toHaveBeenCalledWith('2');
expect(hydrateIndexPattern).toHaveBeenCalledWith('4');
}); });
it('should resolve filter index conflicts', async () => { it('should resolve filter index conflicts', async () => {
const hydrateIndexPattern = jest.fn();
const save = jest.fn(); const save = jest.fn();
const conflictedIndexPatterns = [ const conflictedIndexPatterns = ([
{ {
obj: { obj: {
searchSource: {
getOwnField: (field: string) => {
return field === 'index' ? '1' : [{ meta: { index: 'filterIndex' } }];
},
setField: jest.fn(),
},
hydrateIndexPattern,
save, save,
}, },
doc: {
_source: {
kibanaSavedObjectMeta: {
searchSourceJSON: JSON.stringify({
index: '1',
filter: [{ meta: { index: 'filterIndex' } }],
}),
},
},
},
}, },
{ {
obj: { obj: {
searchSource: {
getOwnField: (field: string) => {
return field === 'index' ? '3' : undefined;
},
},
hydrateIndexPattern,
save, save,
}, },
doc: {
_source: {
kibanaSavedObjectMeta: {
searchSourceJSON: JSON.stringify({
index: '3',
}),
},
},
},
}, },
]; ] as unknown) as Array<{ obj: SavedObject; doc: any }>;
const resolutions = [ const resolutions = [
{ {
@ -337,9 +348,11 @@ describe('resolveSavedObjects', () => {
const overwriteAll = false; 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' } }, { meta: { index: 'newFilterIndex' } },
]); ]);
expect(save.mock.calls.length).toBe(2); expect(save.mock.calls.length).toBe(2);

View file

@ -18,12 +18,17 @@
*/ */
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { OverlayStart } from 'src/core/public'; import { cloneDeep } from 'lodash';
import { OverlayStart, SavedObjectReference } from 'src/core/public';
import { import {
SavedObject, SavedObject,
SavedObjectLoader, SavedObjectLoader,
} from '../../../../../../../../plugins/saved_objects/public'; } from '../../../../../../../../plugins/saved_objects/public';
import { IndexPatternsContract, IIndexPattern } from '../../../../../../../../plugins/data/public'; import {
IndexPatternsContract,
IIndexPattern,
createSearchSource,
} from '../../../../../../../../plugins/data/public';
type SavedObjectsRawDoc = Record<string, any>; type SavedObjectsRawDoc = Record<string, any>;
@ -126,7 +131,7 @@ async function importIndexPattern(
async function importDocument(obj: SavedObject, doc: SavedObjectsRawDoc, overwriteAll: boolean) { async function importDocument(obj: SavedObject, doc: SavedObjectsRawDoc, overwriteAll: boolean) {
await obj.applyESResp({ await obj.applyESResp({
references: doc._references || [], references: doc._references || [],
...doc, ...cloneDeep(doc),
}); });
return await obj.save({ confirmOverwrite: !overwriteAll }); return await obj.save({ confirmOverwrite: !overwriteAll });
} }
@ -160,41 +165,57 @@ async function awaitEachItemInParallel<T, R>(list: T[], op: (item: T) => R) {
export async function resolveIndexPatternConflicts( export async function resolveIndexPatternConflicts(
resolutions: Array<{ oldId: string; newId: string }>, resolutions: Array<{ oldId: string; newId: string }>,
conflictedIndexPatterns: any[], conflictedIndexPatterns: any[],
overwriteAll: boolean overwriteAll: boolean,
indexPatterns: IndexPatternsContract
) { ) {
let importCount = 0; let importCount = 0;
await awaitEachItemInParallel(conflictedIndexPatterns, async ({ obj }) => { await awaitEachItemInParallel(conflictedIndexPatterns, async ({ obj, doc }) => {
// Resolve search index reference: const serializedSearchSource = JSON.parse(
let oldIndexId = obj.searchSource.getOwnField('index'); doc._source.kibanaSavedObjectMeta?.searchSourceJSON || '{}'
// Depending on the object, this can either be the raw id or the actual index pattern object );
if (typeof oldIndexId !== 'string') { const oldIndexId = serializedSearchSource.index;
oldIndexId = oldIndexId.id; let allResolved = true;
} const inlineResolution = resolutions.find(({ oldId }) => oldId === oldIndexId);
let resolution = resolutions.find(({ oldId }) => oldId === oldIndexId); if (inlineResolution) {
if (resolution) { serializedSearchSource.index = inlineResolution.newId;
const newIndexId = resolution.newId; } else {
await obj.hydrateIndexPattern(newIndexId); allResolved = false;
} }
// Resolve filter index reference: // 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)) { if (!(f.meta && f.meta.index)) {
return f; 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; return resolution ? { ...f, ...{ meta: { ...f.meta, index: resolution.newId } } } : f;
}); });
if (filter.length > 0) { 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 // The user decided to skip this conflict so do nothing
return; return;
} }
obj.searchSource = await createSearchSource(indexPatterns)(
JSON.stringify(serializedSearchSource),
replacedReferences
);
if (await saveObject(obj, overwriteAll)) { if (await saveObject(obj, overwriteAll)) {
importCount++; importCount++;
} }

View file

@ -71,6 +71,7 @@ const getResolvedResults = deps => {
return createSavedSearchesLoader({ return createSavedSearchesLoader({
savedObjectsClient: core.savedObjects.client, savedObjectsClient: core.savedObjects.client,
indexPatterns: data.indexPatterns, indexPatterns: data.indexPatterns,
search: data.search,
chrome: core.chrome, chrome: core.chrome,
overlays: core.overlays, overlays: core.overlays,
}).get(results.vis.data.savedSearchId); }).get(results.vis.data.savedSearchId);

View file

@ -28,6 +28,7 @@ const savedObjectsClient = npStart.core.savedObjects.client;
const services = { const services = {
savedObjectsClient, savedObjectsClient,
indexPatterns: npStart.plugins.data.indexPatterns, indexPatterns: npStart.plugins.data.indexPatterns,
search: npStart.plugins.data.search,
chrome: npStart.core.chrome, chrome: npStart.core.chrome,
overlays: npStart.core.overlays, overlays: npStart.core.overlays,
}; };

View file

@ -72,9 +72,11 @@ export function setStartServices(npStart: NpStart) {
visualizationsServices.setAggs(npStart.plugins.data.search.aggs); visualizationsServices.setAggs(npStart.plugins.data.search.aggs);
visualizationsServices.setOverlays(npStart.core.overlays); visualizationsServices.setOverlays(npStart.core.overlays);
visualizationsServices.setChrome(npStart.core.chrome); visualizationsServices.setChrome(npStart.core.chrome);
visualizationsServices.setSearch(npStart.plugins.data.search);
const savedVisualizationsLoader = createSavedVisLoader({ const savedVisualizationsLoader = createSavedVisLoader({
savedObjectsClient: npStart.core.savedObjects.client, savedObjectsClient: npStart.core.savedObjects.client,
indexPatterns: npStart.plugins.data.indexPatterns, indexPatterns: npStart.plugins.data.indexPatterns,
search: npStart.plugins.data.search,
chrome: npStart.core.chrome, chrome: npStart.core.chrome,
overlays: npStart.core.overlays, overlays: npStart.core.overlays,
visualizationTypes: visualizationsServices.getTypes(), visualizationTypes: visualizationsServices.getTypes(),

View file

@ -284,7 +284,7 @@ export class DashboardPlugin
const { notifications } = core; const { notifications } = core;
const { const {
uiActions, uiActions,
data: { indexPatterns }, data: { indexPatterns, search },
} = plugins; } = plugins;
const SavedObjectFinder = getSavedObjectFinder(core.savedObjects, core.uiSettings); const SavedObjectFinder = getSavedObjectFinder(core.savedObjects, core.uiSettings);
@ -300,6 +300,7 @@ export class DashboardPlugin
const savedDashboardLoader = createSavedDashboardLoader({ const savedDashboardLoader = createSavedDashboardLoader({
savedObjectsClient: core.savedObjects.client, savedObjectsClient: core.savedObjects.client,
indexPatterns, indexPatterns,
search,
chrome: core.chrome, chrome: core.chrome,
overlays: core.overlays, overlays: core.overlays,
}); });

View file

@ -18,13 +18,14 @@
*/ */
import { SavedObjectsClientContract, ChromeStart, OverlayStart } from 'kibana/public'; 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 { SavedObjectLoader } from '../../../../plugins/saved_objects/public';
import { createSavedDashboardClass } from './saved_dashboard'; import { createSavedDashboardClass } from './saved_dashboard';
interface Services { interface Services {
savedObjectsClient: SavedObjectsClientContract; savedObjectsClient: SavedObjectsClientContract;
indexPatterns: IndexPatternsContract; indexPatterns: IndexPatternsContract;
search: DataPublicPluginStart['search'];
chrome: ChromeStart; chrome: ChromeStart;
overlays: OverlayStart; overlays: OverlayStart;
} }

View file

@ -366,6 +366,7 @@ export {
SearchStrategyProvider, SearchStrategyProvider,
ISearchSource, ISearchSource,
SearchSource, SearchSource,
createSearchSource,
SearchSourceFields, SearchSourceFields,
EsQuerySortValue, EsQuerySortValue,
SortDirection, SortDirection,

View file

@ -155,7 +155,7 @@ export class DataPublicPlugin implements Plugin<DataPublicPluginSetup, DataPubli
const query = this.queryService.start(savedObjects); const query = this.queryService.start(savedObjects);
setQueryService(query); setQueryService(query);
const search = this.searchService.start(core); const search = this.searchService.start(core, indexPatterns);
setSearchService(search); setSearchService(search);
uiActions.attachAction(APPLY_FILTER_TRIGGER, uiActions.getAction(ACTION_GLOBAL_APPLY_FILTER)); uiActions.attachAction(APPLY_FILTER_TRIGGER, uiActions.getAction(ACTION_GLOBAL_APPLY_FILTER));

View file

@ -45,6 +45,7 @@ import * as React_2 from 'react';
import { Required } from '@kbn/utility-types'; import { Required } from '@kbn/utility-types';
import * as Rx from 'rxjs'; import * as Rx from 'rxjs';
import { SavedObject as SavedObject_2 } from 'src/core/public'; import { SavedObject as SavedObject_2 } from 'src/core/public';
import { SavedObjectReference } from 'kibana/public';
import { SavedObjectsClientContract } from 'src/core/public'; import { SavedObjectsClientContract } from 'src/core/public';
import { SearchParams } from 'elasticsearch'; import { SearchParams } from 'elasticsearch';
import { SearchResponse as SearchResponse_2 } from 'elasticsearch'; import { SearchResponse as SearchResponse_2 } from 'elasticsearch';
@ -209,6 +210,9 @@ export const connectToQueryState: <S extends QueryState>({ timefilter: { timefil
// @public (undocumented) // @public (undocumented)
export const createSavedQueryService: (savedObjectsClient: Pick<import("../../../../../core/public").SavedObjectsClient, "update" | "find" | "get" | "delete" | "create" | "bulkCreate" | "bulkGet" | "bulkUpdate">) => SavedQueryService; export const createSavedQueryService: (savedObjectsClient: Pick<import("../../../../../core/public").SavedObjectsClient, "update" | "find" | "get" | "delete" | "create" | "bulkCreate" | "bulkGet" | "bulkUpdate">) => SavedQueryService;
// @public
export const createSearchSource: (indexPatterns: Pick<import("../../index_patterns/index_patterns").IndexPatternsService, "get" | "clearCache" | "getFieldsForTimePattern" | "getFieldsForWildcard" | "getIds" | "getTitles" | "getFields" | "getCache" | "getDefault" | "make">) => (searchSourceJson: string, references: SavedObjectReference[]) => Promise<SearchSource>;
// Warning: (ae-missing-release-tag) "CustomFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // 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) // @public (undocumented)
@ -1667,6 +1671,10 @@ export class SearchSource {
// (undocumented) // (undocumented)
history: SearchRequest[]; history: SearchRequest[];
onRequestStart(handler: (searchSource: ISearchSource, options?: FetchOptions) => Promise<unknown>): void; onRequestStart(handler: (searchSource: ISearchSource, options?: FetchOptions) => Promise<unknown>): void;
serialize(): {
searchSourceJSON: string;
references: SavedObjectReference[];
};
// (undocumented) // (undocumented)
setField<K extends keyof SearchSourceFields>(field: K, value: SearchSourceFields[K]): this; setField<K extends keyof SearchSourceFields>(field: K, value: SearchSourceFields[K]): this;
// (undocumented) // (undocumented)
@ -1881,21 +1889,21 @@ export type TSearchStrategyProvider<T extends TStrategyTypes> = (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 "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 "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: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: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: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: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: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: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: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: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: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 "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: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: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: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: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 "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 "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: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: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 "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: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: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 "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: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: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: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: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/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 // 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

View file

@ -54,6 +54,7 @@ export {
SearchSourceFields, SearchSourceFields,
EsQuerySortValue, EsQuerySortValue,
SortDirection, SortDirection,
createSearchSource,
} from './search_source'; } from './search_source';
export { SearchInterceptor } from './search_interceptor'; export { SearchInterceptor } from './search_interceptor';

View file

@ -33,6 +33,7 @@ export const searchStartMock: jest.Mocked<ISearchStart> = {
aggs: searchAggsStartMock(), aggs: searchAggsStartMock(),
setInterceptor: jest.fn(), setInterceptor: jest.fn(),
search: jest.fn(), search: jest.fn(),
createSearchSource: jest.fn(),
__LEGACY: { __LEGACY: {
AggConfig: jest.fn() as any, AggConfig: jest.fn() as any,
AggType: jest.fn(), AggType: jest.fn(),

View file

@ -25,6 +25,8 @@ import { TStrategyTypes } from './strategy_types';
import { getEsClient, LegacyApiCaller } from './es_client'; import { getEsClient, LegacyApiCaller } from './es_client';
import { ES_SEARCH_STRATEGY, DEFAULT_SEARCH_STRATEGY } from '../../common/search'; import { ES_SEARCH_STRATEGY, DEFAULT_SEARCH_STRATEGY } from '../../common/search';
import { esSearchStrategyProvider } from './es_search/es_search_strategy'; 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 { QuerySetup } from '../query/query_service';
import { GetInternalStartServicesFn } from '../types'; import { GetInternalStartServicesFn } from '../types';
import { SearchInterceptor } from './search_interceptor'; import { SearchInterceptor } from './search_interceptor';
@ -108,7 +110,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
}; };
} }
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 * 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. * all pending search requests, as well as getting the number of pending search requests.
@ -145,6 +147,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
// TODO: should an intercepror have a destroy method? // TODO: should an intercepror have a destroy method?
this.searchInterceptor = searchInterceptor; this.searchInterceptor = searchInterceptor;
}, },
createSearchSource: createSearchSource(indexPatterns),
__LEGACY: { __LEGACY: {
esClient: this.esClient!, esClient: this.esClient!,
AggConfig, AggConfig,

View file

@ -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<typeof createSearchSourceFactory>;
const indexPatternMock: IIndexPattern = {} as IIndexPattern;
let indexPatternContractMock: jest.Mocked<IndexPatternsContract>;
beforeEach(() => {
indexPatternContractMock = ({
get: jest.fn().mockReturnValue(Promise.resolve(indexPatternMock)),
} as unknown) as jest.Mocked<IndexPatternsContract>;
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',
});
});
});

View file

@ -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<string, unknown>;
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;
};

View file

@ -18,4 +18,5 @@
*/ */
export * from './search_source'; export * from './search_source';
export { createSearchSource } from './create_search_source';
export { SortDirection, EsQuerySortValue, SearchSourceFields } from './types'; export { SortDirection, EsQuerySortValue, SearchSourceFields } from './types';

View file

@ -37,4 +37,5 @@ export const searchSourceMock: MockedKeys<ISearchSource> = {
getSearchRequestBody: jest.fn(), getSearchRequestBody: jest.fn(),
destroy: jest.fn(), destroy: jest.fn(),
history: [], history: [],
serialize: jest.fn(),
}; };

View file

@ -18,7 +18,7 @@
*/ */
import { SearchSource } from './search_source'; import { SearchSource } from './search_source';
import { IndexPattern } from '../..'; import { IndexPattern, SortDirection } from '../..';
import { mockDataServices } from '../aggs/test_helpers'; import { mockDataServices } from '../aggs/test_helpers';
jest.mock('../fetch', () => ({ jest.mock('../fetch', () => ({
@ -150,4 +150,77 @@ describe('SearchSource', function() {
expect(parentFn).toBeCalledWith(searchSource, options); 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);
});
});
}); });

View file

@ -70,6 +70,7 @@
*/ */
import _ from 'lodash'; import _ from 'lodash';
import { SavedObjectReference } from 'kibana/public';
import { normalizeSortRequest } from './normalize_sort_request'; import { normalizeSortRequest } from './normalize_sort_request';
import { filterDocvalueFields } from './filter_docvalue_fields'; import { filterDocvalueFields } from './filter_docvalue_fields';
import { fieldWildcardFilter } from '../../../../kibana_utils/public'; import { fieldWildcardFilter } from '../../../../kibana_utils/public';
@ -419,4 +420,85 @@ export class SearchSource {
return searchRequest; 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[<number>].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<SearchSourceFields, 'sort' | 'size'> = _.omit(this.getFields(), ['sort', 'size']);
let serializedSearchSourceFields: Omit<SearchSourceFields, 'sort' | 'size' | 'filter'> & {
indexRefName?: string;
filter?: Array<Omit<Filter, 'meta'> & { 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];
}
} }

View file

@ -18,6 +18,7 @@
*/ */
import { CoreStart } from 'kibana/public'; import { CoreStart } from 'kibana/public';
import { createSearchSource } from './search_source';
import { SearchAggsSetup, SearchAggsStart, SearchAggsStartLegacy } from './aggs'; import { SearchAggsSetup, SearchAggsStart, SearchAggsStartLegacy } from './aggs';
import { ISearch, ISearchGeneric } from './i_search'; import { ISearch, ISearchGeneric } from './i_search';
import { TStrategyTypes } from './strategy_types'; import { TStrategyTypes } from './strategy_types';
@ -89,5 +90,6 @@ export interface ISearchStart {
aggs: SearchAggsStart; aggs: SearchAggsStart;
setInterceptor: (searchInterceptor: SearchInterceptor) => void; setInterceptor: (searchInterceptor: SearchInterceptor) => void;
search: ISearchGeneric; search: ISearchGeneric;
createSearchSource: ReturnType<typeof createSearchSource>;
__LEGACY: ISearchStartLegacy & SearchAggsStartLegacy; __LEGACY: ISearchStartLegacy & SearchAggsStartLegacy;
} }

View file

@ -39,6 +39,7 @@ export class SavedObjectsPublicPlugin
SavedObjectClass: createSavedObjectClass({ SavedObjectClass: createSavedObjectClass({
indexPatterns: data.indexPatterns, indexPatterns: data.indexPatterns,
savedObjectsClient: core.savedObjects.client, savedObjectsClient: core.savedObjects.client,
search: data.search,
chrome: core.chrome, chrome: core.chrome,
overlays: core.overlays, overlays: core.overlays,
}), }),

View file

@ -18,9 +18,8 @@
*/ */
import _ from 'lodash'; import _ from 'lodash';
import { EsResponse, SavedObject, SavedObjectConfig } from '../../types'; import { EsResponse, SavedObject, SavedObjectConfig } from '../../types';
import { parseSearchSource } from './parse_search_source';
import { expandShorthand, SavedObjectNotFound } from '../../../../kibana_utils/public'; 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 * 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( export async function applyESResp(
resp: EsResponse, resp: EsResponse,
savedObject: SavedObject, savedObject: SavedObject,
config: SavedObjectConfig config: SavedObjectConfig,
createSearchSource: DataPublicPluginStart['search']['createSearchSource']
) { ) {
const mapping = expandShorthand(config.mapping); const mapping = expandShorthand(config.mapping);
const esType = config.type || ''; const esType = config.type || '';
savedObject._source = _.cloneDeep(resp._source); savedObject._source = _.cloneDeep(resp._source);
const injectReferences = config.injectReferences; const injectReferences = config.injectReferences;
const hydrateIndexPattern = savedObject.hydrateIndexPattern!;
if (typeof resp.found === 'boolean' && !resp.found) { if (typeof resp.found === 'boolean' && !resp.found) {
throw new SavedObjectNotFound(esType, savedObject.id || ''); throw new SavedObjectNotFound(esType, savedObject.id || '');
} }
@ -64,13 +63,34 @@ export async function applyESResp(
_.assign(savedObject, savedObject._source); _.assign(savedObject, savedObject._source);
savedObject.lastSavedTitle = savedObject.title; savedObject.lastSavedTitle = savedObject.title;
await parseSearchSource(savedObject, esType, meta.searchSourceJSON, resp.references); if (config.searchSource) {
await hydrateIndexPattern(); 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) { if (injectReferences && resp.references && resp.references.length > 0) {
injectReferences(savedObject, resp.references); injectReferences(savedObject, resp.references);
} }
if (typeof config.afterESResp === 'function') { if (typeof config.afterESResp === 'function') {
savedObject = await config.afterESResp(savedObject); savedObject = await config.afterESResp(savedObject);
} }
return savedObject; return savedObject;
} }

View file

@ -81,7 +81,8 @@ export function buildSavedObject(
*/ */
savedObject.init = _.once(() => intializeSavedObject(savedObject, savedObjectsClient, config)); 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 * Serialize this object

View file

@ -31,25 +31,19 @@ export async function hydrateIndexPattern(
indexPatterns: IndexPatternsContract, indexPatterns: IndexPatternsContract,
config: SavedObjectConfig config: SavedObjectConfig
) { ) {
const clearSavedIndexPattern = !!config.clearSavedIndexPattern;
const indexPattern = config.indexPattern; const indexPattern = config.indexPattern;
if (!savedObject.searchSource) { if (!savedObject.searchSource) {
return null; return null;
} }
if (clearSavedIndexPattern) { const index = id || indexPattern || savedObject.searchSource.getOwnField('index');
savedObject.searchSource!.setField('index', undefined);
return null;
}
const index = id || indexPattern || savedObject.searchSource!.getOwnField('index');
if (typeof index !== 'string' || !index) { if (typeof index !== 'string' || !index) {
return null; return null;
} }
const indexObj = await indexPatterns.get(index); const indexObj = await indexPatterns.get(index);
savedObject.searchSource!.setField('index', indexObj); savedObject.searchSource.setField('index', indexObj);
return indexObj; return indexObj;
} }

View file

@ -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<string, any>;
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<string, any>) => 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<string, any>, 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));
}
}

View file

@ -17,7 +17,6 @@
* under the License. * under the License.
*/ */
import _ from 'lodash'; import _ from 'lodash';
import angular from 'angular';
import { SavedObject, SavedObjectConfig } from '../../types'; import { SavedObject, SavedObjectConfig } from '../../types';
import { expandShorthand } from '../../../../kibana_utils/public'; import { expandShorthand } from '../../../../kibana_utils/public';
@ -41,57 +40,16 @@ export function serializeSavedObject(savedObject: SavedObject, config: SavedObje
}); });
if (savedObject.searchSource) { if (savedObject.searchSource) {
let searchSourceFields: Record<string, any> = _.omit(savedObject.searchSource.getFields(), [ const {
'sort', searchSourceJSON,
'size', references: searchSourceReferences,
]); } = savedObject.searchSource.serialize();
if (searchSourceFields.index) { attributes.kibanaSavedObjectMeta = { searchSourceJSON };
// searchSourceFields.index will normally be an IndexPattern, but can be a string in two scenarios: references.push(...searchSourceReferences);
// (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 = if (savedObject.unresolvedIndexPatternReference) {
typeof searchSourceFields.index === 'string' references.push(savedObject.unresolvedIndexPatternReference);
? 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),
};
} }
return { attributes, references }; return { attributes, references };

View file

@ -103,9 +103,11 @@ describe('Saved Object', () => {
} }
beforeEach(() => { beforeEach(() => {
(dataStartMock.search.createSearchSource as jest.Mock).mockReset();
SavedObjectClass = createSavedObjectClass({ SavedObjectClass = createSavedObjectClass({
savedObjectsClient: savedObjectsClientStub, savedObjectsClient: savedObjectsClientStub,
indexPatterns: dataStartMock.indexPatterns, indexPatterns: dataStartMock.indexPatterns,
search: dataStartMock.search,
} as SavedObjectKibanaServices); } 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'; const id = '123';
stubESResponse(getMockedDocResponse(id)); stubESResponse(getMockedDocResponse(id));
return createInitializedSavedObject({ type: 'dashboard', searchSource: true }).then( 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({ const savedObject = await createInitializedSavedObject({
type: 'dashboard', type: 'dashboard',
searchSource: true, searchSource: true,
}); });
const response = { const response = {
found: true, found: true,
_source: { _source: {},
kibanaSavedObjectMeta: {
searchSourceJSON: '"{\\n \\"filter\\": []\\n}"',
},
},
}; };
try { 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 }); const savedObject = new SavedObjectClass({ type: 'dashboard', searchSource: true });
return savedObject.init!().then(() => { return savedObject.init!().then(() => {
const searchSourceJSON = JSON.stringify({
indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index',
filter: [
{
meta: {
indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index',
},
},
],
});
const response = { const response = {
found: true, found: true,
_source: { _source: {
kibanaSavedObjectMeta: { kibanaSavedObjectMeta: {
searchSourceJSON: JSON.stringify({ searchSourceJSON,
indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index',
filter: [
{
meta: {
indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index',
},
},
],
}),
}, },
}, },
references: [ references: [
@ -619,16 +621,10 @@ describe('Saved Object', () => {
], ],
}; };
savedObject.applyESResp(response); savedObject.applyESResp(response);
expect(savedObject.searchSource!.getFields()).toEqual({ expect(dataStartMock.search.createSearchSource).toBeCalledWith(
index: 'my-index-1', searchSourceJSON,
filter: [ response.references
{ );
meta: {
index: 'my-index-2',
},
},
],
});
}); });
}); });
}); });

View file

@ -24,7 +24,12 @@ import {
SavedObjectAttributes, SavedObjectAttributes,
SavedObjectReference, SavedObjectReference,
} from 'kibana/public'; } from 'kibana/public';
import { IIndexPattern, IndexPatternsContract, ISearchSource } from '../../data/public'; import {
DataPublicPluginStart,
IIndexPattern,
IndexPatternsContract,
ISearchSource,
} from '../../data/public';
export interface SavedObject { export interface SavedObject {
_serialize: () => { attributes: SavedObjectAttributes; references: SavedObjectReference[] }; _serialize: () => { attributes: SavedObjectAttributes; references: SavedObjectReference[] };
@ -49,6 +54,7 @@ export interface SavedObject {
searchSource?: ISearchSource; searchSource?: ISearchSource;
showInRecentlyAccessed: boolean; showInRecentlyAccessed: boolean;
title: string; title: string;
unresolvedIndexPatternReference?: SavedObjectReference;
} }
export interface SavedObjectSaveOpts { export interface SavedObjectSaveOpts {
@ -65,6 +71,7 @@ export interface SavedObjectCreationOpts {
export interface SavedObjectKibanaServices { export interface SavedObjectKibanaServices {
savedObjectsClient: SavedObjectsClientContract; savedObjectsClient: SavedObjectsClientContract;
indexPatterns: IndexPatternsContract; indexPatterns: IndexPatternsContract;
search: DataPublicPluginStart['search'];
chrome: ChromeStart; chrome: ChromeStart;
overlays: OverlayStart; overlays: OverlayStart;
} }
@ -72,7 +79,6 @@ export interface SavedObjectKibanaServices {
export interface SavedObjectConfig { export interface SavedObjectConfig {
// is only used by visualize // is only used by visualize
afterESResp?: (savedObject: SavedObject) => Promise<SavedObject>; afterESResp?: (savedObject: SavedObject) => Promise<SavedObject>;
clearSavedIndexPattern?: boolean;
defaults?: any; defaults?: any;
extractReferences?: (opts: { extractReferences?: (opts: {
attributes: SavedObjectAttributes; attributes: SavedObjectAttributes;

View file

@ -26,6 +26,7 @@ import {
setCapabilities, setCapabilities,
setHttp, setHttp,
setIndexPatterns, setIndexPatterns,
setSearch,
setSavedObjects, setSavedObjects,
setUsageCollector, setUsageCollector,
setFilterManager, setFilterManager,
@ -140,6 +141,7 @@ export class VisualizationsPlugin
setHttp(core.http); setHttp(core.http);
setSavedObjects(core.savedObjects); setSavedObjects(core.savedObjects);
setIndexPatterns(data.indexPatterns); setIndexPatterns(data.indexPatterns);
setSearch(data.search);
setFilterManager(data.query.filterManager); setFilterManager(data.query.filterManager);
setExpressions(expressions); setExpressions(expressions);
setUiActions(uiActions); setUiActions(uiActions);
@ -150,6 +152,7 @@ export class VisualizationsPlugin
const savedVisualizationsLoader = createSavedVisLoader({ const savedVisualizationsLoader = createSavedVisLoader({
savedObjectsClient: core.savedObjects.client, savedObjectsClient: core.savedObjects.client,
indexPatterns: data.indexPatterns, indexPatterns: data.indexPatterns,
search: data.search,
chrome: core.chrome, chrome: core.chrome,
overlays: core.overlays, overlays: core.overlays,
visualizationTypes: types, visualizationTypes: types,

View file

@ -35,7 +35,7 @@ import { extractReferences, injectReferences } from './saved_visualization_refer
import { IIndexPattern, ISearchSource, SearchSource } from '../../../../plugins/data/public'; import { IIndexPattern, ISearchSource, SearchSource } from '../../../../plugins/data/public';
import { ISavedVis, SerializedVis } from '../types'; import { ISavedVis, SerializedVis } from '../types';
import { createSavedSearchesLoader } from '../../../../plugins/discover/public'; 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<SerializedVis> => { export const convertToSerializedVis = async (savedVis: ISavedVis): Promise<SerializedVis> => {
const { visState } = savedVis; const { visState } = savedVis;
@ -87,6 +87,7 @@ const getSearchSource = async (inputSearchSource: ISearchSource, savedSearchId?:
const savedSearch = await createSavedSearchesLoader({ const savedSearch = await createSavedSearchesLoader({
savedObjectsClient: getSavedObjects().client, savedObjectsClient: getSavedObjects().client,
indexPatterns: getIndexPatterns(), indexPatterns: getIndexPatterns(),
search: getSearch(),
chrome: getChrome(), chrome: getChrome(),
overlays: getOverlays(), overlays: getOverlays(),
}).get(savedSearchId); }).get(savedSearchId);

View file

@ -63,6 +63,8 @@ export const [getIndexPatterns, setIndexPatterns] = createGetterSetter<IndexPatt
'IndexPatterns' 'IndexPatterns'
); );
export const [getSearch, setSearch] = createGetterSetter<DataPublicPluginStart['search']>('Search');
export const [getUsageCollector, setUsageCollector] = createGetterSetter<UsageCollectionSetup>( export const [getUsageCollector, setUsageCollector] = createGetterSetter<UsageCollectionSetup>(
'UsageCollection' 'UsageCollection'
); );

View file

@ -17,6 +17,7 @@ module.service('gisMapSavedObjectLoader', function() {
const services = { const services = {
savedObjectsClient, savedObjectsClient,
indexPatterns: npStart.plugins.data.indexPatterns, indexPatterns: npStart.plugins.data.indexPatterns,
search: npStart.plugins.data.search,
chrome: npStart.core.chrome, chrome: npStart.core.chrome,
overlays: npStart.core.overlays, overlays: npStart.core.overlays,
}; };

View file

@ -29,6 +29,7 @@ export const useSearchItems = (defaultSavedObjectId: string | undefined) => {
const savedSearches = createSavedSearchesLoader({ const savedSearches = createSavedSearchesLoader({
savedObjectsClient, savedObjectsClient,
indexPatterns, indexPatterns,
search: appDeps.data.search,
chrome: appDeps.chrome, chrome: appDeps.chrome,
overlays: appDeps.overlays, overlays: appDeps.overlays,
}); });