mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
Add KQL functionality in the find function of the saved objects (#41136)
* Add KQL functionality in the find function of the saved objects wip rename variable from KQL to filter, fix unit test + add new ones miss security pluggins review I fix api changes refactor after reviewing with Rudolf fix type review III review IV for security put back allowed logic back to return empty results remove StaticIndexPattern review V fix core_api_changes fix type * validate filter to match requirement type.attributes.key or type.savedObjectKey * Fix types * fix a bug + add more api integration test * fix types in test until we create package @kbn/types * fix type issue * fix api integration test * export nodeTypes from packages @kbn/es-query instead of the function buildNodeKuery * throw 400- bad request when validation error in find * fix type issue * accept api change * renove _ to represent private * fix unit test + add doc * add comment to explain why we removed the private
This commit is contained in:
parent
bc840e6789
commit
d95c47f776
42 changed files with 2459 additions and 152 deletions
|
@ -41,6 +41,11 @@ experimental[] Retrieve a paginated set of {kib} saved objects by various condit
|
|||
`has_reference`::
|
||||
(Optional, object) Filters to objects that have a relationship with the type and ID combination.
|
||||
|
||||
`filter`::
|
||||
(Optional, string) The filter is a KQL string with the caveat that if you filter with an attribute from your type saved object.
|
||||
It should look like that savedObjectType.attributes.title: "myTitle". However, If you used a direct attribute of a saved object like `updatedAt`,
|
||||
you will have to define your filter like that savedObjectType.updatedAt > 2018-12-22.
|
||||
|
||||
NOTE: As objects change in {kib}, the results on each page of the response also
|
||||
change. Use the find API for traditional paginated results, but avoid using it to export large amounts of data.
|
||||
|
||||
|
|
|
@ -9,5 +9,5 @@ Search for objects
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
find: <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "type" | "defaultSearchOperator" | "searchFields" | "sortField" | "hasReference" | "page" | "perPage" | "fields">) => Promise<SavedObjectsFindResponsePublic<T>>;
|
||||
find: <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "page" | "perPage" | "sortField" | "fields" | "searchFields" | "hasReference" | "defaultSearchOperator">) => Promise<SavedObjectsFindResponsePublic<T>>;
|
||||
```
|
||||
|
|
|
@ -20,7 +20,7 @@ export declare class SavedObjectsClient
|
|||
| [bulkGet](./kibana-plugin-public.savedobjectsclient.bulkget.md) | | <code>(objects?: {</code><br/><code> id: string;</code><br/><code> type: string;</code><br/><code> }[]) => Promise<SavedObjectsBatchResponse<SavedObjectAttributes>></code> | Returns an array of objects by id |
|
||||
| [create](./kibana-plugin-public.savedobjectsclient.create.md) | | <code><T extends SavedObjectAttributes>(type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise<SimpleSavedObject<T>></code> | Persists an object |
|
||||
| [delete](./kibana-plugin-public.savedobjectsclient.delete.md) | | <code>(type: string, id: string) => Promise<{}></code> | Deletes an object |
|
||||
| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <code><T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "type" | "defaultSearchOperator" | "searchFields" | "sortField" | "hasReference" | "page" | "perPage" | "fields">) => Promise<SavedObjectsFindResponsePublic<T>></code> | Search for objects |
|
||||
| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <code><T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "page" | "perPage" | "sortField" | "fields" | "searchFields" | "hasReference" | "defaultSearchOperator">) => Promise<SavedObjectsFindResponsePublic<T>></code> | Search for objects |
|
||||
| [get](./kibana-plugin-public.savedobjectsclient.get.md) | | <code><T extends SavedObjectAttributes>(type: string, id: string) => Promise<SimpleSavedObject<T>></code> | Fetches a single object |
|
||||
|
||||
## Methods
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) > [filter](./kibana-plugin-public.savedobjectsfindoptions.filter.md)
|
||||
|
||||
## SavedObjectsFindOptions.filter property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
filter?: string;
|
||||
```
|
|
@ -17,6 +17,7 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions
|
|||
| --- | --- | --- |
|
||||
| [defaultSearchOperator](./kibana-plugin-public.savedobjectsfindoptions.defaultsearchoperator.md) | <code>'AND' | 'OR'</code> | |
|
||||
| [fields](./kibana-plugin-public.savedobjectsfindoptions.fields.md) | <code>string[]</code> | An array of fields to include in the results |
|
||||
| [filter](./kibana-plugin-public.savedobjectsfindoptions.filter.md) | <code>string</code> | |
|
||||
| [hasReference](./kibana-plugin-public.savedobjectsfindoptions.hasreference.md) | <code>{</code><br/><code> type: string;</code><br/><code> id: string;</code><br/><code> }</code> | |
|
||||
| [page](./kibana-plugin-public.savedobjectsfindoptions.page.md) | <code>number</code> | |
|
||||
| [perPage](./kibana-plugin-public.savedobjectsfindoptions.perpage.md) | <code>number</code> | |
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsFindOptions](./kibana-plugin-server.savedobjectsfindoptions.md) > [filter](./kibana-plugin-server.savedobjectsfindoptions.filter.md)
|
||||
|
||||
## SavedObjectsFindOptions.filter property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
filter?: string;
|
||||
```
|
|
@ -17,6 +17,7 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions
|
|||
| --- | --- | --- |
|
||||
| [defaultSearchOperator](./kibana-plugin-server.savedobjectsfindoptions.defaultsearchoperator.md) | <code>'AND' | 'OR'</code> | |
|
||||
| [fields](./kibana-plugin-server.savedobjectsfindoptions.fields.md) | <code>string[]</code> | An array of fields to include in the results |
|
||||
| [filter](./kibana-plugin-server.savedobjectsfindoptions.filter.md) | <code>string</code> | |
|
||||
| [hasReference](./kibana-plugin-server.savedobjectsfindoptions.hasreference.md) | <code>{</code><br/><code> type: string;</code><br/><code> id: string;</code><br/><code> }</code> | |
|
||||
| [page](./kibana-plugin-server.savedobjectsfindoptions.page.md) | <code>number</code> | |
|
||||
| [perPage](./kibana-plugin-server.savedobjectsfindoptions.perpage.md) | <code>number</code> | |
|
||||
|
|
11
packages/kbn-es-query/src/kuery/ast/ast.d.ts
vendored
11
packages/kbn-es-query/src/kuery/ast/ast.d.ts
vendored
|
@ -17,6 +17,8 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { JsonObject } from '..';
|
||||
|
||||
/**
|
||||
* WARNING: these typings are incomplete
|
||||
*/
|
||||
|
@ -30,15 +32,6 @@ export interface KueryParseOptions {
|
|||
startRule: string;
|
||||
}
|
||||
|
||||
type JsonValue = null | boolean | number | string | JsonObject | JsonArray;
|
||||
|
||||
interface JsonObject {
|
||||
[key: string]: JsonValue;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface JsonArray extends Array<JsonValue> {}
|
||||
|
||||
export function fromKueryExpression(
|
||||
expression: string,
|
||||
parseOptions?: KueryParseOptions
|
||||
|
|
|
@ -32,7 +32,6 @@ export function buildNodeParams(fieldName, value, isPhrase = false) {
|
|||
if (_.isUndefined(value)) {
|
||||
throw new Error('value is a required argument');
|
||||
}
|
||||
|
||||
const fieldNode = typeof fieldName === 'string' ? ast.fromLiteralExpression(fieldName) : literal.buildNode(fieldName);
|
||||
const valueNode = typeof value === 'string' ? ast.fromLiteralExpression(value) : literal.buildNode(value);
|
||||
const isPhraseNode = literal.buildNode(isPhrase);
|
||||
|
@ -42,7 +41,7 @@ export function buildNodeParams(fieldName, value, isPhrase = false) {
|
|||
}
|
||||
|
||||
export function toElasticsearchQuery(node, indexPattern = null, config = {}) {
|
||||
const { arguments: [ fieldNameArg, valueArg, isPhraseArg ] } = node;
|
||||
const { arguments: [fieldNameArg, valueArg, isPhraseArg] } = node;
|
||||
const fieldName = ast.toElasticsearchQuery(fieldNameArg);
|
||||
const value = !_.isUndefined(valueArg) ? ast.toElasticsearchQuery(valueArg) : valueArg;
|
||||
const type = isPhraseArg.value ? 'phrase' : 'best_fields';
|
||||
|
|
10
packages/kbn-es-query/src/kuery/index.d.ts
vendored
10
packages/kbn-es-query/src/kuery/index.d.ts
vendored
|
@ -18,3 +18,13 @@
|
|||
*/
|
||||
|
||||
export * from './ast';
|
||||
export { nodeTypes } from './node_types';
|
||||
|
||||
export type JsonValue = null | boolean | number | string | JsonObject | JsonArray;
|
||||
|
||||
export interface JsonObject {
|
||||
[key: string]: JsonValue;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface JsonArray extends Array<JsonValue> {}
|
||||
|
|
|
@ -19,5 +19,5 @@
|
|||
|
||||
export * from './ast';
|
||||
export * from './filter_migration';
|
||||
export * from './node_types';
|
||||
export { nodeTypes } from './node_types';
|
||||
export * from './errors';
|
||||
|
|
76
packages/kbn-es-query/src/kuery/node_types/index.d.ts
vendored
Normal file
76
packages/kbn-es-query/src/kuery/node_types/index.d.ts
vendored
Normal file
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* WARNING: these typings are incomplete
|
||||
*/
|
||||
|
||||
import { JsonObject, JsonValue } from '..';
|
||||
|
||||
type FunctionName =
|
||||
| 'is'
|
||||
| 'and'
|
||||
| 'or'
|
||||
| 'not'
|
||||
| 'range'
|
||||
| 'exists'
|
||||
| 'geoBoundingBox'
|
||||
| 'geoPolygon';
|
||||
|
||||
interface FunctionTypeBuildNode {
|
||||
type: 'function';
|
||||
function: FunctionName;
|
||||
// TODO -> Need to define a better type for DSL query
|
||||
arguments: any[];
|
||||
}
|
||||
|
||||
interface FunctionType {
|
||||
buildNode: (functionName: FunctionName, ...args: any[]) => FunctionTypeBuildNode;
|
||||
buildNodeWithArgumentNodes: (functionName: FunctionName, ...args: any[]) => FunctionTypeBuildNode;
|
||||
toElasticsearchQuery: (node: any, indexPattern: any, config: JsonObject) => JsonValue;
|
||||
}
|
||||
|
||||
interface LiteralType {
|
||||
buildNode: (
|
||||
value: null | boolean | number | string
|
||||
) => { type: 'literal'; value: null | boolean | number | string };
|
||||
toElasticsearchQuery: (node: any) => null | boolean | number | string;
|
||||
}
|
||||
|
||||
interface NamedArgType {
|
||||
buildNode: (name: string, value: any) => { type: 'namedArg'; name: string; value: any };
|
||||
toElasticsearchQuery: (node: any) => string;
|
||||
}
|
||||
|
||||
interface WildcardType {
|
||||
buildNode: (value: string) => { type: 'wildcard'; value: string };
|
||||
test: (node: any, string: string) => boolean;
|
||||
toElasticsearchQuery: (node: any) => string;
|
||||
toQueryStringQuery: (node: any) => string;
|
||||
hasLeadingWildcard: (node: any) => boolean;
|
||||
}
|
||||
|
||||
interface NodeTypes {
|
||||
function: FunctionType;
|
||||
literal: LiteralType;
|
||||
namedArg: NamedArgType;
|
||||
wildcard: WildcardType;
|
||||
}
|
||||
|
||||
export const nodeTypes: NodeTypes;
|
|
@ -48,7 +48,7 @@ export class NotificationsService {
|
|||
public setup({ uiSettings }: SetupDeps): NotificationsSetup {
|
||||
const notificationSetup = { toasts: this.toasts.setup({ uiSettings }) };
|
||||
|
||||
this.uiSettingsErrorSubscription = uiSettings.getUpdateErrors$().subscribe(error => {
|
||||
this.uiSettingsErrorSubscription = uiSettings.getUpdateErrors$().subscribe((error: Error) => {
|
||||
notificationSetup.toasts.addDanger({
|
||||
title: i18n.translate('core.notifications.unableUpdateUISettingNotificationMessageTitle', {
|
||||
defaultMessage: 'Unable to update UI setting',
|
||||
|
|
|
@ -752,7 +752,7 @@ export class SavedObjectsClient {
|
|||
}[]) => Promise<SavedObjectsBatchResponse<SavedObjectAttributes>>;
|
||||
create: <T extends SavedObjectAttributes>(type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise<SimpleSavedObject<T>>;
|
||||
delete: (type: string, id: string) => Promise<{}>;
|
||||
find: <T extends SavedObjectAttributes>(options: Pick<SavedObjectsFindOptions, "search" | "type" | "defaultSearchOperator" | "searchFields" | "sortField" | "hasReference" | "page" | "perPage" | "fields">) => Promise<SavedObjectsFindResponsePublic<T>>;
|
||||
find: <T extends SavedObjectAttributes>(options: Pick<SavedObjectsFindOptions, "search" | "filter" | "type" | "page" | "perPage" | "sortField" | "fields" | "searchFields" | "hasReference" | "defaultSearchOperator">) => Promise<SavedObjectsFindResponsePublic<T>>;
|
||||
get: <T extends SavedObjectAttributes>(type: string, id: string) => Promise<SimpleSavedObject<T>>;
|
||||
update<T extends SavedObjectAttributes>(type: string, id: string, attributes: T, { version, migrationVersion, references }?: SavedObjectsUpdateOptions): Promise<SimpleSavedObject<T>>;
|
||||
}
|
||||
|
@ -775,6 +775,8 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions {
|
|||
defaultSearchOperator?: 'AND' | 'OR';
|
||||
fields?: string[];
|
||||
// (undocumented)
|
||||
filter?: string;
|
||||
// (undocumented)
|
||||
hasReference?: {
|
||||
type: string;
|
||||
id: string;
|
||||
|
|
|
@ -297,6 +297,7 @@ export class SavedObjectsClient {
|
|||
searchFields: 'search_fields',
|
||||
sortField: 'sort_field',
|
||||
type: 'type',
|
||||
filter: 'filter',
|
||||
};
|
||||
|
||||
const renamedQuery = renameKeys<SavedObjectsFindOptions, any>(renameMap, options);
|
||||
|
|
|
@ -56,6 +56,7 @@ export {
|
|||
SavedObjectsClientWrapperFactory,
|
||||
SavedObjectsClientWrapperOptions,
|
||||
SavedObjectsErrorHelpers,
|
||||
SavedObjectsCacheIndexPatterns,
|
||||
} from './lib';
|
||||
|
||||
export * from './saved_objects_client';
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* 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 { SavedObjectsCacheIndexPatterns } from './cache_index_patterns';
|
||||
|
||||
const mockGetFieldsForWildcard = jest.fn();
|
||||
const mockIndexPatternsService: jest.Mock = jest.fn().mockImplementation(() => ({
|
||||
getFieldsForWildcard: mockGetFieldsForWildcard,
|
||||
getFieldsForTimePattern: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('SavedObjectsRepository', () => {
|
||||
let cacheIndexPatterns: SavedObjectsCacheIndexPatterns;
|
||||
|
||||
const fields = [
|
||||
{
|
||||
aggregatable: true,
|
||||
name: 'config.type',
|
||||
searchable: true,
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
aggregatable: true,
|
||||
name: 'foo.type',
|
||||
searchable: true,
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
aggregatable: true,
|
||||
name: 'bar.type',
|
||||
searchable: true,
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
aggregatable: true,
|
||||
name: 'baz.type',
|
||||
searchable: true,
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
aggregatable: true,
|
||||
name: 'dashboard.otherField',
|
||||
searchable: true,
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
aggregatable: true,
|
||||
name: 'hiddenType.someField',
|
||||
searchable: true,
|
||||
type: 'string',
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
cacheIndexPatterns = new SavedObjectsCacheIndexPatterns();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('setIndexPatterns should return an error object when indexPatternsService is undefined', async () => {
|
||||
try {
|
||||
await cacheIndexPatterns.setIndexPatterns('test-index');
|
||||
} catch (error) {
|
||||
expect(error.message).toMatch('indexPatternsService is not defined');
|
||||
}
|
||||
});
|
||||
|
||||
it('setIndexPatterns should return an error object if getFieldsForWildcard is not defined', async () => {
|
||||
mockGetFieldsForWildcard.mockImplementation(() => {
|
||||
throw new Error('something happen');
|
||||
});
|
||||
try {
|
||||
cacheIndexPatterns.setIndexPatternsService(new mockIndexPatternsService());
|
||||
await cacheIndexPatterns.setIndexPatterns('test-index');
|
||||
} catch (error) {
|
||||
expect(error.message).toMatch('Index Pattern Error - something happen');
|
||||
}
|
||||
});
|
||||
|
||||
it('setIndexPatterns should return empty array when getFieldsForWildcard is returning null or undefined', async () => {
|
||||
mockGetFieldsForWildcard.mockImplementation(() => null);
|
||||
cacheIndexPatterns.setIndexPatternsService(new mockIndexPatternsService());
|
||||
await cacheIndexPatterns.setIndexPatterns('test-index');
|
||||
expect(cacheIndexPatterns.getIndexPatterns()).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('setIndexPatterns should return index pattern when getFieldsForWildcard is returning fields', async () => {
|
||||
mockGetFieldsForWildcard.mockImplementation(() => fields);
|
||||
cacheIndexPatterns.setIndexPatternsService(new mockIndexPatternsService());
|
||||
await cacheIndexPatterns.setIndexPatterns('test-index');
|
||||
expect(cacheIndexPatterns.getIndexPatterns()).toEqual({ fields, title: 'test-index' });
|
||||
});
|
||||
});
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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 { FieldDescriptor } from 'src/legacy/server/index_patterns/service/index_patterns_service';
|
||||
import { IndexPatternsService } from 'src/legacy/server/index_patterns';
|
||||
|
||||
export interface SavedObjectsIndexPatternField {
|
||||
name: string;
|
||||
type: string;
|
||||
aggregatable: boolean;
|
||||
searchable: boolean;
|
||||
}
|
||||
|
||||
export interface SavedObjectsIndexPattern {
|
||||
fields: SavedObjectsIndexPatternField[];
|
||||
title: string;
|
||||
}
|
||||
|
||||
export class SavedObjectsCacheIndexPatterns {
|
||||
private _indexPatterns: SavedObjectsIndexPattern | undefined = undefined;
|
||||
private _indexPatternsService: IndexPatternsService | undefined = undefined;
|
||||
|
||||
public setIndexPatternsService(indexPatternsService: IndexPatternsService) {
|
||||
this._indexPatternsService = indexPatternsService;
|
||||
}
|
||||
|
||||
public getIndexPatternsService() {
|
||||
return this._indexPatternsService;
|
||||
}
|
||||
|
||||
public getIndexPatterns(): SavedObjectsIndexPattern | undefined {
|
||||
return this._indexPatterns;
|
||||
}
|
||||
|
||||
public async setIndexPatterns(index: string) {
|
||||
await this._getIndexPattern(index);
|
||||
}
|
||||
|
||||
private async _getIndexPattern(index: string) {
|
||||
try {
|
||||
if (this._indexPatternsService == null) {
|
||||
throw new TypeError('indexPatternsService is not defined');
|
||||
}
|
||||
const fieldsDescriptor: FieldDescriptor[] = await this._indexPatternsService.getFieldsForWildcard(
|
||||
{
|
||||
pattern: index,
|
||||
}
|
||||
);
|
||||
|
||||
this._indexPatterns =
|
||||
fieldsDescriptor && Array.isArray(fieldsDescriptor) && fieldsDescriptor.length > 0
|
||||
? {
|
||||
fields: fieldsDescriptor.map(field => ({
|
||||
aggregatable: field.aggregatable,
|
||||
name: field.name,
|
||||
searchable: field.searchable,
|
||||
type: field.type,
|
||||
})),
|
||||
title: index,
|
||||
}
|
||||
: undefined;
|
||||
} catch (err) {
|
||||
throw new Error(`Index Pattern Error - ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
457
src/core/server/saved_objects/service/lib/filter_utils.test.ts
Normal file
457
src/core/server/saved_objects/service/lib/filter_utils.test.ts
Normal file
|
@ -0,0 +1,457 @@
|
|||
/*
|
||||
* 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 { fromKueryExpression } from '@kbn/es-query';
|
||||
|
||||
import {
|
||||
validateFilterKueryNode,
|
||||
getSavedObjectTypeIndexPatterns,
|
||||
validateConvertFilterToKueryNode,
|
||||
} from './filter_utils';
|
||||
import { SavedObjectsIndexPattern } from './cache_index_patterns';
|
||||
|
||||
const mockIndexPatterns: SavedObjectsIndexPattern = {
|
||||
fields: [
|
||||
{
|
||||
name: 'updatedAt',
|
||||
type: 'date',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'foo.title',
|
||||
type: 'text',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'foo.description',
|
||||
type: 'text',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'foo.bytes',
|
||||
type: 'number',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'bar.foo',
|
||||
type: 'text',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'bar.description',
|
||||
type: 'text',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'hiddentype.description',
|
||||
type: 'text',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
],
|
||||
title: 'mock',
|
||||
};
|
||||
|
||||
describe('Filter Utils', () => {
|
||||
describe('#validateConvertFilterToKueryNode', () => {
|
||||
test('Validate a simple filter', () => {
|
||||
expect(
|
||||
validateConvertFilterToKueryNode(['foo'], 'foo.attributes.title: "best"', mockIndexPatterns)
|
||||
).toEqual(fromKueryExpression('foo.title: "best"'));
|
||||
});
|
||||
test('Assemble filter kuery node saved object attributes with one saved object type', () => {
|
||||
expect(
|
||||
validateConvertFilterToKueryNode(
|
||||
['foo'],
|
||||
'foo.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)',
|
||||
mockIndexPatterns
|
||||
)
|
||||
).toEqual(
|
||||
fromKueryExpression(
|
||||
'(type: foo and updatedAt: 5678654567) and foo.bytes > 1000 and foo.bytes < 8000 and foo.title: "best" and (foo.description: t* or foo.description :*)'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test('Assemble filter with one type kuery node saved object attributes with multiple saved object type', () => {
|
||||
expect(
|
||||
validateConvertFilterToKueryNode(
|
||||
['foo', 'bar'],
|
||||
'foo.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)',
|
||||
mockIndexPatterns
|
||||
)
|
||||
).toEqual(
|
||||
fromKueryExpression(
|
||||
'(type: foo and updatedAt: 5678654567) and foo.bytes > 1000 and foo.bytes < 8000 and foo.title: "best" and (foo.description: t* or foo.description :*)'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test('Assemble filter with two types kuery node saved object attributes with multiple saved object type', () => {
|
||||
expect(
|
||||
validateConvertFilterToKueryNode(
|
||||
['foo', 'bar'],
|
||||
'(bar.updatedAt: 5678654567 OR foo.updatedAt: 5678654567) and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or bar.attributes.description :*)',
|
||||
mockIndexPatterns
|
||||
)
|
||||
).toEqual(
|
||||
fromKueryExpression(
|
||||
'((type: bar and updatedAt: 5678654567) or (type: foo and updatedAt: 5678654567)) and foo.bytes > 1000 and foo.bytes < 8000 and foo.title: "best" and (foo.description: t* or bar.description :*)'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test('Lets make sure that we are throwing an exception if we get an error', () => {
|
||||
expect(() => {
|
||||
validateConvertFilterToKueryNode(
|
||||
['foo', 'bar'],
|
||||
'updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)',
|
||||
mockIndexPatterns
|
||||
);
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"This key 'updatedAt' need to be wrapped by a saved object type like foo,bar: Bad Request"`
|
||||
);
|
||||
});
|
||||
|
||||
test('Lets make sure that we are throwing an exception if we are using hiddentype with types', () => {
|
||||
expect(() => {
|
||||
validateConvertFilterToKueryNode([], 'hiddentype.title: "title"', mockIndexPatterns);
|
||||
}).toThrowErrorMatchingInlineSnapshot(`"This type hiddentype is not allowed: Bad Request"`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#validateFilterKueryNode', () => {
|
||||
test('Validate filter query through KueryNode - happy path', () => {
|
||||
const validationObject = validateFilterKueryNode(
|
||||
fromKueryExpression(
|
||||
'foo.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)'
|
||||
),
|
||||
['foo'],
|
||||
getSavedObjectTypeIndexPatterns(['foo'], mockIndexPatterns)
|
||||
);
|
||||
|
||||
expect(validationObject).toEqual([
|
||||
{
|
||||
astPath: 'arguments.0',
|
||||
error: null,
|
||||
isSavedObjectAttr: true,
|
||||
key: 'foo.updatedAt',
|
||||
type: 'foo',
|
||||
},
|
||||
{
|
||||
astPath: 'arguments.1.arguments.0',
|
||||
error: null,
|
||||
isSavedObjectAttr: false,
|
||||
key: 'foo.attributes.bytes',
|
||||
type: 'foo',
|
||||
},
|
||||
{
|
||||
astPath: 'arguments.1.arguments.1.arguments.0',
|
||||
error: null,
|
||||
isSavedObjectAttr: false,
|
||||
key: 'foo.attributes.bytes',
|
||||
type: 'foo',
|
||||
},
|
||||
{
|
||||
astPath: 'arguments.1.arguments.1.arguments.1.arguments.0',
|
||||
error: null,
|
||||
isSavedObjectAttr: false,
|
||||
key: 'foo.attributes.title',
|
||||
type: 'foo',
|
||||
},
|
||||
{
|
||||
astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.0',
|
||||
error: null,
|
||||
isSavedObjectAttr: false,
|
||||
key: 'foo.attributes.description',
|
||||
type: 'foo',
|
||||
},
|
||||
{
|
||||
astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.1',
|
||||
error: null,
|
||||
isSavedObjectAttr: false,
|
||||
key: 'foo.attributes.description',
|
||||
type: 'foo',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('Return Error if key is not wrapper by a saved object type', () => {
|
||||
const validationObject = validateFilterKueryNode(
|
||||
fromKueryExpression(
|
||||
'updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)'
|
||||
),
|
||||
['foo'],
|
||||
getSavedObjectTypeIndexPatterns(['foo'], mockIndexPatterns)
|
||||
);
|
||||
|
||||
expect(validationObject).toEqual([
|
||||
{
|
||||
astPath: 'arguments.0',
|
||||
error: "This key 'updatedAt' need to be wrapped by a saved object type like foo",
|
||||
isSavedObjectAttr: true,
|
||||
key: 'updatedAt',
|
||||
type: null,
|
||||
},
|
||||
{
|
||||
astPath: 'arguments.1.arguments.0',
|
||||
error: null,
|
||||
isSavedObjectAttr: false,
|
||||
key: 'foo.attributes.bytes',
|
||||
type: 'foo',
|
||||
},
|
||||
{
|
||||
astPath: 'arguments.1.arguments.1.arguments.0',
|
||||
error: null,
|
||||
isSavedObjectAttr: false,
|
||||
key: 'foo.attributes.bytes',
|
||||
type: 'foo',
|
||||
},
|
||||
{
|
||||
astPath: 'arguments.1.arguments.1.arguments.1.arguments.0',
|
||||
error: null,
|
||||
isSavedObjectAttr: false,
|
||||
key: 'foo.attributes.title',
|
||||
type: 'foo',
|
||||
},
|
||||
{
|
||||
astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.0',
|
||||
error: null,
|
||||
isSavedObjectAttr: false,
|
||||
key: 'foo.attributes.description',
|
||||
type: 'foo',
|
||||
},
|
||||
{
|
||||
astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.1',
|
||||
error: null,
|
||||
isSavedObjectAttr: false,
|
||||
key: 'foo.attributes.description',
|
||||
type: 'foo',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('Return Error if key of a saved object type is not wrapped with attributes', () => {
|
||||
const validationObject = validateFilterKueryNode(
|
||||
fromKueryExpression(
|
||||
'foo.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.description :*)'
|
||||
),
|
||||
['foo'],
|
||||
getSavedObjectTypeIndexPatterns(['foo'], mockIndexPatterns)
|
||||
);
|
||||
|
||||
expect(validationObject).toEqual([
|
||||
{
|
||||
astPath: 'arguments.0',
|
||||
error: null,
|
||||
isSavedObjectAttr: true,
|
||||
key: 'foo.updatedAt',
|
||||
type: 'foo',
|
||||
},
|
||||
{
|
||||
astPath: 'arguments.1.arguments.0',
|
||||
error: null,
|
||||
isSavedObjectAttr: false,
|
||||
key: 'foo.attributes.bytes',
|
||||
type: 'foo',
|
||||
},
|
||||
{
|
||||
astPath: 'arguments.1.arguments.1.arguments.0',
|
||||
error:
|
||||
"This key 'foo.bytes' does NOT match the filter proposition SavedObjectType.attributes.key",
|
||||
isSavedObjectAttr: false,
|
||||
key: 'foo.bytes',
|
||||
type: 'foo',
|
||||
},
|
||||
{
|
||||
astPath: 'arguments.1.arguments.1.arguments.1.arguments.0',
|
||||
error: null,
|
||||
isSavedObjectAttr: false,
|
||||
key: 'foo.attributes.title',
|
||||
type: 'foo',
|
||||
},
|
||||
{
|
||||
astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.0',
|
||||
error: null,
|
||||
isSavedObjectAttr: false,
|
||||
key: 'foo.attributes.description',
|
||||
type: 'foo',
|
||||
},
|
||||
{
|
||||
astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.1',
|
||||
error:
|
||||
"This key 'foo.description' does NOT match the filter proposition SavedObjectType.attributes.key",
|
||||
isSavedObjectAttr: false,
|
||||
key: 'foo.description',
|
||||
type: 'foo',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('Return Error if filter is not using an allowed type', () => {
|
||||
const validationObject = validateFilterKueryNode(
|
||||
fromKueryExpression(
|
||||
'bar.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)'
|
||||
),
|
||||
['foo'],
|
||||
getSavedObjectTypeIndexPatterns(['foo'], mockIndexPatterns)
|
||||
);
|
||||
|
||||
expect(validationObject).toEqual([
|
||||
{
|
||||
astPath: 'arguments.0',
|
||||
error: 'This type bar is not allowed',
|
||||
isSavedObjectAttr: true,
|
||||
key: 'bar.updatedAt',
|
||||
type: 'bar',
|
||||
},
|
||||
{
|
||||
astPath: 'arguments.1.arguments.0',
|
||||
error: null,
|
||||
isSavedObjectAttr: false,
|
||||
key: 'foo.attributes.bytes',
|
||||
type: 'foo',
|
||||
},
|
||||
{
|
||||
astPath: 'arguments.1.arguments.1.arguments.0',
|
||||
error: null,
|
||||
isSavedObjectAttr: false,
|
||||
key: 'foo.attributes.bytes',
|
||||
type: 'foo',
|
||||
},
|
||||
{
|
||||
astPath: 'arguments.1.arguments.1.arguments.1.arguments.0',
|
||||
error: null,
|
||||
isSavedObjectAttr: false,
|
||||
key: 'foo.attributes.title',
|
||||
type: 'foo',
|
||||
},
|
||||
{
|
||||
astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.0',
|
||||
error: null,
|
||||
isSavedObjectAttr: false,
|
||||
key: 'foo.attributes.description',
|
||||
type: 'foo',
|
||||
},
|
||||
{
|
||||
astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.1',
|
||||
error: null,
|
||||
isSavedObjectAttr: false,
|
||||
key: 'foo.attributes.description',
|
||||
type: 'foo',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('Return Error if filter is using an non-existing key in the index patterns of the saved object type', () => {
|
||||
const validationObject = validateFilterKueryNode(
|
||||
fromKueryExpression(
|
||||
'foo.updatedAt33: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.header: "best" and (foo.attributes.description: t* or foo.attributes.description :*)'
|
||||
),
|
||||
['foo'],
|
||||
getSavedObjectTypeIndexPatterns(['foo'], mockIndexPatterns)
|
||||
);
|
||||
|
||||
expect(validationObject).toEqual([
|
||||
{
|
||||
astPath: 'arguments.0',
|
||||
error: "This key 'foo.updatedAt33' does NOT exist in foo saved object index patterns",
|
||||
isSavedObjectAttr: false,
|
||||
key: 'foo.updatedAt33',
|
||||
type: 'foo',
|
||||
},
|
||||
{
|
||||
astPath: 'arguments.1.arguments.0',
|
||||
error: null,
|
||||
isSavedObjectAttr: false,
|
||||
key: 'foo.attributes.bytes',
|
||||
type: 'foo',
|
||||
},
|
||||
{
|
||||
astPath: 'arguments.1.arguments.1.arguments.0',
|
||||
error: null,
|
||||
isSavedObjectAttr: false,
|
||||
key: 'foo.attributes.bytes',
|
||||
type: 'foo',
|
||||
},
|
||||
{
|
||||
astPath: 'arguments.1.arguments.1.arguments.1.arguments.0',
|
||||
error:
|
||||
"This key 'foo.attributes.header' does NOT exist in foo saved object index patterns",
|
||||
isSavedObjectAttr: false,
|
||||
key: 'foo.attributes.header',
|
||||
type: 'foo',
|
||||
},
|
||||
{
|
||||
astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.0',
|
||||
error: null,
|
||||
isSavedObjectAttr: false,
|
||||
key: 'foo.attributes.description',
|
||||
type: 'foo',
|
||||
},
|
||||
{
|
||||
astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.1',
|
||||
error: null,
|
||||
isSavedObjectAttr: false,
|
||||
key: 'foo.attributes.description',
|
||||
type: 'foo',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getSavedObjectTypeIndexPatterns', () => {
|
||||
test('Get index patterns related to your type', () => {
|
||||
const indexPatternsFilterByType = getSavedObjectTypeIndexPatterns(['foo'], mockIndexPatterns);
|
||||
|
||||
expect(indexPatternsFilterByType).toEqual([
|
||||
{
|
||||
name: 'updatedAt',
|
||||
type: 'date',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'foo.title',
|
||||
type: 'text',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'foo.description',
|
||||
type: 'text',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'foo.bytes',
|
||||
type: 'number',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
190
src/core/server/saved_objects/service/lib/filter_utils.ts
Normal file
190
src/core/server/saved_objects/service/lib/filter_utils.ts
Normal file
|
@ -0,0 +1,190 @@
|
|||
/*
|
||||
* 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 { fromKueryExpression, KueryNode, nodeTypes } from '@kbn/es-query';
|
||||
import { get, set } from 'lodash';
|
||||
|
||||
import { SavedObjectsIndexPattern, SavedObjectsIndexPatternField } from './cache_index_patterns';
|
||||
import { SavedObjectsErrorHelpers } from './errors';
|
||||
|
||||
export const validateConvertFilterToKueryNode = (
|
||||
types: string[],
|
||||
filter: string,
|
||||
indexPattern: SavedObjectsIndexPattern | undefined
|
||||
): KueryNode => {
|
||||
if (filter && filter.length > 0 && indexPattern) {
|
||||
const filterKueryNode = fromKueryExpression(filter);
|
||||
|
||||
const typeIndexPatterns = getSavedObjectTypeIndexPatterns(types, indexPattern);
|
||||
const validationFilterKuery = validateFilterKueryNode(
|
||||
filterKueryNode,
|
||||
types,
|
||||
typeIndexPatterns,
|
||||
filterKueryNode.type === 'function' && ['is', 'range'].includes(filterKueryNode.function)
|
||||
);
|
||||
|
||||
if (validationFilterKuery.length === 0) {
|
||||
throw SavedObjectsErrorHelpers.createBadRequestError(
|
||||
'If we have a filter options defined, we should always have validationFilterKuery defined too'
|
||||
);
|
||||
}
|
||||
|
||||
if (validationFilterKuery.some(obj => obj.error != null)) {
|
||||
throw SavedObjectsErrorHelpers.createBadRequestError(
|
||||
validationFilterKuery
|
||||
.filter(obj => obj.error != null)
|
||||
.map(obj => obj.error)
|
||||
.join('\n')
|
||||
);
|
||||
}
|
||||
|
||||
validationFilterKuery.forEach(item => {
|
||||
const path: string[] = item.astPath.length === 0 ? [] : item.astPath.split('.');
|
||||
const existingKueryNode: KueryNode =
|
||||
path.length === 0 ? filterKueryNode : get(filterKueryNode, path);
|
||||
if (item.isSavedObjectAttr) {
|
||||
existingKueryNode.arguments[0].value = existingKueryNode.arguments[0].value.split('.')[1];
|
||||
const itemType = types.filter(t => t === item.type);
|
||||
if (itemType.length === 1) {
|
||||
set(
|
||||
filterKueryNode,
|
||||
path,
|
||||
nodeTypes.function.buildNode('and', [
|
||||
nodeTypes.function.buildNode('is', 'type', itemType[0]),
|
||||
existingKueryNode,
|
||||
])
|
||||
);
|
||||
}
|
||||
} else {
|
||||
existingKueryNode.arguments[0].value = existingKueryNode.arguments[0].value.replace(
|
||||
'.attributes',
|
||||
''
|
||||
);
|
||||
set(filterKueryNode, path, existingKueryNode);
|
||||
}
|
||||
});
|
||||
return filterKueryNode;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getSavedObjectTypeIndexPatterns = (
|
||||
types: string[],
|
||||
indexPattern: SavedObjectsIndexPattern | undefined
|
||||
): SavedObjectsIndexPatternField[] => {
|
||||
return indexPattern != null
|
||||
? indexPattern.fields.filter(
|
||||
ip =>
|
||||
!ip.name.includes('.') || (ip.name.includes('.') && types.includes(ip.name.split('.')[0]))
|
||||
)
|
||||
: [];
|
||||
};
|
||||
|
||||
interface ValidateFilterKueryNode {
|
||||
astPath: string;
|
||||
error: string;
|
||||
isSavedObjectAttr: boolean;
|
||||
key: string;
|
||||
type: string | null;
|
||||
}
|
||||
|
||||
export const validateFilterKueryNode = (
|
||||
astFilter: KueryNode,
|
||||
types: string[],
|
||||
typeIndexPatterns: SavedObjectsIndexPatternField[],
|
||||
storeValue: boolean = false,
|
||||
path: string = 'arguments'
|
||||
): ValidateFilterKueryNode[] => {
|
||||
return astFilter.arguments.reduce((kueryNode: string[], ast: KueryNode, index: number) => {
|
||||
if (ast.arguments) {
|
||||
const myPath = `${path}.${index}`;
|
||||
return [
|
||||
...kueryNode,
|
||||
...validateFilterKueryNode(
|
||||
ast,
|
||||
types,
|
||||
typeIndexPatterns,
|
||||
ast.type === 'function' && ['is', 'range'].includes(ast.function),
|
||||
`${myPath}.arguments`
|
||||
),
|
||||
];
|
||||
}
|
||||
if (storeValue && index === 0) {
|
||||
const splitPath = path.split('.');
|
||||
return [
|
||||
...kueryNode,
|
||||
{
|
||||
astPath: splitPath.slice(0, splitPath.length - 1).join('.'),
|
||||
error: hasFilterKeyError(ast.value, types, typeIndexPatterns),
|
||||
isSavedObjectAttr: isSavedObjectAttr(ast.value, typeIndexPatterns),
|
||||
key: ast.value,
|
||||
type: getType(ast.value),
|
||||
},
|
||||
];
|
||||
}
|
||||
return kueryNode;
|
||||
}, []);
|
||||
};
|
||||
|
||||
const getType = (key: string) => (key.includes('.') ? key.split('.')[0] : null);
|
||||
|
||||
export const isSavedObjectAttr = (
|
||||
key: string,
|
||||
typeIndexPatterns: SavedObjectsIndexPatternField[]
|
||||
) => {
|
||||
const splitKey = key.split('.');
|
||||
if (splitKey.length === 1 && typeIndexPatterns.some(tip => tip.name === splitKey[0])) {
|
||||
return true;
|
||||
} else if (splitKey.length > 1 && typeIndexPatterns.some(tip => tip.name === splitKey[1])) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const hasFilterKeyError = (
|
||||
key: string,
|
||||
types: string[],
|
||||
typeIndexPatterns: SavedObjectsIndexPatternField[]
|
||||
): string | null => {
|
||||
if (!key.includes('.')) {
|
||||
return `This key '${key}' need to be wrapped by a saved object type like ${types.join()}`;
|
||||
} else if (key.includes('.')) {
|
||||
const keySplit = key.split('.');
|
||||
if (keySplit.length <= 1 || !types.includes(keySplit[0])) {
|
||||
return `This type ${keySplit[0]} is not allowed`;
|
||||
}
|
||||
if (
|
||||
(keySplit.length === 2 && typeIndexPatterns.some(tip => tip.name === key)) ||
|
||||
(keySplit.length > 2 && types.includes(keySplit[0]) && keySplit[1] !== 'attributes')
|
||||
) {
|
||||
return `This key '${key}' does NOT match the filter proposition SavedObjectType.attributes.key`;
|
||||
}
|
||||
if (
|
||||
(keySplit.length === 2 && !typeIndexPatterns.some(tip => tip.name === keySplit[1])) ||
|
||||
(keySplit.length > 2 &&
|
||||
!typeIndexPatterns.some(
|
||||
tip =>
|
||||
tip.name === [...keySplit.slice(0, 1), ...keySplit.slice(2, keySplit.length)].join('.')
|
||||
))
|
||||
) {
|
||||
return `This key '${key}' does NOT exist in ${types.join()} saved object index patterns`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
|
@ -26,3 +26,5 @@ export {
|
|||
} from './scoped_client_provider';
|
||||
|
||||
export { SavedObjectsErrorHelpers } from './errors';
|
||||
|
||||
export { SavedObjectsCacheIndexPatterns } from './cache_index_patterns';
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
*/
|
||||
|
||||
import { delay } from 'bluebird';
|
||||
|
||||
import { SavedObjectsRepository } from './repository';
|
||||
import * as getSearchDslNS from './search_dsl/search_dsl';
|
||||
import { SavedObjectsErrorHelpers } from './errors';
|
||||
|
@ -272,6 +273,10 @@ describe('SavedObjectsRepository', () => {
|
|||
|
||||
savedObjectsRepository = new SavedObjectsRepository({
|
||||
index: '.kibana-test',
|
||||
cacheIndexPatterns: {
|
||||
setIndexPatterns: jest.fn(),
|
||||
getIndexPatterns: () => undefined,
|
||||
},
|
||||
mappings,
|
||||
callCluster: callAdminCluster,
|
||||
migrator,
|
||||
|
@ -285,7 +290,7 @@ describe('SavedObjectsRepository', () => {
|
|||
getSearchDslNS.getSearchDsl.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {});
|
||||
afterEach(() => { });
|
||||
|
||||
describe('#create', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -993,7 +998,7 @@ describe('SavedObjectsRepository', () => {
|
|||
expect(onBeforeWrite).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should return objects in the same order regardless of type', () => {});
|
||||
it('should return objects in the same order regardless of type', () => { });
|
||||
});
|
||||
|
||||
describe('#delete', () => {
|
||||
|
@ -1154,6 +1159,13 @@ describe('SavedObjectsRepository', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('requires index pattern to be defined if filter is defined', async () => {
|
||||
callAdminCluster.mockReturnValue(noNamespaceSearchResults);
|
||||
expect(savedObjectsRepository.find({ type: 'foo', filter: 'foo.type: hello' }))
|
||||
.rejects
|
||||
.toThrowErrorMatchingInlineSnapshot('"options.filter is missing index pattern to work correctly: Bad Request"');
|
||||
});
|
||||
|
||||
it('passes mappings, schema, search, defaultSearchOperator, searchFields, type, sortField, sortOrder and hasReference to getSearchDsl',
|
||||
async () => {
|
||||
callAdminCluster.mockReturnValue(namespacedSearchResults);
|
||||
|
@ -1169,6 +1181,8 @@ describe('SavedObjectsRepository', () => {
|
|||
type: 'foo',
|
||||
id: '1',
|
||||
},
|
||||
indexPattern: undefined,
|
||||
kueryNode: null,
|
||||
};
|
||||
|
||||
await savedObjectsRepository.find(relevantOpts);
|
||||
|
|
|
@ -19,11 +19,13 @@
|
|||
|
||||
import { omit } from 'lodash';
|
||||
import { CallCluster } from 'src/legacy/core_plugins/elasticsearch';
|
||||
|
||||
import { getRootPropertiesObjects, IndexMapping } from '../../mappings';
|
||||
import { getSearchDsl } from './search_dsl';
|
||||
import { includedFields } from './included_fields';
|
||||
import { decorateEsError } from './decorate_es_error';
|
||||
import { SavedObjectsErrorHelpers } from './errors';
|
||||
import { SavedObjectsCacheIndexPatterns } from './cache_index_patterns';
|
||||
import { decodeRequestVersion, encodeVersion, encodeHitVersion } from '../../version';
|
||||
import { SavedObjectsSchema } from '../../schema';
|
||||
import { KibanaMigrator } from '../../migrations';
|
||||
|
@ -45,6 +47,7 @@ import {
|
|||
SavedObjectsFindOptions,
|
||||
SavedObjectsMigrationVersion,
|
||||
} from '../../types';
|
||||
import { validateConvertFilterToKueryNode } from './filter_utils';
|
||||
|
||||
// BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository
|
||||
// so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient.
|
||||
|
@ -74,6 +77,7 @@ export interface SavedObjectsRepositoryOptions {
|
|||
serializer: SavedObjectsSerializer;
|
||||
migrator: KibanaMigrator;
|
||||
allowedTypes: string[];
|
||||
cacheIndexPatterns: SavedObjectsCacheIndexPatterns;
|
||||
onBeforeWrite?: (...args: Parameters<CallCluster>) => Promise<void>;
|
||||
}
|
||||
|
||||
|
@ -91,11 +95,13 @@ export class SavedObjectsRepository {
|
|||
private _onBeforeWrite: (...args: Parameters<CallCluster>) => Promise<void>;
|
||||
private _unwrappedCallCluster: CallCluster;
|
||||
private _serializer: SavedObjectsSerializer;
|
||||
private _cacheIndexPatterns: SavedObjectsCacheIndexPatterns;
|
||||
|
||||
constructor(options: SavedObjectsRepositoryOptions) {
|
||||
const {
|
||||
index,
|
||||
config,
|
||||
cacheIndexPatterns,
|
||||
mappings,
|
||||
callCluster,
|
||||
schema,
|
||||
|
@ -106,7 +112,7 @@ export class SavedObjectsRepository {
|
|||
} = options;
|
||||
|
||||
// It's important that we migrate documents / mark them as up-to-date
|
||||
// prior to writing them to the index. Otherwise, we'll cause unecessary
|
||||
// prior to writing them to the index. Otherwise, we'll cause unnecessary
|
||||
// index migrations to run at Kibana startup, and those will probably fail
|
||||
// due to invalidly versioned documents in the index.
|
||||
//
|
||||
|
@ -117,6 +123,7 @@ export class SavedObjectsRepository {
|
|||
this._config = config;
|
||||
this._mappings = mappings;
|
||||
this._schema = schema;
|
||||
this._cacheIndexPatterns = cacheIndexPatterns;
|
||||
if (allowedTypes.length === 0) {
|
||||
throw new Error('Empty or missing types for saved object repository!');
|
||||
}
|
||||
|
@ -126,6 +133,9 @@ export class SavedObjectsRepository {
|
|||
|
||||
this._unwrappedCallCluster = async (...args: Parameters<CallCluster>) => {
|
||||
await migrator.runMigrations();
|
||||
if (this._cacheIndexPatterns.getIndexPatterns() == null) {
|
||||
await this._cacheIndexPatterns.setIndexPatterns(index);
|
||||
}
|
||||
return callCluster(...args);
|
||||
};
|
||||
this._schema = schema;
|
||||
|
@ -404,9 +414,12 @@ export class SavedObjectsRepository {
|
|||
fields,
|
||||
namespace,
|
||||
type,
|
||||
filter,
|
||||
}: SavedObjectsFindOptions): Promise<SavedObjectsFindResponse<T>> {
|
||||
if (!type) {
|
||||
throw new TypeError(`options.type must be a string or an array of strings`);
|
||||
throw SavedObjectsErrorHelpers.createBadRequestError(
|
||||
'options.type must be a string or an array of strings'
|
||||
);
|
||||
}
|
||||
|
||||
const types = Array.isArray(type) ? type : [type];
|
||||
|
@ -421,13 +434,28 @@ export class SavedObjectsRepository {
|
|||
}
|
||||
|
||||
if (searchFields && !Array.isArray(searchFields)) {
|
||||
throw new TypeError('options.searchFields must be an array');
|
||||
throw SavedObjectsErrorHelpers.createBadRequestError('options.searchFields must be an array');
|
||||
}
|
||||
|
||||
if (fields && !Array.isArray(fields)) {
|
||||
throw new TypeError('options.fields must be an array');
|
||||
throw SavedObjectsErrorHelpers.createBadRequestError('options.fields must be an array');
|
||||
}
|
||||
|
||||
if (filter && filter !== '' && this._cacheIndexPatterns.getIndexPatterns() == null) {
|
||||
throw SavedObjectsErrorHelpers.createBadRequestError(
|
||||
'options.filter is missing index pattern to work correctly'
|
||||
);
|
||||
}
|
||||
|
||||
const kueryNode =
|
||||
filter && filter !== ''
|
||||
? validateConvertFilterToKueryNode(
|
||||
allowedTypes,
|
||||
filter,
|
||||
this._cacheIndexPatterns.getIndexPatterns()
|
||||
)
|
||||
: null;
|
||||
|
||||
const esOptions = {
|
||||
index: this.getIndicesForTypes(allowedTypes),
|
||||
size: perPage,
|
||||
|
@ -446,6 +474,8 @@ export class SavedObjectsRepository {
|
|||
sortOrder,
|
||||
namespace,
|
||||
hasReference,
|
||||
indexPattern: kueryNode != null ? this._cacheIndexPatterns.getIndexPatterns() : undefined,
|
||||
kueryNode,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
@ -769,7 +799,7 @@ export class SavedObjectsRepository {
|
|||
|
||||
// The internal representation of the saved object that the serializer returns
|
||||
// includes the namespace, and we use this for migrating documents. However, we don't
|
||||
// want the namespcae to be returned from the repository, as the repository scopes each
|
||||
// want the namespace to be returned from the repository, as the repository scopes each
|
||||
// method transparently to the specified namespace.
|
||||
private _rawToSavedObject(raw: RawDoc): SavedObject {
|
||||
const savedObject = this._serializer.rawToSavedObject(raw);
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
*/
|
||||
|
||||
import { schemaMock } from '../../../schema/schema.mock';
|
||||
import { SavedObjectsIndexPattern } from '../cache_index_patterns';
|
||||
import { getQueryParams } from './query_params';
|
||||
|
||||
const SCHEMA = schemaMock.create();
|
||||
|
@ -61,6 +62,41 @@ const MAPPINGS = {
|
|||
},
|
||||
},
|
||||
};
|
||||
const INDEX_PATTERN: SavedObjectsIndexPattern = {
|
||||
fields: [
|
||||
{
|
||||
aggregatable: true,
|
||||
name: 'type',
|
||||
searchable: true,
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
aggregatable: true,
|
||||
name: 'pending.title',
|
||||
searchable: true,
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
aggregatable: true,
|
||||
name: 'saved.title',
|
||||
searchable: true,
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
aggregatable: true,
|
||||
name: 'saved.obj.key1',
|
||||
searchable: true,
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
aggregatable: true,
|
||||
name: 'global.name',
|
||||
searchable: true,
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
title: 'test',
|
||||
};
|
||||
|
||||
// create a type clause to be used within the "should", if a namespace is specified
|
||||
// the clause will ensure the namespace matches; otherwise, the clause will ensure
|
||||
|
@ -85,7 +121,7 @@ const createTypeClause = (type: string, namespace?: string) => {
|
|||
describe('searchDsl/queryParams', () => {
|
||||
describe('no parameters', () => {
|
||||
it('searches for all known types without a namespace specified', () => {
|
||||
expect(getQueryParams(MAPPINGS, SCHEMA)).toEqual({
|
||||
expect(getQueryParams({ mappings: MAPPINGS, schema: SCHEMA })).toEqual({
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
|
@ -108,7 +144,9 @@ describe('searchDsl/queryParams', () => {
|
|||
|
||||
describe('namespace', () => {
|
||||
it('filters namespaced types for namespace, and ensures namespace agnostic types have no namespace', () => {
|
||||
expect(getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace')).toEqual({
|
||||
expect(
|
||||
getQueryParams({ mappings: MAPPINGS, schema: SCHEMA, namespace: 'foo-namespace' })
|
||||
).toEqual({
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
|
@ -131,7 +169,9 @@ describe('searchDsl/queryParams', () => {
|
|||
|
||||
describe('type (singular, namespaced)', () => {
|
||||
it('includes a terms filter for type and namespace not being specified', () => {
|
||||
expect(getQueryParams(MAPPINGS, SCHEMA, undefined, 'saved')).toEqual({
|
||||
expect(
|
||||
getQueryParams({ mappings: MAPPINGS, schema: SCHEMA, namespace: undefined, type: 'saved' })
|
||||
).toEqual({
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
|
@ -150,7 +190,9 @@ describe('searchDsl/queryParams', () => {
|
|||
|
||||
describe('type (singular, global)', () => {
|
||||
it('includes a terms filter for type and namespace not being specified', () => {
|
||||
expect(getQueryParams(MAPPINGS, SCHEMA, undefined, 'global')).toEqual({
|
||||
expect(
|
||||
getQueryParams({ mappings: MAPPINGS, schema: SCHEMA, namespace: undefined, type: 'global' })
|
||||
).toEqual({
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
|
@ -169,7 +211,14 @@ describe('searchDsl/queryParams', () => {
|
|||
|
||||
describe('type (plural, namespaced and global)', () => {
|
||||
it('includes term filters for types and namespace not being specified', () => {
|
||||
expect(getQueryParams(MAPPINGS, SCHEMA, undefined, ['saved', 'global'])).toEqual({
|
||||
expect(
|
||||
getQueryParams({
|
||||
mappings: MAPPINGS,
|
||||
schema: SCHEMA,
|
||||
namespace: undefined,
|
||||
type: ['saved', 'global'],
|
||||
})
|
||||
).toEqual({
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
|
@ -188,7 +237,14 @@ describe('searchDsl/queryParams', () => {
|
|||
|
||||
describe('namespace, type (plural, namespaced and global)', () => {
|
||||
it('includes a terms filter for type and namespace not being specified', () => {
|
||||
expect(getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', ['saved', 'global'])).toEqual({
|
||||
expect(
|
||||
getQueryParams({
|
||||
mappings: MAPPINGS,
|
||||
schema: SCHEMA,
|
||||
namespace: 'foo-namespace',
|
||||
type: ['saved', 'global'],
|
||||
})
|
||||
).toEqual({
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
|
@ -207,7 +263,15 @@ describe('searchDsl/queryParams', () => {
|
|||
|
||||
describe('search', () => {
|
||||
it('includes a sqs query and all known types without a namespace specified', () => {
|
||||
expect(getQueryParams(MAPPINGS, SCHEMA, undefined, undefined, 'us*')).toEqual({
|
||||
expect(
|
||||
getQueryParams({
|
||||
mappings: MAPPINGS,
|
||||
schema: SCHEMA,
|
||||
namespace: undefined,
|
||||
type: undefined,
|
||||
search: 'us*',
|
||||
})
|
||||
).toEqual({
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
|
@ -239,7 +303,15 @@ describe('searchDsl/queryParams', () => {
|
|||
|
||||
describe('namespace, search', () => {
|
||||
it('includes a sqs query and namespaced types with the namespace and global types without a namespace', () => {
|
||||
expect(getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', undefined, 'us*')).toEqual({
|
||||
expect(
|
||||
getQueryParams({
|
||||
mappings: MAPPINGS,
|
||||
schema: SCHEMA,
|
||||
namespace: 'foo-namespace',
|
||||
type: undefined,
|
||||
search: 'us*',
|
||||
})
|
||||
).toEqual({
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
|
@ -271,7 +343,15 @@ describe('searchDsl/queryParams', () => {
|
|||
|
||||
describe('type (plural, namespaced and global), search', () => {
|
||||
it('includes a sqs query and types without a namespace', () => {
|
||||
expect(getQueryParams(MAPPINGS, SCHEMA, undefined, ['saved', 'global'], 'us*')).toEqual({
|
||||
expect(
|
||||
getQueryParams({
|
||||
mappings: MAPPINGS,
|
||||
schema: SCHEMA,
|
||||
namespace: undefined,
|
||||
type: ['saved', 'global'],
|
||||
search: 'us*',
|
||||
})
|
||||
).toEqual({
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
|
@ -299,40 +379,52 @@ describe('searchDsl/queryParams', () => {
|
|||
|
||||
describe('namespace, type (plural, namespaced and global), search', () => {
|
||||
it('includes a sqs query and namespace type with a namespace and global type without a namespace', () => {
|
||||
expect(getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', ['saved', 'global'], 'us*')).toEqual(
|
||||
{
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
createTypeClause('saved', 'foo-namespace'),
|
||||
createTypeClause('global'),
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
expect(
|
||||
getQueryParams({
|
||||
mappings: MAPPINGS,
|
||||
schema: SCHEMA,
|
||||
namespace: 'foo-namespace',
|
||||
type: ['saved', 'global'],
|
||||
search: 'us*',
|
||||
})
|
||||
).toEqual({
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
should: [createTypeClause('saved', 'foo-namespace'), createTypeClause('global')],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
],
|
||||
must: [
|
||||
{
|
||||
simple_query_string: {
|
||||
query: 'us*',
|
||||
lenient: true,
|
||||
fields: ['*'],
|
||||
},
|
||||
},
|
||||
],
|
||||
must: [
|
||||
{
|
||||
simple_query_string: {
|
||||
query: 'us*',
|
||||
lenient: true,
|
||||
fields: ['*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('search, searchFields', () => {
|
||||
it('includes all types for field', () => {
|
||||
expect(getQueryParams(MAPPINGS, SCHEMA, undefined, undefined, 'y*', ['title'])).toEqual({
|
||||
expect(
|
||||
getQueryParams({
|
||||
mappings: MAPPINGS,
|
||||
schema: SCHEMA,
|
||||
namespace: undefined,
|
||||
type: undefined,
|
||||
search: 'y*',
|
||||
searchFields: ['title'],
|
||||
})
|
||||
).toEqual({
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
|
@ -360,7 +452,16 @@ describe('searchDsl/queryParams', () => {
|
|||
});
|
||||
});
|
||||
it('supports field boosting', () => {
|
||||
expect(getQueryParams(MAPPINGS, SCHEMA, undefined, undefined, 'y*', ['title^3'])).toEqual({
|
||||
expect(
|
||||
getQueryParams({
|
||||
mappings: MAPPINGS,
|
||||
schema: SCHEMA,
|
||||
namespace: undefined,
|
||||
type: undefined,
|
||||
search: 'y*',
|
||||
searchFields: ['title^3'],
|
||||
})
|
||||
).toEqual({
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
|
@ -389,7 +490,14 @@ describe('searchDsl/queryParams', () => {
|
|||
});
|
||||
it('supports field and multi-field', () => {
|
||||
expect(
|
||||
getQueryParams(MAPPINGS, SCHEMA, undefined, undefined, 'y*', ['title', 'title.raw'])
|
||||
getQueryParams({
|
||||
mappings: MAPPINGS,
|
||||
schema: SCHEMA,
|
||||
namespace: undefined,
|
||||
type: undefined,
|
||||
search: 'y*',
|
||||
searchFields: ['title', 'title.raw'],
|
||||
})
|
||||
).toEqual({
|
||||
query: {
|
||||
bool: {
|
||||
|
@ -428,38 +536,52 @@ describe('searchDsl/queryParams', () => {
|
|||
|
||||
describe('namespace, search, searchFields', () => {
|
||||
it('includes all types for field', () => {
|
||||
expect(getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', undefined, 'y*', ['title'])).toEqual(
|
||||
{
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
createTypeClause('pending', 'foo-namespace'),
|
||||
createTypeClause('saved', 'foo-namespace'),
|
||||
createTypeClause('global'),
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
expect(
|
||||
getQueryParams({
|
||||
mappings: MAPPINGS,
|
||||
schema: SCHEMA,
|
||||
namespace: 'foo-namespace',
|
||||
type: undefined,
|
||||
search: 'y*',
|
||||
searchFields: ['title'],
|
||||
})
|
||||
).toEqual({
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
createTypeClause('pending', 'foo-namespace'),
|
||||
createTypeClause('saved', 'foo-namespace'),
|
||||
createTypeClause('global'),
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
],
|
||||
must: [
|
||||
{
|
||||
simple_query_string: {
|
||||
query: 'y*',
|
||||
fields: ['pending.title', 'saved.title', 'global.title'],
|
||||
},
|
||||
},
|
||||
],
|
||||
must: [
|
||||
{
|
||||
simple_query_string: {
|
||||
query: 'y*',
|
||||
fields: ['pending.title', 'saved.title', 'global.title'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
});
|
||||
it('supports field boosting', () => {
|
||||
expect(
|
||||
getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', undefined, 'y*', ['title^3'])
|
||||
getQueryParams({
|
||||
mappings: MAPPINGS,
|
||||
schema: SCHEMA,
|
||||
namespace: 'foo-namespace',
|
||||
type: undefined,
|
||||
search: 'y*',
|
||||
searchFields: ['title^3'],
|
||||
})
|
||||
).toEqual({
|
||||
query: {
|
||||
bool: {
|
||||
|
@ -489,7 +611,14 @@ describe('searchDsl/queryParams', () => {
|
|||
});
|
||||
it('supports field and multi-field', () => {
|
||||
expect(
|
||||
getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', undefined, 'y*', ['title', 'title.raw'])
|
||||
getQueryParams({
|
||||
mappings: MAPPINGS,
|
||||
schema: SCHEMA,
|
||||
namespace: 'foo-namespace',
|
||||
type: undefined,
|
||||
search: 'y*',
|
||||
searchFields: ['title', 'title.raw'],
|
||||
})
|
||||
).toEqual({
|
||||
query: {
|
||||
bool: {
|
||||
|
@ -529,7 +658,14 @@ describe('searchDsl/queryParams', () => {
|
|||
describe('type (plural, namespaced and global), search, searchFields', () => {
|
||||
it('includes all types for field', () => {
|
||||
expect(
|
||||
getQueryParams(MAPPINGS, SCHEMA, undefined, ['saved', 'global'], 'y*', ['title'])
|
||||
getQueryParams({
|
||||
mappings: MAPPINGS,
|
||||
schema: SCHEMA,
|
||||
namespace: undefined,
|
||||
type: ['saved', 'global'],
|
||||
search: 'y*',
|
||||
searchFields: ['title'],
|
||||
})
|
||||
).toEqual({
|
||||
query: {
|
||||
bool: {
|
||||
|
@ -555,7 +691,14 @@ describe('searchDsl/queryParams', () => {
|
|||
});
|
||||
it('supports field boosting', () => {
|
||||
expect(
|
||||
getQueryParams(MAPPINGS, SCHEMA, undefined, ['saved', 'global'], 'y*', ['title^3'])
|
||||
getQueryParams({
|
||||
mappings: MAPPINGS,
|
||||
schema: SCHEMA,
|
||||
namespace: undefined,
|
||||
type: ['saved', 'global'],
|
||||
search: 'y*',
|
||||
searchFields: ['title^3'],
|
||||
})
|
||||
).toEqual({
|
||||
query: {
|
||||
bool: {
|
||||
|
@ -581,10 +724,14 @@ describe('searchDsl/queryParams', () => {
|
|||
});
|
||||
it('supports field and multi-field', () => {
|
||||
expect(
|
||||
getQueryParams(MAPPINGS, SCHEMA, undefined, ['saved', 'global'], 'y*', [
|
||||
'title',
|
||||
'title.raw',
|
||||
])
|
||||
getQueryParams({
|
||||
mappings: MAPPINGS,
|
||||
schema: SCHEMA,
|
||||
namespace: undefined,
|
||||
type: ['saved', 'global'],
|
||||
search: 'y*',
|
||||
searchFields: ['title', 'title.raw'],
|
||||
})
|
||||
).toEqual({
|
||||
query: {
|
||||
bool: {
|
||||
|
@ -613,7 +760,14 @@ describe('searchDsl/queryParams', () => {
|
|||
describe('namespace, type (plural, namespaced and global), search, searchFields', () => {
|
||||
it('includes all types for field', () => {
|
||||
expect(
|
||||
getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', ['saved', 'global'], 'y*', ['title'])
|
||||
getQueryParams({
|
||||
mappings: MAPPINGS,
|
||||
schema: SCHEMA,
|
||||
namespace: 'foo-namespace',
|
||||
type: ['saved', 'global'],
|
||||
search: 'y*',
|
||||
searchFields: ['title'],
|
||||
})
|
||||
).toEqual({
|
||||
query: {
|
||||
bool: {
|
||||
|
@ -639,7 +793,14 @@ describe('searchDsl/queryParams', () => {
|
|||
});
|
||||
it('supports field boosting', () => {
|
||||
expect(
|
||||
getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', ['saved', 'global'], 'y*', ['title^3'])
|
||||
getQueryParams({
|
||||
mappings: MAPPINGS,
|
||||
schema: SCHEMA,
|
||||
namespace: 'foo-namespace',
|
||||
type: ['saved', 'global'],
|
||||
search: 'y*',
|
||||
searchFields: ['title^3'],
|
||||
})
|
||||
).toEqual({
|
||||
query: {
|
||||
bool: {
|
||||
|
@ -665,10 +826,14 @@ describe('searchDsl/queryParams', () => {
|
|||
});
|
||||
it('supports field and multi-field', () => {
|
||||
expect(
|
||||
getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', ['saved', 'global'], 'y*', [
|
||||
'title',
|
||||
'title.raw',
|
||||
])
|
||||
getQueryParams({
|
||||
mappings: MAPPINGS,
|
||||
schema: SCHEMA,
|
||||
namespace: 'foo-namespace',
|
||||
type: ['saved', 'global'],
|
||||
search: 'y*',
|
||||
searchFields: ['title', 'title.raw'],
|
||||
})
|
||||
).toEqual({
|
||||
query: {
|
||||
bool: {
|
||||
|
@ -697,15 +862,15 @@ describe('searchDsl/queryParams', () => {
|
|||
describe('type (plural, namespaced and global), search, defaultSearchOperator', () => {
|
||||
it('supports defaultSearchOperator', () => {
|
||||
expect(
|
||||
getQueryParams(
|
||||
MAPPINGS,
|
||||
SCHEMA,
|
||||
'foo-namespace',
|
||||
['saved', 'global'],
|
||||
'foo',
|
||||
undefined,
|
||||
'AND'
|
||||
)
|
||||
getQueryParams({
|
||||
mappings: MAPPINGS,
|
||||
schema: SCHEMA,
|
||||
namespace: 'foo-namespace',
|
||||
type: ['saved', 'global'],
|
||||
search: 'foo',
|
||||
searchFields: undefined,
|
||||
defaultSearchOperator: 'AND',
|
||||
})
|
||||
).toEqual({
|
||||
query: {
|
||||
bool: {
|
||||
|
@ -771,19 +936,19 @@ describe('searchDsl/queryParams', () => {
|
|||
describe('type (plural, namespaced and global), hasReference', () => {
|
||||
it('supports hasReference', () => {
|
||||
expect(
|
||||
getQueryParams(
|
||||
MAPPINGS,
|
||||
SCHEMA,
|
||||
'foo-namespace',
|
||||
['saved', 'global'],
|
||||
undefined,
|
||||
undefined,
|
||||
'OR',
|
||||
{
|
||||
getQueryParams({
|
||||
mappings: MAPPINGS,
|
||||
schema: SCHEMA,
|
||||
namespace: 'foo-namespace',
|
||||
type: ['saved', 'global'],
|
||||
search: undefined,
|
||||
searchFields: undefined,
|
||||
defaultSearchOperator: 'OR',
|
||||
hasReference: {
|
||||
type: 'bar',
|
||||
id: '1',
|
||||
}
|
||||
)
|
||||
},
|
||||
})
|
||||
).toEqual({
|
||||
query: {
|
||||
bool: {
|
||||
|
@ -823,4 +988,345 @@ describe('searchDsl/queryParams', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('type filter', () => {
|
||||
it(' with namespace', () => {
|
||||
expect(
|
||||
getQueryParams({
|
||||
mappings: MAPPINGS,
|
||||
schema: SCHEMA,
|
||||
namespace: 'foo-namespace',
|
||||
kueryNode: {
|
||||
type: 'function',
|
||||
function: 'is',
|
||||
arguments: [
|
||||
{ type: 'literal', value: 'global.name' },
|
||||
{ type: 'literal', value: 'GLOBAL' },
|
||||
{ type: 'literal', value: false },
|
||||
],
|
||||
},
|
||||
indexPattern: INDEX_PATTERN,
|
||||
})
|
||||
).toEqual({
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
'global.name': 'GLOBAL',
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
type: 'pending',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
namespace: 'foo-namespace',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
type: 'saved',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
namespace: 'foo-namespace',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
type: 'global',
|
||||
},
|
||||
},
|
||||
],
|
||||
must_not: [
|
||||
{
|
||||
exists: {
|
||||
field: 'namespace',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
it(' with namespace and more complex filter', () => {
|
||||
expect(
|
||||
getQueryParams({
|
||||
mappings: MAPPINGS,
|
||||
schema: SCHEMA,
|
||||
namespace: 'foo-namespace',
|
||||
kueryNode: {
|
||||
type: 'function',
|
||||
function: 'and',
|
||||
arguments: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'is',
|
||||
arguments: [
|
||||
{ type: 'literal', value: 'global.name' },
|
||||
{ type: 'literal', value: 'GLOBAL' },
|
||||
{ type: 'literal', value: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: 'not',
|
||||
arguments: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'is',
|
||||
arguments: [
|
||||
{ type: 'literal', value: 'saved.obj.key1' },
|
||||
{ type: 'literal', value: 'key' },
|
||||
{ type: 'literal', value: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
indexPattern: INDEX_PATTERN,
|
||||
})
|
||||
).toEqual({
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
'global.name': 'GLOBAL',
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
match_phrase: {
|
||||
'saved.obj.key1': 'key',
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
type: 'pending',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
namespace: 'foo-namespace',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
type: 'saved',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
namespace: 'foo-namespace',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
type: 'global',
|
||||
},
|
||||
},
|
||||
],
|
||||
must_not: [
|
||||
{
|
||||
exists: {
|
||||
field: 'namespace',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
it(' with search and searchFields', () => {
|
||||
expect(
|
||||
getQueryParams({
|
||||
mappings: MAPPINGS,
|
||||
schema: SCHEMA,
|
||||
namespace: 'foo-namespace',
|
||||
search: 'y*',
|
||||
searchFields: ['title'],
|
||||
kueryNode: {
|
||||
type: 'function',
|
||||
function: 'is',
|
||||
arguments: [
|
||||
{ type: 'literal', value: 'global.name' },
|
||||
{ type: 'literal', value: 'GLOBAL' },
|
||||
{ type: 'literal', value: false },
|
||||
],
|
||||
},
|
||||
indexPattern: INDEX_PATTERN,
|
||||
})
|
||||
).toEqual({
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
'global.name': 'GLOBAL',
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
type: 'pending',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
namespace: 'foo-namespace',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
type: 'saved',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
namespace: 'foo-namespace',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
type: 'global',
|
||||
},
|
||||
},
|
||||
],
|
||||
must_not: [
|
||||
{
|
||||
exists: {
|
||||
field: 'namespace',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
must: [
|
||||
{
|
||||
simple_query_string: {
|
||||
query: 'y*',
|
||||
fields: ['pending.title', 'saved.title', 'global.title'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,9 +16,11 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { toElasticsearchQuery, KueryNode } from '@kbn/es-query';
|
||||
|
||||
import { getRootPropertiesObjects, IndexMapping } from '../../../mappings';
|
||||
import { SavedObjectsSchema } from '../../../schema';
|
||||
import { SavedObjectsIndexPattern } from '../cache_index_patterns';
|
||||
|
||||
/**
|
||||
* Gets the types based on the type. Uses mappings to support
|
||||
|
@ -76,25 +78,43 @@ function getClauseForType(schema: SavedObjectsSchema, namespace: string | undefi
|
|||
};
|
||||
}
|
||||
|
||||
interface HasReferenceQueryParams {
|
||||
type: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface QueryParams {
|
||||
mappings: IndexMapping;
|
||||
schema: SavedObjectsSchema;
|
||||
namespace?: string;
|
||||
type?: string | string[];
|
||||
search?: string;
|
||||
searchFields?: string[];
|
||||
defaultSearchOperator?: string;
|
||||
hasReference?: HasReferenceQueryParams;
|
||||
kueryNode?: KueryNode;
|
||||
indexPattern?: SavedObjectsIndexPattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the "query" related keys for the search body
|
||||
*/
|
||||
export function getQueryParams(
|
||||
mappings: IndexMapping,
|
||||
schema: SavedObjectsSchema,
|
||||
namespace?: string,
|
||||
type?: string | string[],
|
||||
search?: string,
|
||||
searchFields?: string[],
|
||||
defaultSearchOperator?: string,
|
||||
hasReference?: {
|
||||
type: string;
|
||||
id: string;
|
||||
}
|
||||
) {
|
||||
export function getQueryParams({
|
||||
mappings,
|
||||
schema,
|
||||
namespace,
|
||||
type,
|
||||
search,
|
||||
searchFields,
|
||||
defaultSearchOperator,
|
||||
hasReference,
|
||||
kueryNode,
|
||||
indexPattern,
|
||||
}: QueryParams) {
|
||||
const types = getTypes(mappings, type);
|
||||
const bool: any = {
|
||||
filter: [
|
||||
...(kueryNode != null ? [toElasticsearchQuery(kueryNode, indexPattern)] : []),
|
||||
{
|
||||
bool: {
|
||||
must: hasReference
|
||||
|
|
|
@ -72,16 +72,16 @@ describe('getSearchDsl', () => {
|
|||
|
||||
getSearchDsl(MAPPINGS, SCHEMA, opts);
|
||||
expect(getQueryParams).toHaveBeenCalledTimes(1);
|
||||
expect(getQueryParams).toHaveBeenCalledWith(
|
||||
MAPPINGS,
|
||||
SCHEMA,
|
||||
opts.namespace,
|
||||
opts.type,
|
||||
opts.search,
|
||||
opts.searchFields,
|
||||
opts.defaultSearchOperator,
|
||||
opts.hasReference
|
||||
);
|
||||
expect(getQueryParams).toHaveBeenCalledWith({
|
||||
mappings: MAPPINGS,
|
||||
schema: SCHEMA,
|
||||
namespace: opts.namespace,
|
||||
type: opts.type,
|
||||
search: opts.search,
|
||||
searchFields: opts.searchFields,
|
||||
defaultSearchOperator: opts.defaultSearchOperator,
|
||||
hasReference: opts.hasReference,
|
||||
});
|
||||
});
|
||||
|
||||
it('passes (mappings, type, sortField, sortOrder) to getSortingParams', () => {
|
||||
|
|
|
@ -17,12 +17,14 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { KueryNode } from '@kbn/es-query';
|
||||
import Boom from 'boom';
|
||||
|
||||
import { IndexMapping } from '../../../mappings';
|
||||
import { SavedObjectsSchema } from '../../../schema';
|
||||
import { getQueryParams } from './query_params';
|
||||
import { getSortingParams } from './sorting_params';
|
||||
import { SavedObjectsIndexPattern } from '../cache_index_patterns';
|
||||
|
||||
interface GetSearchDslOptions {
|
||||
type: string | string[];
|
||||
|
@ -36,6 +38,8 @@ interface GetSearchDslOptions {
|
|||
type: string;
|
||||
id: string;
|
||||
};
|
||||
kueryNode?: KueryNode;
|
||||
indexPattern?: SavedObjectsIndexPattern;
|
||||
}
|
||||
|
||||
export function getSearchDsl(
|
||||
|
@ -52,6 +56,8 @@ export function getSearchDsl(
|
|||
sortOrder,
|
||||
namespace,
|
||||
hasReference,
|
||||
kueryNode,
|
||||
indexPattern,
|
||||
} = options;
|
||||
|
||||
if (!type) {
|
||||
|
@ -63,7 +69,7 @@ export function getSearchDsl(
|
|||
}
|
||||
|
||||
return {
|
||||
...getQueryParams(
|
||||
...getQueryParams({
|
||||
mappings,
|
||||
schema,
|
||||
namespace,
|
||||
|
@ -71,8 +77,10 @@ export function getSearchDsl(
|
|||
search,
|
||||
searchFields,
|
||||
defaultSearchOperator,
|
||||
hasReference
|
||||
),
|
||||
hasReference,
|
||||
kueryNode,
|
||||
indexPattern,
|
||||
}),
|
||||
...getSortingParams(mappings, type, sortField, sortOrder),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -123,6 +123,7 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions {
|
|||
searchFields?: string[];
|
||||
hasReference?: { type: string; id: string };
|
||||
defaultSearchOperator?: 'AND' | 'OR';
|
||||
filter?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -10,6 +10,7 @@ import { ConfigOptions } from 'elasticsearch';
|
|||
import { DetailedPeerCertificate } from 'tls';
|
||||
import { Duration } from 'moment';
|
||||
import { IncomingHttpHeaders } from 'http';
|
||||
import { IndexPatternsService } from 'src/legacy/server/index_patterns';
|
||||
import { KibanaConfigType } from 'src/core/server/kibana_config';
|
||||
import { Logger as Logger_2 } from 'src/core/server/logging';
|
||||
import { ObjectType } from '@kbn/config-schema';
|
||||
|
@ -841,6 +842,8 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions {
|
|||
defaultSearchOperator?: 'AND' | 'OR';
|
||||
fields?: string[];
|
||||
// (undocumented)
|
||||
filter?: string;
|
||||
// (undocumented)
|
||||
hasReference?: {
|
||||
type: string;
|
||||
id: string;
|
||||
|
|
|
@ -482,7 +482,7 @@ export interface CallCluster {
|
|||
(endpoint: 'indices.upgrade', params: IndicesUpgradeParams, options?: CallClusterOptions): ReturnType<ESClient['indices']['upgrade']>;
|
||||
(endpoint: 'indices.validateQuery', params: IndicesValidateQueryParams, options?: CallClusterOptions): ReturnType<ESClient['indices']['validateQuery']>;
|
||||
|
||||
// ingest namepsace
|
||||
// ingest namespace
|
||||
(endpoint: 'ingest.deletePipeline', params: IngestDeletePipelineParams, options?: CallClusterOptions): ReturnType<ESClient['ingest']['deletePipeline']>;
|
||||
(endpoint: 'ingest.getPipeline', params: IngestGetPipelineParams, options?: CallClusterOptions): ReturnType<ESClient['ingest']['getPipeline']>;
|
||||
(endpoint: 'ingest.putPipeline', params: IngestPutPipelineParams, options?: CallClusterOptions): ReturnType<ESClient['ingest']['putPipeline']>;
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
import { Readable } from 'stream';
|
||||
import { SavedObject } from 'kibana/server';
|
||||
import { SavedObject } from 'src/core/server';
|
||||
import { createSplitStream, createMapStream, createFilterStream } from '../../../utils/streams';
|
||||
|
||||
export function createSavedObjectsStreamFromNdJson(ndJsonStream: Readable) {
|
||||
|
|
|
@ -39,6 +39,7 @@ interface FindRequest extends WithoutQueryAndParams<Hapi.Request> {
|
|||
id: string;
|
||||
};
|
||||
fields?: string[];
|
||||
filter?: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -79,6 +80,9 @@ export const createFindRoute = (prereqs: Prerequisites) => ({
|
|||
fields: Joi.array()
|
||||
.items(Joi.string())
|
||||
.single(),
|
||||
filter: Joi.string()
|
||||
.allow('')
|
||||
.optional(),
|
||||
})
|
||||
.default(),
|
||||
},
|
||||
|
@ -94,6 +98,7 @@ export const createFindRoute = (prereqs: Prerequisites) => ({
|
|||
sortField: query.sort_field,
|
||||
hasReference: query.has_reference,
|
||||
fields: query.fields,
|
||||
filter: query.filter,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
|
|
@ -26,6 +26,7 @@ import {
|
|||
SavedObjectsClient,
|
||||
SavedObjectsRepository,
|
||||
ScopedSavedObjectsClientProvider,
|
||||
SavedObjectsCacheIndexPatterns,
|
||||
getSortedObjectsForExport,
|
||||
importSavedObjects,
|
||||
resolveImportErrors,
|
||||
|
@ -63,6 +64,7 @@ export function savedObjectsMixin(kbnServer, server) {
|
|||
const schema = new SavedObjectsSchema(kbnServer.uiExports.savedObjectSchemas);
|
||||
const visibleTypes = allTypes.filter(type => !schema.isHiddenType(type));
|
||||
const importableAndExportableTypes = getImportableAndExportableTypes({ kbnServer, visibleTypes });
|
||||
const cacheIndexPatterns = new SavedObjectsCacheIndexPatterns();
|
||||
|
||||
server.decorate('server', 'kibanaMigrator', migrator);
|
||||
server.decorate(
|
||||
|
@ -113,11 +115,18 @@ export function savedObjectsMixin(kbnServer, server) {
|
|||
});
|
||||
const combinedTypes = visibleTypes.concat(extraTypes);
|
||||
const allowedTypes = [...new Set(combinedTypes)];
|
||||
|
||||
if (cacheIndexPatterns.getIndexPatternsService() == null) {
|
||||
cacheIndexPatterns.setIndexPatternsService(
|
||||
server.indexPatternsServiceFactory({ callCluster })
|
||||
);
|
||||
}
|
||||
const config = server.config();
|
||||
|
||||
return new SavedObjectsRepository({
|
||||
index: config.get('kibana.index'),
|
||||
config,
|
||||
cacheIndexPatterns,
|
||||
migrator,
|
||||
mappings,
|
||||
schema,
|
||||
|
|
|
@ -84,6 +84,11 @@ describe('Saved Objects Mixin', () => {
|
|||
get: stubConfig,
|
||||
};
|
||||
},
|
||||
indexPatternsServiceFactory: () => {
|
||||
return {
|
||||
getFieldsForWildcard: jest.fn(),
|
||||
};
|
||||
},
|
||||
plugins: {
|
||||
elasticsearch: {
|
||||
getCluster: () => {
|
||||
|
|
|
@ -56,8 +56,10 @@ export abstract class FieldFormat {
|
|||
/**
|
||||
* @property {FieldFormatConvert}
|
||||
* @private
|
||||
* have to remove the private because of
|
||||
* https://github.com/Microsoft/TypeScript/issues/17293
|
||||
*/
|
||||
private convertObject: FieldFormatConvert | undefined;
|
||||
convertObject: FieldFormatConvert | undefined;
|
||||
|
||||
/**
|
||||
* @property {Function} - ref to child class
|
||||
|
@ -171,7 +173,11 @@ export abstract class FieldFormat {
|
|||
return createCustomFieldFormat(convertFn);
|
||||
}
|
||||
|
||||
private static setupContentType(
|
||||
/*
|
||||
* have to remove the private because of
|
||||
* https://github.com/Microsoft/TypeScript/issues/17293
|
||||
*/
|
||||
static setupContentType(
|
||||
fieldFormat: IFieldFormat,
|
||||
convert: Partial<FieldFormatConvert> | FieldFormatConvertFunction = {}
|
||||
): FieldFormatConvert {
|
||||
|
@ -185,7 +191,11 @@ export abstract class FieldFormat {
|
|||
};
|
||||
}
|
||||
|
||||
private static toConvertObject(convert: FieldFormatConvertFunction): Partial<FieldFormatConvert> {
|
||||
/*
|
||||
* have to remove the private because of
|
||||
* https://github.com/Microsoft/TypeScript/issues/17293
|
||||
*/
|
||||
static toConvertObject(convert: FieldFormatConvertFunction): Partial<FieldFormatConvert> {
|
||||
if (isFieldFormatConvertFn(convert)) {
|
||||
return {
|
||||
[TEXT_CONTEXT_TYPE]: convert,
|
||||
|
|
|
@ -109,6 +109,63 @@ export default function ({ getService }) {
|
|||
})
|
||||
));
|
||||
});
|
||||
|
||||
describe('with a filter', () => {
|
||||
it('should return 200 with a valid response', async () => (
|
||||
await supertest
|
||||
.get('/api/saved_objects/_find?type=visualization&filter=visualization.attributes.title:"Count of requests"')
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
total: 1,
|
||||
saved_objects: [
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
attributes: {
|
||||
title: 'Count of requests',
|
||||
visState: resp.body.saved_objects[0].attributes.visState,
|
||||
uiStateJSON: '{"spy":{"mode":{"name":null,"fill":false}}}',
|
||||
description: '',
|
||||
version: 1,
|
||||
kibanaSavedObjectMeta: {
|
||||
searchSourceJSON: resp.body.saved_objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON,
|
||||
},
|
||||
},
|
||||
references: [
|
||||
{
|
||||
name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
|
||||
type: 'index-pattern',
|
||||
id: '91200a00-9efd-11e7-acb3-3dab96693fab',
|
||||
}
|
||||
],
|
||||
migrationVersion: {
|
||||
visualization: '7.3.1',
|
||||
},
|
||||
updated_at: '2017-09-21T18:51:23.794Z',
|
||||
version: 'WzIsMV0=',
|
||||
},
|
||||
],
|
||||
});
|
||||
})
|
||||
));
|
||||
|
||||
it('wrong type should return 400 with Bad Request', async () => (
|
||||
await supertest
|
||||
.get('/api/saved_objects/_find?type=visualization&filter=dashboard.attributes.title:foo')
|
||||
.expect(400)
|
||||
.then(resp => {
|
||||
console.log('body', JSON.stringify(resp.body));
|
||||
expect(resp.body).to.eql({
|
||||
error: 'Bad Request',
|
||||
message: 'This type dashboard is not allowed: Bad Request',
|
||||
statusCode: 400,
|
||||
});
|
||||
})
|
||||
));
|
||||
});
|
||||
});
|
||||
|
||||
describe('without kibana index', () => {
|
||||
|
@ -200,6 +257,36 @@ export default function ({ getService }) {
|
|||
})
|
||||
));
|
||||
});
|
||||
|
||||
describe('with a filter', () => {
|
||||
it('should return 200 with an empty response', async () => (
|
||||
await supertest
|
||||
.get('/api/saved_objects/_find?type=visualization&filter=visualization.attributes.title:"Count of requests"')
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
total: 0,
|
||||
saved_objects: []
|
||||
});
|
||||
})
|
||||
));
|
||||
|
||||
it('wrong type should return 400 with Bad Request', async () => (
|
||||
await supertest
|
||||
.get('/api/saved_objects/_find?type=visualization&filter=dashboard.attributes.title:foo')
|
||||
.expect(400)
|
||||
.then(resp => {
|
||||
console.log('body', JSON.stringify(resp.body));
|
||||
expect(resp.body).to.eql({
|
||||
error: 'Bad Request',
|
||||
message: 'This type dashboard is not allowed: Bad Request',
|
||||
statusCode: 400,
|
||||
});
|
||||
})
|
||||
));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -14,9 +14,9 @@
|
|||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"../typings/lodash.topath/*.ts",
|
||||
"typings/**/*",
|
||||
"typings/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"plugin_functional/plugins/**/*"
|
||||
]
|
||||
}
|
||||
}
|
6
test/typings/index.d.ts
vendored
6
test/typings/index.d.ts
vendored
|
@ -17,6 +17,12 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
declare module '*.html' {
|
||||
const template: string;
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default template;
|
||||
}
|
||||
|
||||
type MethodKeysOf<T> = {
|
||||
[K in keyof T]: T[K] extends (...args: any[]) => any ? K : never;
|
||||
}[keyof T];
|
||||
|
|
|
@ -23,6 +23,11 @@ interface FindTests {
|
|||
unknownSearchField: FindTest;
|
||||
hiddenType: FindTest;
|
||||
noType: FindTest;
|
||||
filterWithNotSpaceAwareType: FindTest;
|
||||
filterWithHiddenType: FindTest;
|
||||
filterWithUnknownType: FindTest;
|
||||
filterWithNoType: FindTest;
|
||||
filterWithUnAllowedType: FindTest;
|
||||
}
|
||||
|
||||
interface FindTestDefinition {
|
||||
|
@ -73,6 +78,14 @@ export function findTestSuiteFactory(esArchiver: any, supertest: SuperTest<any>)
|
|||
});
|
||||
};
|
||||
|
||||
const expectFilterWrongTypeError = (resp: { [key: string]: any }) => {
|
||||
expect(resp.body).to.eql({
|
||||
error: 'Bad Request',
|
||||
message: 'This type dashboard is not allowed: Bad Request',
|
||||
statusCode: 400,
|
||||
});
|
||||
};
|
||||
|
||||
const expectTypeRequired = (resp: { [key: string]: any }) => {
|
||||
expect(resp.body).to.eql({
|
||||
error: 'Bad Request',
|
||||
|
@ -184,6 +197,67 @@ export function findTestSuiteFactory(esArchiver: any, supertest: SuperTest<any>)
|
|||
.expect(tests.noType.statusCode)
|
||||
.then(tests.noType.response));
|
||||
});
|
||||
|
||||
describe('filter', () => {
|
||||
it(`by wrong type should return ${tests.filterWithUnAllowedType.statusCode} with ${tests.filterWithUnAllowedType.description}`, async () =>
|
||||
await supertest
|
||||
.get(
|
||||
`${getUrlPrefix(
|
||||
spaceId
|
||||
)}/api/saved_objects/_find?type=globaltype&filter=dashboard.title:'Requests'`
|
||||
)
|
||||
.auth(user.username, user.password)
|
||||
.expect(tests.filterWithUnAllowedType.statusCode)
|
||||
.then(tests.filterWithUnAllowedType.response));
|
||||
|
||||
it(`not space aware type should return ${tests.filterWithNotSpaceAwareType.statusCode} with ${tests.filterWithNotSpaceAwareType.description}`, async () =>
|
||||
await supertest
|
||||
.get(
|
||||
`${getUrlPrefix(
|
||||
spaceId
|
||||
)}/api/saved_objects/_find?type=globaltype&filter=globaltype.attributes.name:*global*`
|
||||
)
|
||||
.auth(user.username, user.password)
|
||||
.expect(tests.filterWithNotSpaceAwareType.statusCode)
|
||||
.then(tests.filterWithNotSpaceAwareType.response));
|
||||
|
||||
it(`finding a hiddentype should return ${tests.filterWithHiddenType.statusCode} with ${tests.filterWithHiddenType.description}`, async () =>
|
||||
await supertest
|
||||
.get(
|
||||
`${getUrlPrefix(
|
||||
spaceId
|
||||
)}/api/saved_objects/_find?type=hiddentype&fields=name&filter=hiddentype.attributes.name:'hello'`
|
||||
)
|
||||
.auth(user.username, user.password)
|
||||
.expect(tests.filterWithHiddenType.statusCode)
|
||||
.then(tests.filterWithHiddenType.response));
|
||||
|
||||
describe('unknown type', () => {
|
||||
it(`should return ${tests.filterWithUnknownType.statusCode} with ${tests.filterWithUnknownType.description}`, async () =>
|
||||
await supertest
|
||||
.get(
|
||||
`${getUrlPrefix(
|
||||
spaceId
|
||||
)}/api/saved_objects/_find?type=wigwags&filter=wigwags.attributes.title:'unknown'`
|
||||
)
|
||||
.auth(user.username, user.password)
|
||||
.expect(tests.filterWithUnknownType.statusCode)
|
||||
.then(tests.filterWithUnknownType.response));
|
||||
});
|
||||
|
||||
describe('no type', () => {
|
||||
it(`should return ${tests.filterWithNoType.statusCode} with ${tests.filterWithNoType.description}`, async () =>
|
||||
await supertest
|
||||
.get(
|
||||
`${getUrlPrefix(
|
||||
spaceId
|
||||
)}/api/saved_objects/_find?filter=global.attributes.name:*global*`
|
||||
)
|
||||
.auth(user.username, user.password)
|
||||
.expect(tests.filterWithNoType.statusCode)
|
||||
.then(tests.filterWithNoType.response));
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -195,6 +269,7 @@ export function findTestSuiteFactory(esArchiver: any, supertest: SuperTest<any>)
|
|||
createExpectEmpty,
|
||||
createExpectRbacForbidden,
|
||||
createExpectVisualizationResults,
|
||||
expectFilterWrongTypeError,
|
||||
expectNotSpaceAwareResults,
|
||||
expectTypeRequired,
|
||||
findTest,
|
||||
|
|
|
@ -18,6 +18,7 @@ export default function({ getService }: FtrProviderContext) {
|
|||
createExpectEmpty,
|
||||
createExpectRbacForbidden,
|
||||
createExpectVisualizationResults,
|
||||
expectFilterWrongTypeError,
|
||||
expectNotSpaceAwareResults,
|
||||
expectTypeRequired,
|
||||
findTest,
|
||||
|
@ -94,6 +95,31 @@ export default function({ getService }: FtrProviderContext) {
|
|||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithNotSpaceAwareType: {
|
||||
description: 'forbidden login and find globaltype message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('globaltype'),
|
||||
},
|
||||
filterWithHiddenType: {
|
||||
description: 'forbidden find hiddentype message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('hiddentype'),
|
||||
},
|
||||
filterWithUnknownType: {
|
||||
description: 'forbidden find wigwags message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('wigwags'),
|
||||
},
|
||||
filterWithNoType: {
|
||||
description: 'bad request, type is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithUnAllowedType: {
|
||||
description: 'forbidden',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('globaltype'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -136,6 +162,31 @@ export default function({ getService }: FtrProviderContext) {
|
|||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithNotSpaceAwareType: {
|
||||
description: 'only the globaltype',
|
||||
statusCode: 200,
|
||||
response: expectNotSpaceAwareResults,
|
||||
},
|
||||
filterWithHiddenType: {
|
||||
description: 'empty result',
|
||||
statusCode: 200,
|
||||
response: createExpectEmpty(1, 20, 0),
|
||||
},
|
||||
filterWithUnknownType: {
|
||||
description: 'empty result',
|
||||
statusCode: 200,
|
||||
response: createExpectEmpty(1, 20, 0),
|
||||
},
|
||||
filterWithNoType: {
|
||||
description: 'bad request, type is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithUnAllowedType: {
|
||||
description: 'Bad Request',
|
||||
statusCode: 400,
|
||||
response: expectFilterWrongTypeError,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -178,6 +229,31 @@ export default function({ getService }: FtrProviderContext) {
|
|||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithNotSpaceAwareType: {
|
||||
description: 'forbidden login and find globaltype message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('globaltype'),
|
||||
},
|
||||
filterWithHiddenType: {
|
||||
description: 'forbidden find hiddentype message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('hiddentype'),
|
||||
},
|
||||
filterWithUnknownType: {
|
||||
description: 'forbidden find wigwags message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('wigwags'),
|
||||
},
|
||||
filterWithNoType: {
|
||||
description: 'bad request, type is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithUnAllowedType: {
|
||||
description: 'forbidden',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('globaltype'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -220,6 +296,31 @@ export default function({ getService }: FtrProviderContext) {
|
|||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithNotSpaceAwareType: {
|
||||
description: 'only the globaltype',
|
||||
statusCode: 200,
|
||||
response: expectNotSpaceAwareResults,
|
||||
},
|
||||
filterWithHiddenType: {
|
||||
description: 'forbidden find hiddentype message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('hiddentype'),
|
||||
},
|
||||
filterWithUnknownType: {
|
||||
description: 'forbidden find wigwags message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('wigwags'),
|
||||
},
|
||||
filterWithNoType: {
|
||||
description: 'bad request, type is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithUnAllowedType: {
|
||||
description: 'Bad Request',
|
||||
statusCode: 400,
|
||||
response: expectFilterWrongTypeError,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -262,6 +363,31 @@ export default function({ getService }: FtrProviderContext) {
|
|||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithNotSpaceAwareType: {
|
||||
description: 'only the globaltype',
|
||||
statusCode: 200,
|
||||
response: expectNotSpaceAwareResults,
|
||||
},
|
||||
filterWithHiddenType: {
|
||||
description: 'forbidden find hiddentype message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('hiddentype'),
|
||||
},
|
||||
filterWithUnknownType: {
|
||||
description: 'forbidden find wigwags message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('wigwags'),
|
||||
},
|
||||
filterWithNoType: {
|
||||
description: 'bad request, type is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithUnAllowedType: {
|
||||
description: 'Bad Request',
|
||||
statusCode: 400,
|
||||
response: expectFilterWrongTypeError,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -304,6 +430,31 @@ export default function({ getService }: FtrProviderContext) {
|
|||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithNotSpaceAwareType: {
|
||||
description: 'only the globaltype',
|
||||
statusCode: 200,
|
||||
response: expectNotSpaceAwareResults,
|
||||
},
|
||||
filterWithHiddenType: {
|
||||
description: 'forbidden find hiddentype message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('hiddentype'),
|
||||
},
|
||||
filterWithUnknownType: {
|
||||
description: 'forbidden find wigwags message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('wigwags'),
|
||||
},
|
||||
filterWithNoType: {
|
||||
description: 'bad request, type is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithUnAllowedType: {
|
||||
description: 'Bad Request',
|
||||
statusCode: 400,
|
||||
response: expectFilterWrongTypeError,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -346,6 +497,31 @@ export default function({ getService }: FtrProviderContext) {
|
|||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithNotSpaceAwareType: {
|
||||
description: 'only the globaltype',
|
||||
statusCode: 200,
|
||||
response: expectNotSpaceAwareResults,
|
||||
},
|
||||
filterWithHiddenType: {
|
||||
description: 'forbidden find hiddentype message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('hiddentype'),
|
||||
},
|
||||
filterWithUnknownType: {
|
||||
description: 'forbidden find wigwags message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('wigwags'),
|
||||
},
|
||||
filterWithNoType: {
|
||||
description: 'bad request, type is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithUnAllowedType: {
|
||||
description: 'Bad Request',
|
||||
statusCode: 400,
|
||||
response: expectFilterWrongTypeError,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -388,6 +564,31 @@ export default function({ getService }: FtrProviderContext) {
|
|||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithNotSpaceAwareType: {
|
||||
description: 'only the globaltype',
|
||||
statusCode: 200,
|
||||
response: expectNotSpaceAwareResults,
|
||||
},
|
||||
filterWithHiddenType: {
|
||||
description: 'forbidden find hiddentype message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('hiddentype'),
|
||||
},
|
||||
filterWithUnknownType: {
|
||||
description: 'forbidden find wigwags message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('wigwags'),
|
||||
},
|
||||
filterWithNoType: {
|
||||
description: 'bad request, type is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithUnAllowedType: {
|
||||
description: 'Bad Request',
|
||||
statusCode: 400,
|
||||
response: expectFilterWrongTypeError,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -430,6 +631,31 @@ export default function({ getService }: FtrProviderContext) {
|
|||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithNotSpaceAwareType: {
|
||||
description: 'only the globaltype',
|
||||
statusCode: 200,
|
||||
response: expectNotSpaceAwareResults,
|
||||
},
|
||||
filterWithHiddenType: {
|
||||
description: 'forbidden find hiddentype message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('hiddentype'),
|
||||
},
|
||||
filterWithUnknownType: {
|
||||
description: 'forbidden find wigwags message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('wigwags'),
|
||||
},
|
||||
filterWithNoType: {
|
||||
description: 'bad request, type is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithUnAllowedType: {
|
||||
description: 'Bad Request',
|
||||
statusCode: 400,
|
||||
response: expectFilterWrongTypeError,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -472,6 +698,31 @@ export default function({ getService }: FtrProviderContext) {
|
|||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithNotSpaceAwareType: {
|
||||
description: 'forbidden login and find globaltype message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('globaltype'),
|
||||
},
|
||||
filterWithHiddenType: {
|
||||
description: 'forbidden find hiddentype message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('hiddentype'),
|
||||
},
|
||||
filterWithUnknownType: {
|
||||
description: 'forbidden find wigwags message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('wigwags'),
|
||||
},
|
||||
filterWithNoType: {
|
||||
description: 'bad request, type is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithUnAllowedType: {
|
||||
description: 'forbidden',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('globaltype'),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,6 +17,7 @@ export default function({ getService }: FtrProviderContext) {
|
|||
createExpectEmpty,
|
||||
createExpectRbacForbidden,
|
||||
createExpectVisualizationResults,
|
||||
expectFilterWrongTypeError,
|
||||
expectNotSpaceAwareResults,
|
||||
expectTypeRequired,
|
||||
findTest,
|
||||
|
@ -60,6 +61,31 @@ export default function({ getService }: FtrProviderContext) {
|
|||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithNotSpaceAwareType: {
|
||||
description: 'forbidden login and find globaltype message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('globaltype'),
|
||||
},
|
||||
filterWithHiddenType: {
|
||||
description: 'forbidden login and find hiddentype message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('hiddentype'),
|
||||
},
|
||||
filterWithUnknownType: {
|
||||
description: 'forbidden find wigwags message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('wigwags'),
|
||||
},
|
||||
filterWithNoType: {
|
||||
description: 'bad request, type is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithUnAllowedType: {
|
||||
description: 'forbidden',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('globaltype'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -101,6 +127,31 @@ export default function({ getService }: FtrProviderContext) {
|
|||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithNotSpaceAwareType: {
|
||||
description: 'only the globaltype',
|
||||
statusCode: 200,
|
||||
response: expectNotSpaceAwareResults,
|
||||
},
|
||||
filterWithHiddenType: {
|
||||
description: 'empty result',
|
||||
statusCode: 200,
|
||||
response: createExpectEmpty(1, 20, 0),
|
||||
},
|
||||
filterWithUnknownType: {
|
||||
description: 'empty result',
|
||||
statusCode: 200,
|
||||
response: createExpectEmpty(1, 20, 0),
|
||||
},
|
||||
filterWithNoType: {
|
||||
description: 'bad request, type is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithUnAllowedType: {
|
||||
description: 'Bad Request',
|
||||
statusCode: 400,
|
||||
response: expectFilterWrongTypeError,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -142,6 +193,31 @@ export default function({ getService }: FtrProviderContext) {
|
|||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithNotSpaceAwareType: {
|
||||
description: 'forbidden login and find globaltype message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('globaltype'),
|
||||
},
|
||||
filterWithHiddenType: {
|
||||
description: 'forbidden login and find hiddentype message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('hiddentype'),
|
||||
},
|
||||
filterWithUnknownType: {
|
||||
description: 'forbidden find wigwags message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('wigwags'),
|
||||
},
|
||||
filterWithNoType: {
|
||||
description: 'bad request, type is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithUnAllowedType: {
|
||||
description: 'forbidden',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('globaltype'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -183,6 +259,31 @@ export default function({ getService }: FtrProviderContext) {
|
|||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithNotSpaceAwareType: {
|
||||
description: 'only the globaltype',
|
||||
statusCode: 200,
|
||||
response: expectNotSpaceAwareResults,
|
||||
},
|
||||
filterWithHiddenType: {
|
||||
description: 'forbidden find hiddentype message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('hiddentype'),
|
||||
},
|
||||
filterWithUnknownType: {
|
||||
description: 'forbidden find wigwags message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('wigwags'),
|
||||
},
|
||||
filterWithNoType: {
|
||||
description: 'bad request, type is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithUnAllowedType: {
|
||||
description: 'Bad Request',
|
||||
statusCode: 400,
|
||||
response: expectFilterWrongTypeError,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -224,6 +325,31 @@ export default function({ getService }: FtrProviderContext) {
|
|||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithNotSpaceAwareType: {
|
||||
description: 'only the globaltype',
|
||||
statusCode: 200,
|
||||
response: expectNotSpaceAwareResults,
|
||||
},
|
||||
filterWithHiddenType: {
|
||||
description: 'forbidden find hiddentype message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('hiddentype'),
|
||||
},
|
||||
filterWithUnknownType: {
|
||||
description: 'forbidden find wigwags message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('wigwags'),
|
||||
},
|
||||
filterWithNoType: {
|
||||
description: 'bad request, type is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithUnAllowedType: {
|
||||
description: 'Bad Request',
|
||||
statusCode: 400,
|
||||
response: expectFilterWrongTypeError,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -265,6 +391,31 @@ export default function({ getService }: FtrProviderContext) {
|
|||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithNotSpaceAwareType: {
|
||||
description: 'only the globaltype',
|
||||
statusCode: 200,
|
||||
response: expectNotSpaceAwareResults,
|
||||
},
|
||||
filterWithHiddenType: {
|
||||
description: 'forbidden find hiddentype message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('hiddentype'),
|
||||
},
|
||||
filterWithUnknownType: {
|
||||
description: 'forbidden find wigwags message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('wigwags'),
|
||||
},
|
||||
filterWithNoType: {
|
||||
description: 'bad request, type is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithUnAllowedType: {
|
||||
description: 'Bad Request',
|
||||
statusCode: 400,
|
||||
response: expectFilterWrongTypeError,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -306,6 +457,31 @@ export default function({ getService }: FtrProviderContext) {
|
|||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithNotSpaceAwareType: {
|
||||
description: 'only the globaltype',
|
||||
statusCode: 200,
|
||||
response: expectNotSpaceAwareResults,
|
||||
},
|
||||
filterWithHiddenType: {
|
||||
description: 'forbidden find hiddentype message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('hiddentype'),
|
||||
},
|
||||
filterWithUnknownType: {
|
||||
description: 'forbidden find wigwags message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('wigwags'),
|
||||
},
|
||||
filterWithNoType: {
|
||||
description: 'bad request, type is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithUnAllowedType: {
|
||||
description: 'Bad Request',
|
||||
statusCode: 400,
|
||||
response: expectFilterWrongTypeError,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -347,6 +523,31 @@ export default function({ getService }: FtrProviderContext) {
|
|||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithNotSpaceAwareType: {
|
||||
description: 'only the globaltype',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('globaltype'),
|
||||
},
|
||||
filterWithHiddenType: {
|
||||
description: 'forbidden find hiddentype message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('hiddentype'),
|
||||
},
|
||||
filterWithUnknownType: {
|
||||
description: 'forbidden find wigwags message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('wigwags'),
|
||||
},
|
||||
filterWithNoType: {
|
||||
description: 'bad request, type is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithUnAllowedType: {
|
||||
description: 'forbidden',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('globaltype'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -388,6 +589,31 @@ export default function({ getService }: FtrProviderContext) {
|
|||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithNotSpaceAwareType: {
|
||||
description: 'only the globaltype',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('globaltype'),
|
||||
},
|
||||
filterWithHiddenType: {
|
||||
description: 'forbidden find hiddentype message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('hiddentype'),
|
||||
},
|
||||
filterWithUnknownType: {
|
||||
description: 'forbidden find wigwags message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('wigwags'),
|
||||
},
|
||||
filterWithNoType: {
|
||||
description: 'bad request, type is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithUnAllowedType: {
|
||||
description: 'forbidden',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('globaltype'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -429,6 +655,31 @@ export default function({ getService }: FtrProviderContext) {
|
|||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithNotSpaceAwareType: {
|
||||
description: 'only the globaltype',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('globaltype'),
|
||||
},
|
||||
filterWithHiddenType: {
|
||||
description: 'forbidden find hiddentype message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('hiddentype'),
|
||||
},
|
||||
filterWithUnknownType: {
|
||||
description: 'forbidden find wigwags message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('wigwags'),
|
||||
},
|
||||
filterWithNoType: {
|
||||
description: 'bad request, type is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithUnAllowedType: {
|
||||
description: 'forbidden',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('globaltype'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -470,6 +721,31 @@ export default function({ getService }: FtrProviderContext) {
|
|||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithNotSpaceAwareType: {
|
||||
description: 'only the globaltype',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('globaltype'),
|
||||
},
|
||||
filterWithHiddenType: {
|
||||
description: 'forbidden find hiddentype message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('hiddentype'),
|
||||
},
|
||||
filterWithUnknownType: {
|
||||
description: 'forbidden find wigwags message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('wigwags'),
|
||||
},
|
||||
filterWithNoType: {
|
||||
description: 'bad request, type is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithUnAllowedType: {
|
||||
description: 'forbidden',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('globaltype'),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,6 +15,7 @@ export default function({ getService }: FtrProviderContext) {
|
|||
const {
|
||||
createExpectEmpty,
|
||||
createExpectVisualizationResults,
|
||||
expectFilterWrongTypeError,
|
||||
expectNotSpaceAwareResults,
|
||||
expectTypeRequired,
|
||||
findTest,
|
||||
|
@ -59,6 +60,31 @@ export default function({ getService }: FtrProviderContext) {
|
|||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithNotSpaceAwareType: {
|
||||
description: 'only the visualization',
|
||||
statusCode: 200,
|
||||
response: expectNotSpaceAwareResults,
|
||||
},
|
||||
filterWithHiddenType: {
|
||||
description: 'empty result',
|
||||
statusCode: 200,
|
||||
response: createExpectEmpty(1, 20, 0),
|
||||
},
|
||||
filterWithUnknownType: {
|
||||
description: 'empty result',
|
||||
statusCode: 200,
|
||||
response: createExpectEmpty(1, 20, 0),
|
||||
},
|
||||
filterWithNoType: {
|
||||
description: 'bad request, type is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithUnAllowedType: {
|
||||
description: 'Bad Request',
|
||||
statusCode: 400,
|
||||
response: expectFilterWrongTypeError,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -100,6 +126,31 @@ export default function({ getService }: FtrProviderContext) {
|
|||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithNotSpaceAwareType: {
|
||||
description: 'only the visualization',
|
||||
statusCode: 200,
|
||||
response: expectNotSpaceAwareResults,
|
||||
},
|
||||
filterWithHiddenType: {
|
||||
description: 'empty result',
|
||||
statusCode: 200,
|
||||
response: createExpectEmpty(1, 20, 0),
|
||||
},
|
||||
filterWithUnknownType: {
|
||||
description: 'empty result',
|
||||
statusCode: 200,
|
||||
response: createExpectEmpty(1, 20, 0),
|
||||
},
|
||||
filterWithNoType: {
|
||||
description: 'bad request, type is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeRequired,
|
||||
},
|
||||
filterWithUnAllowedType: {
|
||||
description: 'Bad Request',
|
||||
statusCode: 400,
|
||||
response: expectFilterWrongTypeError,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue