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:
Xavier Mouligneau 2019-10-02 18:23:44 -04:00 committed by GitHub
parent bc840e6789
commit d95c47f776
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 2459 additions and 152 deletions

View file

@ -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.

View file

@ -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>>;
```

View file

@ -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> }[]) =&gt; Promise&lt;SavedObjectsBatchResponse&lt;SavedObjectAttributes&gt;&gt;</code> | Returns an array of objects by id |
| [create](./kibana-plugin-public.savedobjectsclient.create.md) | | <code>&lt;T extends SavedObjectAttributes&gt;(type: string, attributes: T, options?: SavedObjectsCreateOptions) =&gt; Promise&lt;SimpleSavedObject&lt;T&gt;&gt;</code> | Persists an object |
| [delete](./kibana-plugin-public.savedobjectsclient.delete.md) | | <code>(type: string, id: string) =&gt; Promise&lt;{}&gt;</code> | Deletes an object |
| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <code>&lt;T extends SavedObjectAttributes&gt;(options: Pick&lt;SavedObjectFindOptionsServer, &quot;search&quot; &#124; &quot;type&quot; &#124; &quot;defaultSearchOperator&quot; &#124; &quot;searchFields&quot; &#124; &quot;sortField&quot; &#124; &quot;hasReference&quot; &#124; &quot;page&quot; &#124; &quot;perPage&quot; &#124; &quot;fields&quot;&gt;) =&gt; Promise&lt;SavedObjectsFindResponsePublic&lt;T&gt;&gt;</code> | Search for objects |
| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <code>&lt;T extends SavedObjectAttributes&gt;(options: Pick&lt;SavedObjectFindOptionsServer, &quot;search&quot; &#124; &quot;filter&quot; &#124; &quot;type&quot; &#124; &quot;page&quot; &#124; &quot;perPage&quot; &#124; &quot;sortField&quot; &#124; &quot;fields&quot; &#124; &quot;searchFields&quot; &#124; &quot;hasReference&quot; &#124; &quot;defaultSearchOperator&quot;&gt;) =&gt; Promise&lt;SavedObjectsFindResponsePublic&lt;T&gt;&gt;</code> | Search for objects |
| [get](./kibana-plugin-public.savedobjectsclient.get.md) | | <code>&lt;T extends SavedObjectAttributes&gt;(type: string, id: string) =&gt; Promise&lt;SimpleSavedObject&lt;T&gt;&gt;</code> | Fetches a single object |
## Methods

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) &gt; [filter](./kibana-plugin-public.savedobjectsfindoptions.filter.md)
## SavedObjectsFindOptions.filter property
<b>Signature:</b>
```typescript
filter?: string;
```

View file

@ -17,6 +17,7 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions
| --- | --- | --- |
| [defaultSearchOperator](./kibana-plugin-public.savedobjectsfindoptions.defaultsearchoperator.md) | <code>'AND' &#124; '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> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [SavedObjectsFindOptions](./kibana-plugin-server.savedobjectsfindoptions.md) &gt; [filter](./kibana-plugin-server.savedobjectsfindoptions.filter.md)
## SavedObjectsFindOptions.filter property
<b>Signature:</b>
```typescript
filter?: string;
```

View file

@ -17,6 +17,7 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions
| --- | --- | --- |
| [defaultSearchOperator](./kibana-plugin-server.savedobjectsfindoptions.defaultsearchoperator.md) | <code>'AND' &#124; '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> | |

View file

@ -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

View file

@ -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';

View file

@ -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> {}

View file

@ -19,5 +19,5 @@
export * from './ast';
export * from './filter_migration';
export * from './node_types';
export { nodeTypes } from './node_types';
export * from './errors';

View 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;

View file

@ -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',

View file

@ -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;

View file

@ -297,6 +297,7 @@ export class SavedObjectsClient {
searchFields: 'search_fields',
sortField: 'sort_field',
type: 'type',
filter: 'filter',
};
const renamedQuery = renameKeys<SavedObjectsFindOptions, any>(renameMap, options);

View file

@ -56,6 +56,7 @@ export {
SavedObjectsClientWrapperFactory,
SavedObjectsClientWrapperOptions,
SavedObjectsErrorHelpers,
SavedObjectsCacheIndexPatterns,
} from './lib';
export * from './saved_objects_client';

View file

@ -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' });
});
});

View file

@ -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}`);
}
}
}

View 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,
},
]);
});
});
});

View 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;
};

View file

@ -26,3 +26,5 @@ export {
} from './scoped_client_provider';
export { SavedObjectsErrorHelpers } from './errors';
export { SavedObjectsCacheIndexPatterns } from './cache_index_patterns';

View file

@ -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);

View file

@ -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);

View file

@ -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'],
},
},
],
},
},
});
});
});
});

View file

@ -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

View file

@ -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', () => {

View file

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

View file

@ -123,6 +123,7 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions {
searchFields?: string[];
hasReference?: { type: string; id: string };
defaultSearchOperator?: 'AND' | 'OR';
filter?: string;
}
/**

View file

@ -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;

View file

@ -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']>;

View file

@ -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) {

View file

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

View file

@ -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,

View file

@ -84,6 +84,11 @@ describe('Saved Objects Mixin', () => {
get: stubConfig,
};
},
indexPatternsServiceFactory: () => {
return {
getFieldsForWildcard: jest.fn(),
};
},
plugins: {
elasticsearch: {
getCluster: () => {

View file

@ -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,

View file

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

View file

@ -14,9 +14,9 @@
"**/*.ts",
"**/*.tsx",
"../typings/lodash.topath/*.ts",
"typings/**/*",
"typings/**/*"
],
"exclude": [
"plugin_functional/plugins/**/*"
]
}
}

View file

@ -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];

View file

@ -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,

View file

@ -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'),
},
},
});
});

View file

@ -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'),
},
},
});
});

View file

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