From b699c706b0c3cdaf78e6cd95379aa6dca8fa75be Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Mon, 13 Apr 2020 21:40:05 -0400 Subject: [PATCH 01/21] step 1 to add aggs in the find function of saved object --- .../saved_objects/saved_objects_client.ts | 1 + .../saved_objects/service/lib/aggs_utils.ts | 46 ++++ .../server/saved_objects/service/lib/index.ts | 2 + .../saved_objects/service/lib/repository.ts | 12 ++ .../bucket_aggs/index.ts | 48 +++++ .../saved_object_aggs_types/helpers.test.ts | 196 ++++++++++++++++++ .../lib/saved_object_aggs_types/helpers.ts | 122 +++++++++++ .../lib/saved_object_aggs_types/index.ts | 45 ++++ .../metrics_aggs/index.ts | 74 +++++++ .../service/saved_objects_client.ts | 8 +- src/core/server/saved_objects/types.ts | 3 +- .../common/suites/find.ts | 24 +-- 12 files changed, 567 insertions(+), 14 deletions(-) create mode 100644 src/core/server/saved_objects/service/lib/aggs_utils.ts create mode 100644 src/core/server/saved_objects/service/lib/saved_object_aggs_types/bucket_aggs/index.ts create mode 100644 src/core/server/saved_objects/service/lib/saved_object_aggs_types/helpers.test.ts create mode 100644 src/core/server/saved_objects/service/lib/saved_object_aggs_types/helpers.ts create mode 100644 src/core/server/saved_objects/service/lib/saved_object_aggs_types/index.ts create mode 100644 src/core/server/saved_objects/service/lib/saved_object_aggs_types/metrics_aggs/index.ts diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index afc77806afb91..78acf1c76d3c4 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -306,6 +306,7 @@ export class SavedObjectsClient { sortField: 'sort_field', type: 'type', filter: 'filter', + aggs: 'aggs', }; const renamedQuery = renameKeys(renameMap, options); diff --git a/src/core/server/saved_objects/service/lib/aggs_utils.ts b/src/core/server/saved_objects/service/lib/aggs_utils.ts new file mode 100644 index 0000000000000..0e7bc130bb3a5 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/aggs_utils.ts @@ -0,0 +1,46 @@ +/* + * 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 { IndexMapping } from '../../mappings'; + +import { SavedObjectsErrorHelpers } from './errors'; +import { hasFilterKeyError } from './filter_utils'; +import { SavedObjectAggs, validateSavedObjectTypeAggs } from './saved_object_aggs_types'; + +export const validateSavedObjectAggs = ( + allowedTypes: string[], + aggs: SavedObjectAggs, + indexMapping: IndexMapping +) => { + validateSavedObjectTypeAggs(aggs); + validateAggFieldValue(allowedTypes, aggs, indexMapping); +}; + +const validateAggFieldValue = (allowedTypes: string[], aggs: any, indexMapping: IndexMapping) => { + Object.keys(aggs).forEach(key => { + if (key === 'field') { + const error = hasFilterKeyError(key, allowedTypes, indexMapping); + if (error != null) { + throw SavedObjectsErrorHelpers.createBadRequestError(error); + } + } else if (typeof aggs[key] === 'object') { + validateAggFieldValue(allowedTypes, aggs[key], indexMapping); + } + }); +}; diff --git a/src/core/server/saved_objects/service/lib/index.ts b/src/core/server/saved_objects/service/lib/index.ts index e103120388e35..d338e939ffb9c 100644 --- a/src/core/server/saved_objects/service/lib/index.ts +++ b/src/core/server/saved_objects/service/lib/index.ts @@ -30,3 +30,5 @@ export { } from './scoped_client_provider'; export { SavedObjectsErrorHelpers } from './errors'; + +export { SavedObjectAggs } from './saved_object_aggs_types'; diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 5f17c11792763..37a0dcdd3bc22 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -57,6 +57,7 @@ import { } from '../../types'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { validateConvertFilterToKueryNode } from './filter_utils'; +import { validateSavedObjectAggs } from './aggs_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. @@ -592,6 +593,7 @@ export class SavedObjectsRepository { * @property {Array} [options.fields] * @property {string} [options.namespace] * @property {object} [options.hasReference] - { type, id } + * @property {object} [options.aggs] - see ./saved_object_aggs for more insight * @returns {promise} - { saved_objects: [{ id, type, version, attributes }], total, per_page, page } */ async find({ @@ -607,6 +609,7 @@ export class SavedObjectsRepository { namespace, type, filter, + aggs, }: SavedObjectsFindOptions): Promise> { if (!type) { throw SavedObjectsErrorHelpers.createBadRequestError( @@ -647,6 +650,14 @@ export class SavedObjectsRepository { } } + try { + if (aggs) { + validateSavedObjectAggs(allowedTypes, aggs, this._mappings); + } + } catch (e) { + throw e; + } + const esOptions = { index: this.getIndicesForTypes(allowedTypes), size: perPage, @@ -656,6 +667,7 @@ export class SavedObjectsRepository { rest_total_hits_as_int: true, body: { seq_no_primary_term: true, + ...(aggs != null ? { aggs } : {}), ...getSearchDsl(this._mappings, this._registry, { search, defaultSearchOperator, diff --git a/src/core/server/saved_objects/service/lib/saved_object_aggs_types/bucket_aggs/index.ts b/src/core/server/saved_objects/service/lib/saved_object_aggs_types/bucket_aggs/index.ts new file mode 100644 index 0000000000000..8be958a94b4c1 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/saved_object_aggs_types/bucket_aggs/index.ts @@ -0,0 +1,48 @@ +/* + * 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 * as rt from 'io-ts'; + +import { FieldBasicRT } from '../helpers'; + +export const BucketAggsTypeRt = rt.partial({ + filter: rt.type({ + term: rt.record(rt.string, rt.string), + }), + histogram: rt.intersection([ + FieldBasicRT, + rt.type({ interval: rt.number }), + rt.partial({ + min_doc_count: rt.number, + extended_bounds: rt.type({ min: rt.number, max: rt.number }), + keyed: rt.boolean, + missing: rt.number, + order: rt.record(rt.string, rt.literal('asc', 'desc')), + }), + ]), + terms: rt.intersection([ + FieldBasicRT, + rt.partial({ + field: rt.string, + size: rt.number, + show_term_doc_count_error: rt.boolean, + order: rt.record(rt.string, rt.literal('asc', 'desc')), + }), + ]), +}); diff --git a/src/core/server/saved_objects/service/lib/saved_object_aggs_types/helpers.test.ts b/src/core/server/saved_objects/service/lib/saved_object_aggs_types/helpers.test.ts new file mode 100644 index 0000000000000..1a72f14497783 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/saved_object_aggs_types/helpers.test.ts @@ -0,0 +1,196 @@ +/* + * 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 * as rt from 'io-ts'; +import { PathReporter } from 'io-ts/lib/PathReporter'; + +import { excess } from './helpers'; + +const runDecode = (codec: rt.Type, data: any) => { + const result = codec.decode(data); + return PathReporter.report(result); +}; + +describe('Saved object aggs helpers', () => { + describe('happy path', () => { + test('excess Record', () => { + const codec = excess( + rt.record( + rt.string, + rt.partial({ + max: rt.type({ field: rt.string }), + }) + ) + ); + + expect(runDecode(codec, { aggName: { max: { field: 'hi' } } })).toEqual(['No errors!']); + }); + + test('excess Record', () => { + const codec = excess( + rt.record( + rt.string, + rt.intersection([ + rt.partial({ + max: rt.type({ field: rt.string }), + }), + rt.partial({ + min: rt.type({ field: rt.string }), + }), + ]) + ) + ); + + expect(runDecode(codec, { aggName: { min: { field: 'hi' } } })).toEqual(['No errors!']); + }); + + test('When you intersection as a DictionnaryType', () => { + const codec = excess( + rt.record( + rt.string, + rt.intersection([ + rt.partial({ + max: rt.type({ field: rt.string }), + }), + rt.partial({ + filter: rt.type({ field: rt.string }), + aggs: rt.record( + rt.string, + rt.partial({ + min: rt.type({ field: rt.string }), + }) + ), + }), + ]) + ) + ); + + expect( + runDecode(codec, { + aggName: { filter: { field: 'hi' }, aggs: { aggNewName: { min: { field: 'low' } } } }, + }) + ).toEqual(['No errors!']); + }); + }); + + describe('Errors', () => { + test('throw error when you add an attributes who is not expected for Record', () => { + const codec = excess( + rt.record( + rt.string, + rt.partial({ + max: rt.type({ field: rt.string }), + }) + ) + ); + + expect(runDecode(codec, { aggName: { max: { field: 'hi', script: '' } } })).toEqual([ + 'Invalid value {"aggName":{"max":{"field":"hi","script":""}}} supplied to : { [K in string]: Partial<{ max: { field: string } }> }, excess properties: ["script"]', + ]); + }); + + test('throw error when you add an attributes who is not expected for Record', () => { + const codec = excess( + rt.record( + rt.string, + rt.intersection([ + rt.partial({ + max: rt.type({ field: rt.string }), + }), + rt.partial({ + min: rt.type({ field: rt.string }), + }), + ]) + ) + ); + + expect(runDecode(codec, { aggName: { min: { field: 'hi', script: 'field' } } })).toEqual([ + 'Invalid value {"aggName":{"min":{"field":"hi","script":"field"}}} supplied to : { [K in string]: (Partial<{ max: { field: string } }> & Partial<{ min: { field: string } }>) }, excess properties: ["script"]', + ]); + }); + + test('throw error when you do not match types for Record', () => { + const codec = excess( + rt.record( + rt.string, + rt.partial({ + max: rt.type({ field: rt.string }), + }) + ) + ); + + expect(runDecode(codec, { aggName: { max: { field: 33 } } })).toEqual([ + 'Invalid value 33 supplied to : { [K in string]: Partial<{ max: { field: string } }> }/aggName: Partial<{ max: { field: string } }>/max: { field: string }/field: string', + ]); + }); + + test('throw error when when you do not match types for Record', () => { + const codec = excess( + rt.record( + rt.string, + rt.intersection([ + rt.partial({ + max: rt.type({ field: rt.string }), + }), + rt.partial({ + min: rt.type({ field: rt.string }), + }), + ]) + ) + ); + + expect(runDecode(codec, { aggName: { min: { field: 33 } } })).toEqual([ + 'Invalid value 33 supplied to : { [K in string]: (Partial<{ max: { field: string } }> & Partial<{ min: { field: string } }>) }/aggName: (Partial<{ max: { field: string } }> & Partial<{ min: { field: string } }>)/1: Partial<{ min: { field: string } }>/min: { field: string }/field: string', + ]); + }); + + test('throw error when you add an attributes in your second agg who is not expected for Record', () => { + const codec = excess( + rt.record( + rt.string, + rt.intersection([ + rt.partial({ + max: rt.type({ field: rt.string }), + }), + rt.partial({ + filter: rt.type({ field: rt.string }), + aggs: rt.record( + rt.string, + rt.partial({ + min: rt.type({ field: rt.string }), + }) + ), + }), + ]) + ) + ); + + expect( + runDecode(codec, { + aggName: { + filter: { field: 'hi' }, + aggs: { aggNewName: { min: { field: 'low' }, script: 'error' } }, + }, + }) + ).toEqual([ + 'Invalid value {"aggName":{"filter":{"field":"hi"},"aggs":{"aggNewName":{"min":{"field":"low"},"script":"error"}}}} supplied to : { [K in string]: (Partial<{ max: { field: string } }> & Partial<{ filter: { field: string }, aggs: { [K in string]: Partial<{ min: { field: string } }> } }>) }, excess properties: ["script"]', + ]); + }); + }); +}); diff --git a/src/core/server/saved_objects/service/lib/saved_object_aggs_types/helpers.ts b/src/core/server/saved_objects/service/lib/saved_object_aggs_types/helpers.ts new file mode 100644 index 0000000000000..0ea5c3d9301bb --- /dev/null +++ b/src/core/server/saved_objects/service/lib/saved_object_aggs_types/helpers.ts @@ -0,0 +1,122 @@ +/* + * 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 { either } from 'fp-ts/lib/Either'; +import * as rt from 'io-ts'; +import { failure } from 'io-ts/lib/PathReporter'; +import { get } from 'lodash'; + +type ErrorFactory = (message: string) => Error; + +export const FieldBasicRT = rt.type({ field: rt.string }); + +export const throwErrors = (createError: ErrorFactory) => (errors: rt.Errors) => { + throw createError(failure(errors).join('\n')); +}; + +const getProps = (codec: rt.HasProps | rt.RecordC): rt.Props | null => { + if (codec == null) { + return null; + } + switch (codec._tag) { + case 'DictionaryType': + if (codec.codomain.props != null) { + return codec.codomain.props; + } + const types: rt.HasProps[] = codec.codomain.types; + return types.reduce((props, type) => Object.assign(props, getProps(type)), {}); + case 'RefinementType': + case 'ReadonlyType': + return getProps(codec.type); + case 'InterfaceType': + case 'StrictType': + case 'PartialType': + return codec.props; + case 'IntersectionType': + return codec.types.reduce( + (props, type) => Object.assign(props, getProps(type)), + {} + ); + default: + return null; + } +}; + +const getExcessProps = ( + props: rt.Props | rt.RecordC, + r: Record +): string[] => + Object.keys(r).reduce((acc, k) => { + const codecChildren = get>(props, [k]); + const childrenProps = getProps(codecChildren); + const childrenObject = r[k] as Record; + if (codecChildren != null && childrenProps != null && codecChildren._tag === 'DictionaryType') { + const keys = Object.keys(childrenObject); + return [ + ...acc, + ...keys.reduce( + (kAcc, i) => [...kAcc, ...getExcessProps(childrenProps, childrenObject[i])], + [] + ), + ]; + } + if (props.hasOwnProperty(k) && childrenProps != null) { + return [...acc, ...getExcessProps(childrenProps, childrenObject)]; + } else if (!props.hasOwnProperty(k)) { + return [...acc, k]; + } + return acc; + }, []); + +export const excess = (codec: rt.RecordC): rt.InterfaceType => { + const codecProps = getProps(codec); + + const r = new rt.DictionaryType( + codec.name, + codec.is, + (u, c) => + either.chain(codec.validate(u, c), (o: Record) => { + if (codecProps == null) { + return rt.failure( + u, + c, + `Invalid Aggs object ${JSON.stringify(u)} supplied to : ${codec.name}` + ); + } + const keys = Object.keys(o); + const ex = keys.reduce((acc, k) => { + return [...acc, ...getExcessProps(codecProps, o[k])]; + }, []); + + return ex.length > 0 + ? rt.failure( + u, + c, + `Invalid value ${JSON.stringify(u)} supplied to : ${ + codec.name + }, excess properties: ${JSON.stringify(ex)}` + ) + : codec.validate(u, c); + }), + codec.encode, + codec.domain, + codec.codomain + ); + return r as any; +}; diff --git a/src/core/server/saved_objects/service/lib/saved_object_aggs_types/index.ts b/src/core/server/saved_objects/service/lib/saved_object_aggs_types/index.ts new file mode 100644 index 0000000000000..68461ebbe3ee0 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/saved_object_aggs_types/index.ts @@ -0,0 +1,45 @@ +/* + * 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 { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; +import { pipe } from 'fp-ts/lib/pipeable'; +import * as rt from 'io-ts'; + +import { BucketAggsTypeRt } from './bucket_aggs'; +import { MetricsAggsTypeRt } from './metrics_aggs'; + +import { SavedObjectsErrorHelpers } from '../errors'; +import { excess, throwErrors } from './helpers'; + +const AllAggsRt = rt.intersection([BucketAggsTypeRt, MetricsAggsTypeRt]); + +const SavedObjectAggsRt = rt.record( + rt.string, + rt.intersection([AllAggsRt, rt.partial({ aggs: AllAggsRt })]) +); + +export type SavedObjectAggs = rt.TypeOf; + +export const validateSavedObjectTypeAggs = (aggObjects: SavedObjectAggs) => { + pipe( + excess(SavedObjectAggsRt).decode(aggObjects), + fold(throwErrors(SavedObjectsErrorHelpers.createBadRequestError), identity) + ); +}; diff --git a/src/core/server/saved_objects/service/lib/saved_object_aggs_types/metrics_aggs/index.ts b/src/core/server/saved_objects/service/lib/saved_object_aggs_types/metrics_aggs/index.ts new file mode 100644 index 0000000000000..169d8d5c426a0 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/saved_object_aggs_types/metrics_aggs/index.ts @@ -0,0 +1,74 @@ +/* + * 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 * as rt from 'io-ts'; + +import { FieldBasicRT } from '../helpers'; + +/* + * Types for Metrics Aggregations + * + * TODO: + * - Extended Stats Aggregation https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-extendedstats-aggregation.html + * - Geo Bounds Aggregation https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-geobounds-aggregation.html + * - Geo Centroid Aggregation https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-geocentroid-aggregation.html + * - Percentiles Aggregation https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-percentile-aggregation.html + * - Percentile Ranks Aggregation https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-percentile-rank-aggregation.html + * - Scripted Metric Aggregation https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-scripted-metric-aggregation.html + * - Stats Aggregation https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-stats-aggregation.html + * - String Stats Aggregation (x-pack) https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-string-stats-aggregation.html + * - Sum Aggregation https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-sum-aggregation.html + * - Top Hits Aggregation https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-top-hits-aggregation.html + * - Value Count Aggregation https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-valuecount-aggregation.html + * - Median Absolute Deviation Aggregation https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-median-absolute-deviation-aggregation.html + */ + +const MinMaxAggBodyRt = rt.intersection([FieldBasicRT, rt.partial({ missing: rt.number })]); + +export const MetricsAggsTypeRt = rt.partial({ + avg: FieldBasicRT, + weighted_avg: rt.intersection([ + rt.type({ + value: rt.intersection([FieldBasicRT, rt.partial({ missing: rt.number })]), + weight: rt.intersection([FieldBasicRT, rt.partial({ missing: rt.number })]), + }), + rt.partial({ + format: rt.string, + value_type: rt.string, + }), + ]), + cardinality: FieldBasicRT, + max: MinMaxAggBodyRt, + min: MinMaxAggBodyRt, + top_hits: rt.partial({ + explain: rt.boolean, + from: rt.string, + highlight: rt.any, + seq_no_primary_term: rt.boolean, + size: rt.number, + sort: rt.any, + stored_fields: rt.array(rt.string), + version: rt.boolean, + _name: rt.string, + _source: rt.partial({ + includes: rt.array(rt.string), + excludes: rt.array(rt.string), + }), + }), +}); diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts index 8780f07cc3091..19fb504509c12 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -17,7 +17,7 @@ * under the License. */ -import { ISavedObjectsRepository } from './lib'; +import { ISavedObjectsRepository, SavedObjectAggs } from './lib'; import { SavedObject, SavedObjectReference, @@ -182,6 +182,12 @@ export interface SavedObjectsUpdateResponse references: SavedObjectReference[] | undefined; } +/** + * + * @public + */ +export { SavedObjectAggs }; + /** * * @public diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index 9efc82603b179..44e54119d87f8 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -17,7 +17,7 @@ * under the License. */ -import { SavedObjectsClient } from './service/saved_objects_client'; +import { SavedObjectsClient, SavedObjectAggs } from './service/saved_objects_client'; import { SavedObjectsTypeMappingDefinition, SavedObjectsTypeMappingDefinitions } from './mappings'; import { SavedObjectMigrationMap } from './migrations'; import { PropertyValidators } from './validation'; @@ -82,6 +82,7 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { hasReference?: { type: string; id: string }; defaultSearchOperator?: 'AND' | 'OR'; filter?: string; + aggs?: SavedObjectAggs; } /** diff --git a/x-pack/test/saved_object_api_integration/common/suites/find.ts b/x-pack/test/saved_object_api_integration/common/suites/find.ts index 75d6653365fdf..2fda8bb0aaab0 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/find.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/find.ts @@ -34,7 +34,7 @@ export interface FindTestCase { failure?: 400 | 403; } -export const getTestCases = (spaceId?: string) => ({ +export const getTestCases = (spaceId?: string): Record => ({ singleNamespaceType: { title: 'find single-namespace type', query: 'type=isolatedtype&fields=title', @@ -46,7 +46,7 @@ export const getTestCases = (spaceId?: string) => ({ ? CASES.SINGLE_NAMESPACE_SPACE_2 : CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, }, - } as FindTestCase, + }, multiNamespaceType: { title: 'find multi-namespace type', query: 'type=sharedtype&fields=title', @@ -58,41 +58,41 @@ export const getTestCases = (spaceId?: string) => ({ ? CASES.MULTI_NAMESPACE_ONLY_SPACE_2 : CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, }, - } as FindTestCase, + }, namespaceAgnosticType: { title: 'find namespace-agnostic type', query: 'type=globaltype&fields=title', successResult: { savedObjects: CASES.NAMESPACE_AGNOSTIC }, - } as FindTestCase, - hiddenType: { title: 'find hidden type', query: 'type=hiddentype&fields=name' } as FindTestCase, - unknownType: { title: 'find unknown type', query: 'type=wigwags' } as FindTestCase, + }, + hiddenType: { title: 'find hidden type', query: 'type=hiddentype&fields=name' }, + unknownType: { title: 'find unknown type', query: 'type=wigwags' }, pageBeyondTotal: { title: 'find page beyond total', query: 'type=isolatedtype&page=100&per_page=100', successResult: { page: 100, perPage: 100, total: 1, savedObjects: [] }, - } as FindTestCase, + }, unknownSearchField: { title: 'find unknown search field', query: 'type=url&search_fields=a', - } as FindTestCase, + }, filterWithNamespaceAgnosticType: { title: 'filter with namespace-agnostic type', query: 'type=globaltype&filter=globaltype.attributes.title:*global*', successResult: { savedObjects: CASES.NAMESPACE_AGNOSTIC }, - } as FindTestCase, + }, filterWithHiddenType: { title: 'filter with hidden type', query: `type=hiddentype&fields=name&filter=hiddentype.attributes.title:'hello'`, - } as FindTestCase, + }, filterWithUnknownType: { title: 'filter with unknown type', query: `type=wigwags&filter=wigwags.attributes.title:'unknown'`, - } as FindTestCase, + }, filterWithDisallowedType: { title: 'filter with disallowed type', query: `type=globaltype&filter=dashboard.title:'Requests'`, failure: 400, - } as FindTestCase, + }, }); export const createRequest = ({ query }: FindTestCase) => ({ query }); const getTestTitle = ({ failure, title }: FindTestCase) => { From 3819cd520059263e30287e98b360bb278f8a8320 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Wed, 15 Apr 2020 15:53:24 -0400 Subject: [PATCH 02/21] setp 2 - add specific unit test to aggs + fix bug found during integrations --- .../saved_objects/saved_objects_client.ts | 9 ++- src/core/server/saved_objects/routes/find.ts | 2 + .../saved_objects/service/lib/aggs_utils.ts | 28 +++++--- .../saved_objects/service/lib/repository.ts | 13 ++-- .../service/saved_objects_client.ts | 7 +- src/core/server/saved_objects/types.ts | 2 +- .../apis/saved_objects/find.js | 70 +++++++++++++++++++ 7 files changed, 112 insertions(+), 19 deletions(-) diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index 78acf1c76d3c4..90bdc7ecb3751 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -106,7 +106,9 @@ export interface SavedObjectsBatchResponse { * * @public */ -export interface SavedObjectsFindResponsePublic extends SavedObjectsBatchResponse { +export interface SavedObjectsFindResponsePublic + extends SavedObjectsBatchResponse { + aggregations?: A; total: number; perPage: number; page: number; @@ -291,7 +293,7 @@ export class SavedObjectsClient { * @property {object} [options.hasReference] - { type, id } * @returns A find result with objects matching the specified search. */ - public find = ( + public find = ( options: SavedObjectsFindOptions ): Promise> => { const path = this.getPath(['_find']); @@ -323,13 +325,14 @@ export class SavedObjectsClient { SavedObjectsFindResponsePublic >( { + aggregations: 'aggregations', saved_objects: 'savedObjects', total: 'total', per_page: 'perPage', page: 'page', }, resp - ) as SavedObjectsFindResponsePublic; + ) as SavedObjectsFindResponsePublic; }); }; diff --git a/src/core/server/saved_objects/routes/find.ts b/src/core/server/saved_objects/routes/find.ts index 5c1c2c9a9ab87..4af5acd585097 100644 --- a/src/core/server/saved_objects/routes/find.ts +++ b/src/core/server/saved_objects/routes/find.ts @@ -45,6 +45,7 @@ export const registerFindRoute = (router: IRouter) => { ), fields: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])), filter: schema.maybe(schema.string()), + aggs: schema.maybe(schema.string()), }), }, }, @@ -62,6 +63,7 @@ export const registerFindRoute = (router: IRouter) => { hasReference: query.has_reference, fields: typeof query.fields === 'string' ? [query.fields] : query.fields, filter: query.filter, + aggs: query.aggs, }); return res.ok({ body: result }); diff --git a/src/core/server/saved_objects/service/lib/aggs_utils.ts b/src/core/server/saved_objects/service/lib/aggs_utils.ts index 0e7bc130bb3a5..21084c1ec2c84 100644 --- a/src/core/server/saved_objects/service/lib/aggs_utils.ts +++ b/src/core/server/saved_objects/service/lib/aggs_utils.ts @@ -23,24 +23,36 @@ import { SavedObjectsErrorHelpers } from './errors'; import { hasFilterKeyError } from './filter_utils'; import { SavedObjectAggs, validateSavedObjectTypeAggs } from './saved_object_aggs_types'; -export const validateSavedObjectAggs = ( +export const validateGetSavedObjectAggs = ( allowedTypes: string[], aggs: SavedObjectAggs, indexMapping: IndexMapping -) => { +): SavedObjectAggs => { validateSavedObjectTypeAggs(aggs); - validateAggFieldValue(allowedTypes, aggs, indexMapping); + return validateGetAggFieldValue(allowedTypes, aggs, indexMapping); }; -const validateAggFieldValue = (allowedTypes: string[], aggs: any, indexMapping: IndexMapping) => { - Object.keys(aggs).forEach(key => { +const validateGetAggFieldValue = ( + allowedTypes: string[], + aggs: any, + indexMapping: IndexMapping +) => { + return Object.keys(aggs).reduce((acc, key) => { if (key === 'field') { - const error = hasFilterKeyError(key, allowedTypes, indexMapping); + const error = hasFilterKeyError(aggs[key], allowedTypes, indexMapping); if (error != null) { throw SavedObjectsErrorHelpers.createBadRequestError(error); } + return { + ...acc, + [key]: aggs[key].replace('.attributes', ''), + }; } else if (typeof aggs[key] === 'object') { - validateAggFieldValue(allowedTypes, aggs[key], indexMapping); + return { ...acc, [key]: validateGetAggFieldValue(allowedTypes, aggs[key], indexMapping) }; } - }); + return { + ...acc, + [key]: aggs[key], + }; + }, {}); }; diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 37a0dcdd3bc22..78a5e43ee543d 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -47,6 +47,7 @@ import { SavedObjectsDeleteOptions, SavedObjectsAddToNamespacesOptions, SavedObjectsDeleteFromNamespacesOptions, + SavedObjectAggs, } from '../saved_objects_client'; import { SavedObject, @@ -57,7 +58,7 @@ import { } from '../../types'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { validateConvertFilterToKueryNode } from './filter_utils'; -import { validateSavedObjectAggs } from './aggs_utils'; +import { validateGetSavedObjectAggs } from './aggs_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. @@ -596,7 +597,7 @@ export class SavedObjectsRepository { * @property {object} [options.aggs] - see ./saved_object_aggs for more insight * @returns {promise} - { saved_objects: [{ id, type, version, attributes }], total, per_page, page } */ - async find({ + async find({ search, defaultSearchOperator = 'OR', searchFields, @@ -610,7 +611,7 @@ export class SavedObjectsRepository { type, filter, aggs, - }: SavedObjectsFindOptions): Promise> { + }: SavedObjectsFindOptions): Promise> { if (!type) { throw SavedObjectsErrorHelpers.createBadRequestError( 'options.type must be a string or an array of strings' @@ -650,9 +651,10 @@ export class SavedObjectsRepository { } } + let aggsObject = null; try { if (aggs) { - validateSavedObjectAggs(allowedTypes, aggs, this._mappings); + aggsObject = validateGetSavedObjectAggs(allowedTypes, JSON.parse(aggs), this._mappings); } } catch (e) { throw e; @@ -667,7 +669,7 @@ export class SavedObjectsRepository { rest_total_hits_as_int: true, body: { seq_no_primary_term: true, - ...(aggs != null ? { aggs } : {}), + ...(aggsObject != null ? { aggs: aggsObject } : {}), ...getSearchDsl(this._mappings, this._registry, { search, defaultSearchOperator, @@ -696,6 +698,7 @@ export class SavedObjectsRepository { } return { + ...(response.aggregations != null ? { aggregations: response.aggregations } : {}), page, per_page: perPage, total: response.hits.total, diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts index 19fb504509c12..047048c68179e 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -87,7 +87,8 @@ export interface SavedObjectsBulkResponse { * * @public */ -export interface SavedObjectsFindResponse { +export interface SavedObjectsFindResponse { + aggregations?: A; saved_objects: Array>; total: number; per_page: number; @@ -243,7 +244,9 @@ export class SavedObjectsClient { * * @param options */ - async find(options: SavedObjectsFindOptions): Promise> { + async find( + options: SavedObjectsFindOptions + ): Promise> { return await this._repository.find(options); } diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index 44e54119d87f8..414487012cc30 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -82,7 +82,7 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { hasReference?: { type: string; id: string }; defaultSearchOperator?: 'AND' | 'OR'; filter?: string; - aggs?: SavedObjectAggs; + aggs?: string; } /** diff --git a/test/api_integration/apis/saved_objects/find.js b/test/api_integration/apis/saved_objects/find.js index 54a19602fd414..7db7e5789816e 100644 --- a/test/api_integration/apis/saved_objects/find.js +++ b/test/api_integration/apis/saved_objects/find.js @@ -182,6 +182,76 @@ export default function({ getService }) { }); })); }); + + describe('with a aggs', () => { + it('should return 200 with a valid response', async () => + await supertest + .get( + `/api/saved_objects/_find?type=visualization&per_page=0&aggs=${encodeURIComponent( + JSON.stringify({ + type_count: { max: { field: 'visualization.attributes.version' } }, + }) + )}` + ) + .expect(200) + .then(resp => { + expect(resp.body).to.eql({ + aggregations: { + type_count: { + value: 1, + }, + }, + page: 1, + per_page: 0, + saved_objects: [], + total: 1, + }); + })); + + it('wrong type should return 400 with Bad Request', async () => + await supertest + .get( + `/api/saved_objects/_find?type=visualization&per_page=0&aggs=${encodeURIComponent( + JSON.stringify({ + type_count: { max: { field: 'dashboard.attributes.version' } }, + }) + )}` + ) + .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, + }); + })); + + it('adding a wrong attributes should return 400 with Bad Request', async () => + await supertest + .get( + `/api/saved_objects/_find?type=visualization&per_page=0&aggs=${encodeURIComponent( + JSON.stringify({ + type_count: { + max: { + field: 'dashboard.attributes.version', + script: 'Oh yes I am going to a script', + }, + }, + }) + )}` + ) + .expect(400) + .then(resp => { + console.log('body', JSON.stringify(resp.body)); + expect(resp.body).to.eql({ + error: 'Bad Request', + message: + 'Invalid value {"type_count":{"max":{"field":"dashboard.attributes.version","script":"Oh yes I am going to a script"}}} supplied to : { [K in string]: ((Partial<{ filter: { term: { [K in string]: string } }, histogram: ({ field: string } & { interval: number } & Partial<{ min_doc_count: number, extended_bounds: { min: number, max: number }, keyed: boolean, missing: number, order: { [K in string]: desc } }>), terms: ({ field: string } & Partial<{ field: string, size: number, show_term_doc_count_error: boolean, order: { [K in string]: desc } }>) }> & Partial<{ avg: { field: string }, weighted_avg: ({ value: ({ field: string } & Partial<{ missing: number }>), weight: ({ field: string } & Partial<{ missing: number }>) } & Partial<{ format: string, value_type: string }>), cardinality: { field: string }, max: ({ field: string } & Partial<{ missing: number }>), min: ({ field: string } & Partial<{ missing: number }>), top_hits: Partial<{ explain: boolean, from: string, highlight: any, seq_no_primary_term: boolean, size: number, sort: any, stored_fields: Array, version: boolean, _name: string, _source: Partial<{ includes: Array, excludes: Array }> }> }>) & Partial<{ aggs: (Partial<{ filter: { term: { [K in string]: string } }, histogram: ({ field: string } & { interval: number } & Partial<{ min_doc_count: number, extended_bounds: { min: number, max: number }, keyed: boolean, missing: number, order: { [K in string]: desc } }>), terms: ({ field: string } & Partial<{ field: string, size: number, show_term_doc_count_error: boolean, order: { [K in string]: desc } }>) }> & Partial<{ avg: { field: string }, weighted_avg: ({ value: ({ field: string } & Partial<{ missing: number }>), weight: ({ field: string } & Partial<{ missing: number }>) } & Partial<{ format: string, value_type: string }>), cardinality: { field: string }, max: ({ field: string } & Partial<{ missing: number }>), min: ({ field: string } & Partial<{ missing: number }>), top_hits: Partial<{ explain: boolean, from: string, highlight: any, seq_no_primary_term: boolean, size: number, sort: any, stored_fields: Array, version: boolean, _name: string, _source: Partial<{ includes: Array, excludes: Array }> }> }>) }>) }, excess properties: ["script"]: Bad Request', + statusCode: 400, + }); + })); + }); }); describe('without kibana index', () => { From 1c4bac9e09b1020ab64d5b514653c15516c4d933 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Mon, 20 Apr 2020 12:26:40 -0400 Subject: [PATCH 03/21] step 3 - add security api_integration arounds aggs --- .../saved_objects/spaces/data.json | 3 +- .../saved_objects/spaces/mappings.json | 3 ++ .../mappings.json | 3 ++ .../common/suites/find.ts | 45 ++++++++++++++++++- .../security_and_spaces/apis/find.ts | 4 ++ .../security_only/apis/find.ts | 4 ++ 6 files changed, 60 insertions(+), 2 deletions(-) diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json index d2c14189e2529..80a27f1915997 100644 --- a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json +++ b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json @@ -338,7 +338,8 @@ "index": ".kibana", "source": { "globaltype": { - "title": "My favorite global object" + "title": "My favorite global object", + "version": 1 }, "type": "globaltype", "updated_at": "2017-09-21T18:59:16.270Z" diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json index 7b5b1d86f6bcc..2c1023b2854b6 100644 --- a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json +++ b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json @@ -90,6 +90,9 @@ } }, "type": "text" + }, + "version": { + "type": "integer" } } }, diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/mappings.json b/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/mappings.json index 64d309b4209a2..f2b4b58369e1a 100644 --- a/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/mappings.json +++ b/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/mappings.json @@ -9,6 +9,9 @@ "ignore_above": 2048 } } + }, + "version": { + "type": "integer" } } } diff --git a/x-pack/test/saved_object_api_integration/common/suites/find.ts b/x-pack/test/saved_object_api_integration/common/suites/find.ts index 2fda8bb0aaab0..55e39a5f89a17 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/find.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/find.ts @@ -32,6 +32,7 @@ export interface FindTestCase { total?: number; }; failure?: 400 | 403; + typeUseInQueryField?: string; } export const getTestCases = (spaceId?: string): Record => ({ @@ -79,19 +80,61 @@ export const getTestCases = (spaceId?: string): Record => title: 'filter with namespace-agnostic type', query: 'type=globaltype&filter=globaltype.attributes.title:*global*', successResult: { savedObjects: CASES.NAMESPACE_AGNOSTIC }, + typeUseInQueryField: 'globaltype', }, filterWithHiddenType: { title: 'filter with hidden type', query: `type=hiddentype&fields=name&filter=hiddentype.attributes.title:'hello'`, + typeUseInQueryField: 'hiddentype', }, filterWithUnknownType: { title: 'filter with unknown type', query: `type=wigwags&filter=wigwags.attributes.title:'unknown'`, + typeUseInQueryField: 'wigwags', }, filterWithDisallowedType: { title: 'filter with disallowed type', query: `type=globaltype&filter=dashboard.title:'Requests'`, failure: 400, + typeUseInQueryField: 'dashboard', + }, + aggsWithNamespaceAgnosticType: { + title: 'aggs with namespace-agnostic type', + query: `type=globaltype&aggs=${encodeURIComponent( + JSON.stringify({ + type_count: { max: { field: 'globaltype.attributes.version' } }, + }) + )}`, + successResult: { savedObjects: CASES.NAMESPACE_AGNOSTIC }, + typeUseInQueryField: 'globaltype', + }, + aggsWithHiddenType: { + title: 'aggs with hidden type', + query: `type=hiddentype&fields=name&aggs=${encodeURIComponent( + JSON.stringify({ + type_count: { max: { field: 'hiddentype.attributes.title' } }, + }) + )}`, + typeUseInQueryField: 'hiddentype', + }, + aggsWithUnknownType: { + title: 'aggs with unknown type', + query: `type=wigwags&aggs=${encodeURIComponent( + JSON.stringify({ + type_count: { max: { field: 'wigwags.attributes.version' } }, + }) + )}`, + typeUseInQueryField: 'wigwags', + }, + aggsWithDisallowedType: { + title: 'aggs with disallowed type', + query: `type=globaltype&aggs=${encodeURIComponent( + JSON.stringify({ + type_count: { max: { field: 'dashboard.attributes.version' } }, + }) + )}`, + failure: 400, + typeUseInQueryField: 'dashboard', }, }); export const createRequest = ({ query }: FindTestCase) => ({ query }); @@ -116,7 +159,7 @@ export function findTestSuiteFactory(esArchiver: any, supertest: SuperTest) const type = parsedQuery.type; await expectForbidden(type)(response); } else if (failure === 400) { - const type = (parsedQuery.filter as string).split('.')[0]; + const type = testCase.typeUseInQueryField ?? 'unknown type'; expect(response.body.error).to.eql('Bad Request'); expect(response.body.statusCode).to.eql(failure); expect(response.body.message).to.eql(`This type ${type} is not allowed: Bad Request`); diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/find.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/find.ts index 7c16c01d203c0..f415c2e5e8650 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/find.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/find.ts @@ -19,12 +19,16 @@ const createTestCases = (spaceId: string) => { cases.unknownSearchField, cases.filterWithNamespaceAgnosticType, cases.filterWithDisallowedType, + cases.aggsWithNamespaceAgnosticType, + cases.aggsWithDisallowedType, ]; const hiddenAndUnknownTypes = [ cases.hiddenType, cases.unknownType, cases.filterWithHiddenType, cases.filterWithUnknownType, + cases.aggsWithHiddenType, + cases.aggsWithUnknownType, ]; const allTypes = normalTypes.concat(hiddenAndUnknownTypes); return { normalTypes, hiddenAndUnknownTypes, allTypes }; diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/find.ts b/x-pack/test/saved_object_api_integration/security_only/apis/find.ts index 97513783b94b9..3fef638fcb08e 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/find.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/find.ts @@ -19,12 +19,16 @@ const createTestCases = () => { cases.unknownSearchField, cases.filterWithNamespaceAgnosticType, cases.filterWithDisallowedType, + cases.aggsWithNamespaceAgnosticType, + cases.aggsWithDisallowedType, ]; const hiddenAndUnknownTypes = [ cases.hiddenType, cases.unknownType, cases.filterWithHiddenType, cases.filterWithUnknownType, + cases.aggsWithHiddenType, + cases.aggsWithUnknownType, ]; const allTypes = normalTypes.concat(hiddenAndUnknownTypes); return { normalTypes, hiddenAndUnknownTypes, allTypes }; From 6feca9b3a72b54405024e09a5c65b5313f7d43ba Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Mon, 20 Apr 2020 12:58:44 -0400 Subject: [PATCH 04/21] fix types --- src/core/server/saved_objects/service/lib/aggs_utils.ts | 2 +- src/core/server/saved_objects/service/lib/repository.ts | 1 - src/core/server/saved_objects/types.ts | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/core/server/saved_objects/service/lib/aggs_utils.ts b/src/core/server/saved_objects/service/lib/aggs_utils.ts index 21084c1ec2c84..b3850498ee958 100644 --- a/src/core/server/saved_objects/service/lib/aggs_utils.ts +++ b/src/core/server/saved_objects/service/lib/aggs_utils.ts @@ -36,7 +36,7 @@ const validateGetAggFieldValue = ( allowedTypes: string[], aggs: any, indexMapping: IndexMapping -) => { +): SavedObjectAggs => { return Object.keys(aggs).reduce((acc, key) => { if (key === 'field') { const error = hasFilterKeyError(aggs[key], allowedTypes, indexMapping); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 78a5e43ee543d..9f7d465749c3f 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -47,7 +47,6 @@ import { SavedObjectsDeleteOptions, SavedObjectsAddToNamespacesOptions, SavedObjectsDeleteFromNamespacesOptions, - SavedObjectAggs, } from '../saved_objects_client'; import { SavedObject, diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index 414487012cc30..6e762b6180dce 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -17,7 +17,7 @@ * under the License. */ -import { SavedObjectsClient, SavedObjectAggs } from './service/saved_objects_client'; +import { SavedObjectsClient } from './service/saved_objects_client'; import { SavedObjectsTypeMappingDefinition, SavedObjectsTypeMappingDefinitions } from './mappings'; import { SavedObjectMigrationMap } from './migrations'; import { PropertyValidators } from './validation'; From 8dda82105188348607c4df22235fa1f87b3b77bc Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Mon, 20 Apr 2020 16:09:57 -0400 Subject: [PATCH 05/21] unit test added for aggs_utils --- .../service/lib/aggs_utils.test.ts | 103 ++++++++++++++++++ .../service/lib/filter_utils.test.ts | 2 +- 2 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 src/core/server/saved_objects/service/lib/aggs_utils.test.ts diff --git a/src/core/server/saved_objects/service/lib/aggs_utils.test.ts b/src/core/server/saved_objects/service/lib/aggs_utils.test.ts new file mode 100644 index 0000000000000..1bbe3443bdc22 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/aggs_utils.test.ts @@ -0,0 +1,103 @@ +/* + * 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 { validateGetSavedObjectAggs } from './aggs_utils'; +import { mockMappings } from './filter_utils.test'; + +describe('Filter Utils', () => { + describe('#validateGetSavedObjectAggs', () => { + test('Validate a simple aggregations', () => { + expect( + validateGetSavedObjectAggs( + ['foo'], + { aggName: { max: { field: 'foo.attributes.bytes' } } }, + mockMappings + ) + ).toEqual({ + aggName: { + max: { + field: 'foo.bytes', + }, + }, + }); + }); + test('Validate a nested simple aggregations', () => { + expect( + validateGetSavedObjectAggs( + ['alert'], + { aggName: { cardinality: { field: 'alert.attributes.actions.group' } } }, + mockMappings + ) + ).toEqual({ + aggName: { + cardinality: { + field: 'alert.actions.group', + }, + }, + }); + }); + + test('Throw an error when types is not allowed', () => { + expect(() => { + validateGetSavedObjectAggs( + ['alert'], + { + aggName: { + max: { field: 'foo.attributes.bytes' }, + }, + }, + mockMappings + ); + }).toThrowErrorMatchingInlineSnapshot(`"This type foo is not allowed: Bad Request"`); + }); + + test('Throw an error when aggregation is not defined in SavedObjectAggs', () => { + expect(() => { + validateGetSavedObjectAggs( + ['foo'], + { + aggName: { + MySuperAgg: { field: 'foo.attributes.bytes' }, + }, + }, + mockMappings + ); + }).toThrowErrorMatchingInlineSnapshot( + `"Invalid value {\\"aggName\\":{\\"MySuperAgg\\":{\\"field\\":\\"foo.attributes.bytes\\"}}} supplied to : { [K in string]: ((Partial<{ filter: { term: { [K in string]: string } }, histogram: ({ field: string } & { interval: number } & Partial<{ min_doc_count: number, extended_bounds: { min: number, max: number }, keyed: boolean, missing: number, order: { [K in string]: desc } }>), terms: ({ field: string } & Partial<{ field: string, size: number, show_term_doc_count_error: boolean, order: { [K in string]: desc } }>) }> & Partial<{ avg: { field: string }, weighted_avg: ({ value: ({ field: string } & Partial<{ missing: number }>), weight: ({ field: string } & Partial<{ missing: number }>) } & Partial<{ format: string, value_type: string }>), cardinality: { field: string }, max: ({ field: string } & Partial<{ missing: number }>), min: ({ field: string } & Partial<{ missing: number }>), top_hits: Partial<{ explain: boolean, from: string, highlight: any, seq_no_primary_term: boolean, size: number, sort: any, stored_fields: Array, version: boolean, _name: string, _source: Partial<{ includes: Array, excludes: Array }> }> }>) & Partial<{ aggs: (Partial<{ filter: { term: { [K in string]: string } }, histogram: ({ field: string } & { interval: number } & Partial<{ min_doc_count: number, extended_bounds: { min: number, max: number }, keyed: boolean, missing: number, order: { [K in string]: desc } }>), terms: ({ field: string } & Partial<{ field: string, size: number, show_term_doc_count_error: boolean, order: { [K in string]: desc } }>) }> & Partial<{ avg: { field: string }, weighted_avg: ({ value: ({ field: string } & Partial<{ missing: number }>), weight: ({ field: string } & Partial<{ missing: number }>) } & Partial<{ format: string, value_type: string }>), cardinality: { field: string }, max: ({ field: string } & Partial<{ missing: number }>), min: ({ field: string } & Partial<{ missing: number }>), top_hits: Partial<{ explain: boolean, from: string, highlight: any, seq_no_primary_term: boolean, size: number, sort: any, stored_fields: Array, version: boolean, _name: string, _source: Partial<{ includes: Array, excludes: Array }> }> }>) }>) }, excess properties: [\\"MySuperAgg\\"]: Bad Request"` + ); + }); + + test('Throw an error when you add attributes who are not defined in SavedObjectAggs', () => { + expect(() => { + validateGetSavedObjectAggs( + ['alert'], + { + aggName: { + cardinality: { field: 'alert.attributes.actions.group' }, + script: 'I want to access that I should not', + }, + }, + mockMappings + ); + }).toThrowErrorMatchingInlineSnapshot( + `"Invalid value {\\"aggName\\":{\\"cardinality\\":{\\"field\\":\\"alert.attributes.actions.group\\"},\\"script\\":\\"I want to access that I should not\\"}} supplied to : { [K in string]: ((Partial<{ filter: { term: { [K in string]: string } }, histogram: ({ field: string } & { interval: number } & Partial<{ min_doc_count: number, extended_bounds: { min: number, max: number }, keyed: boolean, missing: number, order: { [K in string]: desc } }>), terms: ({ field: string } & Partial<{ field: string, size: number, show_term_doc_count_error: boolean, order: { [K in string]: desc } }>) }> & Partial<{ avg: { field: string }, weighted_avg: ({ value: ({ field: string } & Partial<{ missing: number }>), weight: ({ field: string } & Partial<{ missing: number }>) } & Partial<{ format: string, value_type: string }>), cardinality: { field: string }, max: ({ field: string } & Partial<{ missing: number }>), min: ({ field: string } & Partial<{ missing: number }>), top_hits: Partial<{ explain: boolean, from: string, highlight: any, seq_no_primary_term: boolean, size: number, sort: any, stored_fields: Array, version: boolean, _name: string, _source: Partial<{ includes: Array, excludes: Array }> }> }>) & Partial<{ aggs: (Partial<{ filter: { term: { [K in string]: string } }, histogram: ({ field: string } & { interval: number } & Partial<{ min_doc_count: number, extended_bounds: { min: number, max: number }, keyed: boolean, missing: number, order: { [K in string]: desc } }>), terms: ({ field: string } & Partial<{ field: string, size: number, show_term_doc_count_error: boolean, order: { [K in string]: desc } }>) }> & Partial<{ avg: { field: string }, weighted_avg: ({ value: ({ field: string } & Partial<{ missing: number }>), weight: ({ field: string } & Partial<{ missing: number }>) } & Partial<{ format: string, value_type: string }>), cardinality: { field: string }, max: ({ field: string } & Partial<{ missing: number }>), min: ({ field: string } & Partial<{ missing: number }>), top_hits: Partial<{ explain: boolean, from: string, highlight: any, seq_no_primary_term: boolean, size: number, sort: any, stored_fields: Array, version: boolean, _name: string, _source: Partial<{ includes: Array, excludes: Array }> }> }>) }>) }, excess properties: [\\"script\\"]: Bad Request"` + ); + }); + }); +}); diff --git a/src/core/server/saved_objects/service/lib/filter_utils.test.ts b/src/core/server/saved_objects/service/lib/filter_utils.test.ts index 4d9bcdda3c8ae..df7a40081fabf 100644 --- a/src/core/server/saved_objects/service/lib/filter_utils.test.ts +++ b/src/core/server/saved_objects/service/lib/filter_utils.test.ts @@ -21,7 +21,7 @@ import { esKuery } from '../../../../../plugins/data/server'; import { validateFilterKueryNode, validateConvertFilterToKueryNode } from './filter_utils'; -const mockMappings = { +export const mockMappings = { properties: { updatedAt: { type: 'date', From 44dcb25e24e29458b0ef2f9ad9c22f50f97c0b6c Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Tue, 21 Apr 2020 21:23:00 -0400 Subject: [PATCH 06/21] add documentation --- docs/api/saved-objects/find.asciidoc | 4 ++++ ...bana-plugin-core-server.savedobjectsclient.find.md | 4 ++-- ...plugin-core-server.savedobjectsfindoptions.aggs.md | 11 +++++++++++ ...bana-plugin-core-server.savedobjectsfindoptions.md | 1 + ...re-server.savedobjectsfindresponse.aggregations.md | 11 +++++++++++ ...ana-plugin-core-server.savedobjectsfindresponse.md | 3 ++- ...-plugin-core-server.savedobjectsrepository.find.md | 6 +++--- ...ibana-plugin-core-server.savedobjectsrepository.md | 2 +- src/core/server/server.api.md | 10 +++++++--- 9 files changed, 42 insertions(+), 10 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.aggs.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.aggregations.md diff --git a/docs/api/saved-objects/find.asciidoc b/docs/api/saved-objects/find.asciidoc index 93e60be5d4923..5ea1762983f90 100644 --- a/docs/api/saved-objects/find.asciidoc +++ b/docs/api/saved-objects/find.asciidoc @@ -54,6 +54,10 @@ experimental[] Retrieve a paginated set of {kib} saved objects by various condit 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. +`aggs`:: + (Optional, string) The aggs will support aggregation string with the caveat that your field from the aggregation will have the attribute from your type saved object, + it should look like this: savedObjectType.attributes.field. However, If you use a direct attribute of a saved object like updatedAt, you will have to define your filter like this: savedObjectType.updatedAt. + 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. diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.find.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.find.md index 9a4c3df5d2d92..56d76125108d1 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.find.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.find.md @@ -9,7 +9,7 @@ Find all SavedObjects matching the search query Signature: ```typescript -find(options: SavedObjectsFindOptions): Promise>; +find(options: SavedObjectsFindOptions): Promise>; ``` ## Parameters @@ -20,5 +20,5 @@ find(options: SavedObjectsFindOptions): PromiseReturns: -`Promise>` +`Promise>` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.aggs.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.aggs.md new file mode 100644 index 0000000000000..46b4936295c50 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.aggs.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsFindOptions](./kibana-plugin-core-server.savedobjectsfindoptions.md) > [aggs](./kibana-plugin-core-server.savedobjectsfindoptions.aggs.md) + +## SavedObjectsFindOptions.aggs property + +Signature: + +```typescript +aggs?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md index 7421f4282ec93..0481c581c208c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md @@ -15,6 +15,7 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions | Property | Type | Description | | --- | --- | --- | +| [aggs](./kibana-plugin-core-server.savedobjectsfindoptions.aggs.md) | string | | | [defaultSearchOperator](./kibana-plugin-core-server.savedobjectsfindoptions.defaultsearchoperator.md) | 'AND' | 'OR' | | | [fields](./kibana-plugin-core-server.savedobjectsfindoptions.fields.md) | string[] | An array of fields to include in the results | | [filter](./kibana-plugin-core-server.savedobjectsfindoptions.filter.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.aggregations.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.aggregations.md new file mode 100644 index 0000000000000..17a899f4c8280 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.aggregations.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsFindResponse](./kibana-plugin-core-server.savedobjectsfindresponse.md) > [aggregations](./kibana-plugin-core-server.savedobjectsfindresponse.aggregations.md) + +## SavedObjectsFindResponse.aggregations property + +Signature: + +```typescript +aggregations?: A; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.md index a1b1a7a056206..e1f776f83193f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.md @@ -11,13 +11,14 @@ Return type of the Saved Objects `find()` method. Signature: ```typescript -export interface SavedObjectsFindResponse +export interface SavedObjectsFindResponse ``` ## Properties | Property | Type | Description | | --- | --- | --- | +| [aggregations](./kibana-plugin-core-server.savedobjectsfindresponse.aggregations.md) | A | | | [page](./kibana-plugin-core-server.savedobjectsfindresponse.page.md) | number | | | [per\_page](./kibana-plugin-core-server.savedobjectsfindresponse.per_page.md) | number | | | [saved\_objects](./kibana-plugin-core-server.savedobjectsfindresponse.saved_objects.md) | Array<SavedObject<T>> | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.find.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.find.md index 22222061b3077..81e1bb5f1a033 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.find.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.find.md @@ -7,18 +7,18 @@ Signature: ```typescript -find({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, }: SavedObjectsFindOptions): Promise>; +find({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, aggs, }: SavedObjectsFindOptions): Promise>; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| { search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, } | SavedObjectsFindOptions | | +| { search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, aggs, } | SavedObjectsFindOptions | | Returns: -`Promise>` +`Promise>` {promise} - { saved\_objects: \[{ id, type, version, attributes }\], total, per\_page, page } diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md index bd86ff3abbe9b..e77537fc99922 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md @@ -23,7 +23,7 @@ export declare class SavedObjectsRepository | [delete(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.delete.md) | | Deletes an object | | [deleteByNamespace(namespace, options)](./kibana-plugin-core-server.savedobjectsrepository.deletebynamespace.md) | | Deletes all objects from the provided namespace. | | [deleteFromNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) | | Removes one or more namespaces from a given multi-namespace saved object. If no namespaces remain, the saved object is deleted entirely. This method and \[addToNamespaces\][SavedObjectsRepository.addToNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to. | -| [find({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, })](./kibana-plugin-core-server.savedobjectsrepository.find.md) | | | +| [find({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, aggs, })](./kibana-plugin-core-server.savedobjectsrepository.find.md) | | | | [get(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.get.md) | | Gets a single object | | [incrementCounter(type, id, counterFieldName, options)](./kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md) | | Increases a counter field by one. Creates the document if one doesn't exist for the given id. | | [update(type, id, attributes, options)](./kibana-plugin-core-server.savedobjectsrepository.update.md) | | Updates an object | diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 7ca5c75f19e8f..b4ca1fb65c72d 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1784,7 +1784,7 @@ export class SavedObjectsClient { static errors: typeof SavedObjectsErrorHelpers; // (undocumented) errors: typeof SavedObjectsErrorHelpers; - find(options: SavedObjectsFindOptions): Promise>; + find(options: SavedObjectsFindOptions): Promise>; get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; } @@ -1956,6 +1956,8 @@ export type SavedObjectsFieldMapping = SavedObjectsCoreFieldMapping | SavedObjec // @public (undocumented) export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { + // (undocumented) + aggs?: string; // (undocumented) defaultSearchOperator?: 'AND' | 'OR'; fields?: string[]; @@ -1981,7 +1983,9 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { } // @public -export interface SavedObjectsFindResponse { +export interface SavedObjectsFindResponse { + // (undocumented) + aggregations?: A; // (undocumented) page: number; // (undocumented) @@ -2170,7 +2174,7 @@ export class SavedObjectsRepository { deleteByNamespace(namespace: string, options?: SavedObjectsDeleteByNamespaceOptions): Promise; deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<{}>; // (undocumented) - find({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, }: SavedObjectsFindOptions): Promise>; + find({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, aggs, }: SavedObjectsFindOptions): Promise>; get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; incrementCounter(type: string, id: string, counterFieldName: string, options?: SavedObjectsIncrementCounterOptions): Promise<{ id: string; From 0f57677b8a9b8cb9a05a089d6416c1e0194b25d5 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Mon, 27 Apr 2020 12:29:27 -0400 Subject: [PATCH 07/21] fix docs --- .../core/server/kibana-plugin-core-server.md | 1 + ...gin-core-server.savedobjectunsanitizeddoc.md | 13 +++++++++++++ x-pack/plugins/siem/index.scss | 17 +++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectunsanitizeddoc.md create mode 100644 x-pack/plugins/siem/index.scss diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index b8cffb36b821a..a775c76fece63 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -270,6 +270,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsClientWrapperFactory](./kibana-plugin-core-server.savedobjectsclientwrapperfactory.md) | Describes the factory used to create instances of Saved Objects Client Wrappers. | | [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) | Describe a [saved object type mapping](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) field.Please refer to [elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html) For the mapping documentation | | [SavedObjectsNamespaceType](./kibana-plugin-core-server.savedobjectsnamespacetype.md) | The namespace type dictates how a saved object can be interacted in relation to namespaces. Each type is mutually exclusive: \* single (default): this type of saved object is namespace-isolated, e.g., it exists in only one namespace. \* multiple: this type of saved object is shareable, e.g., it can exist in one or more namespaces. \* agnostic: this type of saved object is global.Note: do not write logic that uses this value directly; instead, use the appropriate accessors in the [type registry](./kibana-plugin-core-server.savedobjecttyperegistry.md). | +| [SavedObjectUnsanitizedDoc](./kibana-plugin-core-server.savedobjectunsanitizeddoc.md) | We want to have two types, one that guarantees a "references" attribute will exist and one that allows it to be null. Since we're not migrating all the saved objects to have a "references" array, we need to support the scenarios where it may be missing (ex migrations). | | [ScopeableRequest](./kibana-plugin-core-server.scopeablerequest.md) | A user credentials container. It accommodates the necessary auth credentials to impersonate the current user.See [KibanaRequest](./kibana-plugin-core-server.kibanarequest.md). | | [ServiceStatusLevel](./kibana-plugin-core-server.servicestatuslevel.md) | A convenience type that represents the union of each value in [ServiceStatusLevels](./kibana-plugin-core-server.servicestatuslevels.md). | | [SharedGlobalConfig](./kibana-plugin-core-server.sharedglobalconfig.md) | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectunsanitizeddoc.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectunsanitizeddoc.md new file mode 100644 index 0000000000000..fe4eaecd003d0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectunsanitizeddoc.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectUnsanitizedDoc](./kibana-plugin-core-server.savedobjectunsanitizeddoc.md) + +## SavedObjectUnsanitizedDoc type + +We want to have two types, one that guarantees a "references" attribute will exist and one that allows it to be null. Since we're not migrating all the saved objects to have a "references" array, we need to support the scenarios where it may be missing (ex migrations). + +Signature: + +```typescript +export declare type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial; +``` diff --git a/x-pack/plugins/siem/index.scss b/x-pack/plugins/siem/index.scss new file mode 100644 index 0000000000000..8b2f6d3cb6156 --- /dev/null +++ b/x-pack/plugins/siem/index.scss @@ -0,0 +1,17 @@ +/* GIS plugin styles */ + +// Import the EUI global scope so we can use EUI constants +@import 'src/legacy/ui/public/styles/_styling_constants'; + +// Prefix all styles with "map" to avoid conflicts. +// Examples +// mapChart +// mapChart__legend +// mapChart__legend--small +// mapChart__legend-isLoading + +@import 'main'; +@import 'mapbox_hacks'; +@import 'connected_components/index'; +@import 'components/index'; +@import 'layers/index'; From 817a3ef7a80764399c827555dd09dfa9bab787ef Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Mon, 6 Jul 2020 21:25:48 -0400 Subject: [PATCH 08/21] review I --- src/core/server/saved_objects/routes/find.ts | 2 +- .../saved_objects/service/lib/aggs_utils.test.ts | 9 +++++---- .../server/saved_objects/service/lib/repository.ts | 2 +- .../lib/saved_object_aggs_types/helpers.test.ts | 6 +++--- .../service/lib/saved_object_aggs_types/helpers.ts | 12 +++--------- .../service/lib/saved_object_aggs_types/index.ts | 1 + src/core/server/saved_objects/types.ts | 4 ++-- 7 files changed, 16 insertions(+), 20 deletions(-) diff --git a/src/core/server/saved_objects/routes/find.ts b/src/core/server/saved_objects/routes/find.ts index 4af5acd585097..ae728e8e449fa 100644 --- a/src/core/server/saved_objects/routes/find.ts +++ b/src/core/server/saved_objects/routes/find.ts @@ -63,7 +63,7 @@ export const registerFindRoute = (router: IRouter) => { hasReference: query.has_reference, fields: typeof query.fields === 'string' ? [query.fields] : query.fields, filter: query.filter, - aggs: query.aggs, + aggs: query.aggs != null ? JSON.parse(query.aggs) : undefined, }); return res.ok({ body: result }); diff --git a/src/core/server/saved_objects/service/lib/aggs_utils.test.ts b/src/core/server/saved_objects/service/lib/aggs_utils.test.ts index 1bbe3443bdc22..fcc32d92b6363 100644 --- a/src/core/server/saved_objects/service/lib/aggs_utils.test.ts +++ b/src/core/server/saved_objects/service/lib/aggs_utils.test.ts @@ -19,6 +19,7 @@ import { validateGetSavedObjectAggs } from './aggs_utils'; import { mockMappings } from './filter_utils.test'; +import { SavedObjectAggs } from './saved_object_aggs_types'; describe('Filter Utils', () => { describe('#validateGetSavedObjectAggs', () => { @@ -75,11 +76,11 @@ describe('Filter Utils', () => { aggName: { MySuperAgg: { field: 'foo.attributes.bytes' }, }, - }, + } as SavedObjectAggs, mockMappings ); }).toThrowErrorMatchingInlineSnapshot( - `"Invalid value {\\"aggName\\":{\\"MySuperAgg\\":{\\"field\\":\\"foo.attributes.bytes\\"}}} supplied to : { [K in string]: ((Partial<{ filter: { term: { [K in string]: string } }, histogram: ({ field: string } & { interval: number } & Partial<{ min_doc_count: number, extended_bounds: { min: number, max: number }, keyed: boolean, missing: number, order: { [K in string]: desc } }>), terms: ({ field: string } & Partial<{ field: string, size: number, show_term_doc_count_error: boolean, order: { [K in string]: desc } }>) }> & Partial<{ avg: { field: string }, weighted_avg: ({ value: ({ field: string } & Partial<{ missing: number }>), weight: ({ field: string } & Partial<{ missing: number }>) } & Partial<{ format: string, value_type: string }>), cardinality: { field: string }, max: ({ field: string } & Partial<{ missing: number }>), min: ({ field: string } & Partial<{ missing: number }>), top_hits: Partial<{ explain: boolean, from: string, highlight: any, seq_no_primary_term: boolean, size: number, sort: any, stored_fields: Array, version: boolean, _name: string, _source: Partial<{ includes: Array, excludes: Array }> }> }>) & Partial<{ aggs: (Partial<{ filter: { term: { [K in string]: string } }, histogram: ({ field: string } & { interval: number } & Partial<{ min_doc_count: number, extended_bounds: { min: number, max: number }, keyed: boolean, missing: number, order: { [K in string]: desc } }>), terms: ({ field: string } & Partial<{ field: string, size: number, show_term_doc_count_error: boolean, order: { [K in string]: desc } }>) }> & Partial<{ avg: { field: string }, weighted_avg: ({ value: ({ field: string } & Partial<{ missing: number }>), weight: ({ field: string } & Partial<{ missing: number }>) } & Partial<{ format: string, value_type: string }>), cardinality: { field: string }, max: ({ field: string } & Partial<{ missing: number }>), min: ({ field: string } & Partial<{ missing: number }>), top_hits: Partial<{ explain: boolean, from: string, highlight: any, seq_no_primary_term: boolean, size: number, sort: any, stored_fields: Array, version: boolean, _name: string, _source: Partial<{ includes: Array, excludes: Array }> }> }>) }>) }, excess properties: [\\"MySuperAgg\\"]: Bad Request"` + `"Invalid value {\\"aggName\\":{\\"MySuperAgg\\":{\\"field\\":\\"foo.attributes.bytes\\"}}}, excess properties: [\\"MySuperAgg\\"]: Bad Request"` ); }); @@ -92,11 +93,11 @@ describe('Filter Utils', () => { cardinality: { field: 'alert.attributes.actions.group' }, script: 'I want to access that I should not', }, - }, + } as SavedObjectAggs, mockMappings ); }).toThrowErrorMatchingInlineSnapshot( - `"Invalid value {\\"aggName\\":{\\"cardinality\\":{\\"field\\":\\"alert.attributes.actions.group\\"},\\"script\\":\\"I want to access that I should not\\"}} supplied to : { [K in string]: ((Partial<{ filter: { term: { [K in string]: string } }, histogram: ({ field: string } & { interval: number } & Partial<{ min_doc_count: number, extended_bounds: { min: number, max: number }, keyed: boolean, missing: number, order: { [K in string]: desc } }>), terms: ({ field: string } & Partial<{ field: string, size: number, show_term_doc_count_error: boolean, order: { [K in string]: desc } }>) }> & Partial<{ avg: { field: string }, weighted_avg: ({ value: ({ field: string } & Partial<{ missing: number }>), weight: ({ field: string } & Partial<{ missing: number }>) } & Partial<{ format: string, value_type: string }>), cardinality: { field: string }, max: ({ field: string } & Partial<{ missing: number }>), min: ({ field: string } & Partial<{ missing: number }>), top_hits: Partial<{ explain: boolean, from: string, highlight: any, seq_no_primary_term: boolean, size: number, sort: any, stored_fields: Array, version: boolean, _name: string, _source: Partial<{ includes: Array, excludes: Array }> }> }>) & Partial<{ aggs: (Partial<{ filter: { term: { [K in string]: string } }, histogram: ({ field: string } & { interval: number } & Partial<{ min_doc_count: number, extended_bounds: { min: number, max: number }, keyed: boolean, missing: number, order: { [K in string]: desc } }>), terms: ({ field: string } & Partial<{ field: string, size: number, show_term_doc_count_error: boolean, order: { [K in string]: desc } }>) }> & Partial<{ avg: { field: string }, weighted_avg: ({ value: ({ field: string } & Partial<{ missing: number }>), weight: ({ field: string } & Partial<{ missing: number }>) } & Partial<{ format: string, value_type: string }>), cardinality: { field: string }, max: ({ field: string } & Partial<{ missing: number }>), min: ({ field: string } & Partial<{ missing: number }>), top_hits: Partial<{ explain: boolean, from: string, highlight: any, seq_no_primary_term: boolean, size: number, sort: any, stored_fields: Array, version: boolean, _name: string, _source: Partial<{ includes: Array, excludes: Array }> }> }>) }>) }, excess properties: [\\"script\\"]: Bad Request"` + `"Invalid value {\\"aggName\\":{\\"cardinality\\":{\\"field\\":\\"alert.attributes.actions.group\\"},\\"script\\":\\"I want to access that I should not\\"}}, excess properties: [\\"script\\"]: Bad Request"` ); }); }); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index f95a4024d4d78..48464041377c9 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -640,7 +640,7 @@ export class SavedObjectsRepository { let aggsObject = null; try { if (aggs) { - aggsObject = validateGetSavedObjectAggs(allowedTypes, JSON.parse(aggs), this._mappings); + aggsObject = validateGetSavedObjectAggs(allowedTypes, aggs, this._mappings); } } catch (e) { throw e; diff --git a/src/core/server/saved_objects/service/lib/saved_object_aggs_types/helpers.test.ts b/src/core/server/saved_objects/service/lib/saved_object_aggs_types/helpers.test.ts index 1a72f14497783..2feb7fa6c7f7e 100644 --- a/src/core/server/saved_objects/service/lib/saved_object_aggs_types/helpers.test.ts +++ b/src/core/server/saved_objects/service/lib/saved_object_aggs_types/helpers.test.ts @@ -101,7 +101,7 @@ describe('Saved object aggs helpers', () => { ); expect(runDecode(codec, { aggName: { max: { field: 'hi', script: '' } } })).toEqual([ - 'Invalid value {"aggName":{"max":{"field":"hi","script":""}}} supplied to : { [K in string]: Partial<{ max: { field: string } }> }, excess properties: ["script"]', + 'Invalid value {"aggName":{"max":{"field":"hi","script":""}}}, excess properties: ["script"]', ]); }); @@ -121,7 +121,7 @@ describe('Saved object aggs helpers', () => { ); expect(runDecode(codec, { aggName: { min: { field: 'hi', script: 'field' } } })).toEqual([ - 'Invalid value {"aggName":{"min":{"field":"hi","script":"field"}}} supplied to : { [K in string]: (Partial<{ max: { field: string } }> & Partial<{ min: { field: string } }>) }, excess properties: ["script"]', + 'Invalid value {"aggName":{"min":{"field":"hi","script":"field"}}}, excess properties: ["script"]', ]); }); @@ -189,7 +189,7 @@ describe('Saved object aggs helpers', () => { }, }) ).toEqual([ - 'Invalid value {"aggName":{"filter":{"field":"hi"},"aggs":{"aggNewName":{"min":{"field":"low"},"script":"error"}}}} supplied to : { [K in string]: (Partial<{ max: { field: string } }> & Partial<{ filter: { field: string }, aggs: { [K in string]: Partial<{ min: { field: string } }> } }>) }, excess properties: ["script"]', + 'Invalid value {"aggName":{"filter":{"field":"hi"},"aggs":{"aggNewName":{"min":{"field":"low"},"script":"error"}}}}, excess properties: ["script"]', ]); }); }); diff --git a/src/core/server/saved_objects/service/lib/saved_object_aggs_types/helpers.ts b/src/core/server/saved_objects/service/lib/saved_object_aggs_types/helpers.ts index 0ea5c3d9301bb..6475d97ec6e79 100644 --- a/src/core/server/saved_objects/service/lib/saved_object_aggs_types/helpers.ts +++ b/src/core/server/saved_objects/service/lib/saved_object_aggs_types/helpers.ts @@ -63,7 +63,7 @@ const getExcessProps = ( r: Record ): string[] => Object.keys(r).reduce((acc, k) => { - const codecChildren = get>(props, [k]); + const codecChildren = get(props, [k]); const childrenProps = getProps(codecChildren); const childrenObject = r[k] as Record; if (codecChildren != null && childrenProps != null && codecChildren._tag === 'DictionaryType') { @@ -93,11 +93,7 @@ export const excess = (codec: rt.RecordC): rt.InterfaceType either.chain(codec.validate(u, c), (o: Record) => { if (codecProps == null) { - return rt.failure( - u, - c, - `Invalid Aggs object ${JSON.stringify(u)} supplied to : ${codec.name}` - ); + return rt.failure(u, c, `Invalid Aggs object ${JSON.stringify(u)}`); } const keys = Object.keys(o); const ex = keys.reduce((acc, k) => { @@ -108,9 +104,7 @@ export const excess = (codec: rt.RecordC): rt.InterfaceType Date: Mon, 6 Jul 2020 21:43:31 -0400 Subject: [PATCH 09/21] doc --- ...bana-plugin-core-public.savedobjectsclient.find.md | 2 +- .../kibana-plugin-core-public.savedobjectsclient.md | 2 +- ...plugin-core-public.savedobjectsfindoptions.aggs.md | 11 +++++++++++ ...bana-plugin-core-public.savedobjectsfindoptions.md | 1 + ...lic.savedobjectsfindresponsepublic.aggregations.md | 11 +++++++++++ ...ugin-core-public.savedobjectsfindresponsepublic.md | 3 ++- ...plugin-core-server.savedobjectsfindoptions.aggs.md | 2 +- ...bana-plugin-core-server.savedobjectsfindoptions.md | 2 +- ...-plugin-core-server.savedobjectsrepository.find.md | 2 +- src/core/public/public.api.md | 11 +++++++++-- .../saved_objects/service/saved_objects_client.ts | 2 +- src/core/server/server.api.md | 7 +++++-- src/plugins/data/public/public.api.md | 1 + src/plugins/data/server/server.api.md | 1 + 14 files changed, 47 insertions(+), 11 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.aggs.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsfindresponsepublic.aggregations.md diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.find.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.find.md index ddd8b207e3d78..fc9652b96450f 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.find.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.find.md @@ -9,5 +9,5 @@ Search for objects Signature: ```typescript -find: (options: SavedObjectsFindOptions) => Promise>; +find: (options: SavedObjectsFindOptions) => Promise>; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.md index 904b9cce09d4e..c93027c48c1b7 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.md @@ -24,7 +24,7 @@ The constructor for this class is marked as internal. Third-party code should no | [bulkGet](./kibana-plugin-core-public.savedobjectsclient.bulkget.md) | | (objects?: Array<{
id: string;
type: string;
}>) => Promise<SavedObjectsBatchResponse<unknown>> | Returns an array of objects by id | | [create](./kibana-plugin-core-public.savedobjectsclient.create.md) | | <T = unknown>(type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise<SimpleSavedObject<T>> | Persists an object | | [delete](./kibana-plugin-core-public.savedobjectsclient.delete.md) | | (type: string, id: string) => ReturnType<SavedObjectsApi['delete']> | Deletes an object | -| [find](./kibana-plugin-core-public.savedobjectsclient.find.md) | | <T = unknown>(options: SavedObjectsFindOptions) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects | +| [find](./kibana-plugin-core-public.savedobjectsclient.find.md) | | <T = unknown, A = unknown>(options: SavedObjectsFindOptions) => Promise<SavedObjectsFindResponsePublic<T, unknown>> | Search for objects | | [get](./kibana-plugin-core-public.savedobjectsclient.get.md) | | <T = unknown>(type: string, id: string) => Promise<SimpleSavedObject<T>> | Fetches a single object | ## Methods diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.aggs.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.aggs.md new file mode 100644 index 0000000000000..5509baeb428ec --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.aggs.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsFindOptions](./kibana-plugin-core-public.savedobjectsfindoptions.md) > [aggs](./kibana-plugin-core-public.savedobjectsfindoptions.aggs.md) + +## SavedObjectsFindOptions.aggs property + +Signature: + +```typescript +aggs?: SavedObjectAggs; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md index 5f33d62382818..7bd22af761868 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md @@ -15,6 +15,7 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions | Property | Type | Description | | --- | --- | --- | +| [aggs](./kibana-plugin-core-public.savedobjectsfindoptions.aggs.md) | SavedObjectAggs | | | [defaultSearchOperator](./kibana-plugin-core-public.savedobjectsfindoptions.defaultsearchoperator.md) | 'AND' | 'OR' | | | [fields](./kibana-plugin-core-public.savedobjectsfindoptions.fields.md) | string[] | An array of fields to include in the results | | [filter](./kibana-plugin-core-public.savedobjectsfindoptions.filter.md) | string | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindresponsepublic.aggregations.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindresponsepublic.aggregations.md new file mode 100644 index 0000000000000..14401b02f25c7 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindresponsepublic.aggregations.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsFindResponsePublic](./kibana-plugin-core-public.savedobjectsfindresponsepublic.md) > [aggregations](./kibana-plugin-core-public.savedobjectsfindresponsepublic.aggregations.md) + +## SavedObjectsFindResponsePublic.aggregations property + +Signature: + +```typescript +aggregations?: A; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindresponsepublic.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindresponsepublic.md index 7d75878041264..6f2276194f054 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindresponsepublic.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindresponsepublic.md @@ -11,13 +11,14 @@ Return type of the Saved Objects `find()` method. Signature: ```typescript -export interface SavedObjectsFindResponsePublic extends SavedObjectsBatchResponse +export interface SavedObjectsFindResponsePublic extends SavedObjectsBatchResponse ``` ## Properties | Property | Type | Description | | --- | --- | --- | +| [aggregations](./kibana-plugin-core-public.savedobjectsfindresponsepublic.aggregations.md) | A | | | [page](./kibana-plugin-core-public.savedobjectsfindresponsepublic.page.md) | number | | | [perPage](./kibana-plugin-core-public.savedobjectsfindresponsepublic.perpage.md) | number | | | [total](./kibana-plugin-core-public.savedobjectsfindresponsepublic.total.md) | number | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.aggs.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.aggs.md index 46b4936295c50..6ad9fe06ef1fc 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.aggs.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.aggs.md @@ -7,5 +7,5 @@ Signature: ```typescript -aggs?: string; +aggs?: SavedObjectAggs; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md index 5b76cbae6a5a0..c9f4d51324295 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md @@ -15,7 +15,7 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions | Property | Type | Description | | --- | --- | --- | -| [aggs](./kibana-plugin-core-server.savedobjectsfindoptions.aggs.md) | string | | +| [aggs](./kibana-plugin-core-server.savedobjectsfindoptions.aggs.md) | SavedObjectAggs | | | [defaultSearchOperator](./kibana-plugin-core-server.savedobjectsfindoptions.defaultsearchoperator.md) | 'AND' | 'OR' | | | [fields](./kibana-plugin-core-server.savedobjectsfindoptions.fields.md) | string[] | An array of fields to include in the results | | [filter](./kibana-plugin-core-server.savedobjectsfindoptions.filter.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.find.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.find.md index bcbeb6d44d6c8..47d9fd79e4802 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.find.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.find.md @@ -7,7 +7,7 @@ Signature: ```typescript -find({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, preference, aggs, }: SavedObjectsFindOptions): Promise>; +find({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, preference, aggs, }: SavedObjectsFindOptions): Promise>; ``` ## Parameters diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 86e281a49b744..273443e99599a 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -120,6 +120,7 @@ import { RecursiveReadonly } from '@kbn/utility-types'; import { ReindexParams } from 'elasticsearch'; import { ReindexRethrottleParams } from 'elasticsearch'; import { RenderSearchTemplateParams } from 'elasticsearch'; +import * as rt from 'io-ts'; import * as Rx from 'rxjs'; import { ScrollParams } from 'elasticsearch'; import { SearchParams } from 'elasticsearch'; @@ -1258,7 +1259,7 @@ export class SavedObjectsClient { // Warning: (ae-forgotten-export) The symbol "SavedObjectsClientContract" needs to be exported by the entry point index.d.ts delete: (type: string, id: string) => ReturnType; // Warning: (ae-forgotten-export) The symbol "SavedObjectsFindOptions" needs to be exported by the entry point index.d.ts - find: (options: SavedObjectsFindOptions_2) => Promise>; + find: (options: SavedObjectsFindOptions_2) => Promise>; get: (type: string, id: string) => Promise>; update(type: string, id: string, attributes: T, { version, migrationVersion, references }?: SavedObjectsUpdateOptions): Promise>; } @@ -1277,6 +1278,10 @@ export interface SavedObjectsCreateOptions { // @public (undocumented) export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { + // Warning: (ae-forgotten-export) The symbol "SavedObjectAggs" needs to be exported by the entry point index.d.ts + // + // (undocumented) + aggs?: SavedObjectAggs; // (undocumented) defaultSearchOperator?: 'AND' | 'OR'; fields?: string[]; @@ -1303,7 +1308,9 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { } // @public -export interface SavedObjectsFindResponsePublic extends SavedObjectsBatchResponse { +export interface SavedObjectsFindResponsePublic extends SavedObjectsBatchResponse { + // (undocumented) + aggregations?: A; // (undocumented) page: number; // (undocumented) diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts index db51102e0d656..16f3643893d83 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -100,7 +100,7 @@ export interface SavedObjectsFindResult extends SavedObject { */ export interface SavedObjectsFindResponse { aggregations?: A; - saved_objects: Array>; + saved_objects: Array>; total: number; per_page: number; page: number; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 4dd5a06198ea4..eaa0e0207f093 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -115,6 +115,7 @@ import { RenderSearchTemplateParams } from 'elasticsearch'; import { Request } from 'hapi'; import { ResponseObject } from 'hapi'; import { ResponseToolkit } from 'hapi'; +import * as rt from 'io-ts'; import { SchemaTypeError } from '@kbn/config-schema'; import { ScrollParams } from 'elasticsearch'; import { SearchParams } from 'elasticsearch'; @@ -2114,8 +2115,10 @@ export type SavedObjectsFieldMapping = SavedObjectsCoreFieldMapping | SavedObjec // @public (undocumented) export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { + // Warning: (ae-forgotten-export) The symbol "SavedObjectAggs" needs to be exported by the entry point index.d.ts + // // (undocumented) - aggs?: string; + aggs?: SavedObjectAggs; // (undocumented) defaultSearchOperator?: 'AND' | 'OR'; fields?: string[]; @@ -2340,7 +2343,7 @@ export class SavedObjectsRepository { deleteByNamespace(namespace: string, options?: SavedObjectsDeleteByNamespaceOptions): Promise; deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<{}>; // (undocumented) - find({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, preference, aggs, }: SavedObjectsFindOptions): Promise>; + find({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, preference, aggs, }: SavedObjectsFindOptions): Promise>; get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; incrementCounter(type: string, id: string, counterFieldName: string, options?: SavedObjectsIncrementCounterOptions): Promise<{ id: string; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 670b40e7d9472..7cba6386c790f 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -145,6 +145,7 @@ import { ReindexParams } from 'elasticsearch'; import { ReindexRethrottleParams } from 'elasticsearch'; import { RenderSearchTemplateParams } from 'elasticsearch'; import { Required } from '@kbn/utility-types'; +import * as rt from 'io-ts'; import * as Rx from 'rxjs'; import { SavedObject } from 'src/core/server'; import { SavedObject as SavedObject_3 } from 'src/core/public'; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index f029609cbf7ec..00ab8e500849c 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -118,6 +118,7 @@ import { RenderSearchTemplateParams } from 'elasticsearch'; import { Request } from 'hapi'; import { ResponseObject } from 'hapi'; import { ResponseToolkit } from 'hapi'; +import * as rt from 'io-ts'; import { SavedObject as SavedObject_2 } from 'src/core/server'; import { SchemaTypeError } from '@kbn/config-schema'; import { ScrollParams } from 'elasticsearch'; From e698b0ef8abda43c671bfafbc4fb296529439a55 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Tue, 7 Jul 2020 11:10:09 -0400 Subject: [PATCH 10/21] try to fix test --- .../saved_objects/spaces/mappings.json | 1 + .../mappings.json | 18 ------------------ 2 files changed, 1 insertion(+), 18 deletions(-) delete mode 100644 x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/mappings.json diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json index 2c1023b2854b6..54a17ad88012a 100644 --- a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json +++ b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json @@ -81,6 +81,7 @@ } }, "globaltype": { + "dynamic": "true", "properties": { "title": { "fields": { diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/mappings.json b/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/mappings.json deleted file mode 100644 index f2b4b58369e1a..0000000000000 --- a/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/mappings.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "globaltype": { - "properties": { - "title": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 2048 - } - } - }, - "version": { - "type": "integer" - } - } - } -} From c16e72b9ac69cb8619e03465f732bbe64b9e8b27 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Tue, 7 Jul 2020 11:40:54 -0400 Subject: [PATCH 11/21] add the new property to the saved object globaltype --- .../fixtures/saved_object_test_plugin/server/plugin.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/saved_object_test_plugin/server/plugin.ts b/x-pack/test/saved_object_api_integration/common/fixtures/saved_object_test_plugin/server/plugin.ts index 0c15ab4bd2f80..503349ba6e174 100644 --- a/x-pack/test/saved_object_api_integration/common/fixtures/saved_object_test_plugin/server/plugin.ts +++ b/x-pack/test/saved_object_api_integration/common/fixtures/saved_object_test_plugin/server/plugin.ts @@ -55,7 +55,14 @@ export class Plugin { hidden: false, namespaceType: 'agnostic', management, - mappings, + mappings: { + properties: { + ...mappings.properties, + version: { + type: 'integer', + }, + }, + }, }); core.savedObjects.registerType({ name: 'hiddentype', From 5a12d44ed626a7a4b7dcb7dfd3255f881e4547f0 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Tue, 7 Jul 2020 15:14:47 -0400 Subject: [PATCH 12/21] fix types --- .../saved_objects/encrypted_saved_objects_client_wrapper.ts | 4 ++-- .../saved_objects/secure_saved_objects_client_wrapper.ts | 4 ++-- .../server/saved_objects/spaces_saved_objects_client.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts index bdc2b6cb2e667..c560aab23cc09 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts @@ -165,9 +165,9 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon return await this.options.baseClient.delete(type, id, options); } - public async find(options: SavedObjectsFindOptions) { + public async find(options: SavedObjectsFindOptions) { return await this.handleEncryptedAttributesInBulkResponse( - await this.options.baseClient.find(options), + await this.options.baseClient.find(options), undefined, options.namespace ); diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts index 969344afae5e3..23ca0ea81a313 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts @@ -98,10 +98,10 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra return await this.baseClient.delete(type, id, options); } - public async find(options: SavedObjectsFindOptions) { + public async find(options: SavedObjectsFindOptions) { await this.ensureAuthorized(options.type, 'find', options.namespace, { options }); - const response = await this.baseClient.find(options); + const response = await this.baseClient.find(options); return await this.redactSavedObjectsNamespaces(response); } diff --git a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts index 6611725be8b67..46dd48d9151e3 100644 --- a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts +++ b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts @@ -135,10 +135,10 @@ export class SpacesSavedObjectsClient implements SavedObjectsClientContract { * @property {object} [options.hasReference] - { type, id } * @returns {promise} - { saved_objects: [{ id, type, version, attributes }], total, per_page, page } */ - public async find(options: SavedObjectsFindOptions) { + public async find(options: SavedObjectsFindOptions) { throwErrorIfNamespaceSpecified(options); - return await this.client.find({ + return await this.client.find({ ...options, type: (options.type ? coerceToArray(options.type) : this.types).filter( (type) => type !== 'space' From f04ab64181448951ca7c3db895f87f3224569827 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Mon, 9 Nov 2020 15:28:02 -0500 Subject: [PATCH 13/21] delete old files --- x-pack/plugins/security_solution/index.scss | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 x-pack/plugins/security_solution/index.scss diff --git a/x-pack/plugins/security_solution/index.scss b/x-pack/plugins/security_solution/index.scss deleted file mode 100644 index 8b2f6d3cb6156..0000000000000 --- a/x-pack/plugins/security_solution/index.scss +++ /dev/null @@ -1,17 +0,0 @@ -/* GIS plugin styles */ - -// Import the EUI global scope so we can use EUI constants -@import 'src/legacy/ui/public/styles/_styling_constants'; - -// Prefix all styles with "map" to avoid conflicts. -// Examples -// mapChart -// mapChart__legend -// mapChart__legend--small -// mapChart__legend-isLoading - -@import 'main'; -@import 'mapbox_hacks'; -@import 'connected_components/index'; -@import 'components/index'; -@import 'layers/index'; From 8f64144cf73bf1e88514d299d358af71713a0b25 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Mon, 9 Nov 2020 16:29:12 -0500 Subject: [PATCH 14/21] fix types + test api integration --- .../spaces_saved_objects_client.ts | 4 ++-- .../common/suites/find.ts | 22 ++++++++----------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts index cda3c48e71ccb..08e18524d6746 100644 --- a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts +++ b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts @@ -180,12 +180,12 @@ export class SpacesSavedObjectsClient implements SavedObjectsClientContract { } if (namespaces.length === 0) { // return empty response, since the user is unauthorized in this space (or these spaces), but we don't return forbidden errors for `find` operations - return SavedObjectsUtils.createEmptyFindResponse(options); + return SavedObjectsUtils.createEmptyFindResponse(options); } } catch (err) { if (Boom.isBoom(err) && err.output.payload.statusCode === 403) { // return empty response, since the user is unauthorized in any space, but we don't return forbidden errors for `find` operations - return SavedObjectsUtils.createEmptyFindResponse(options); + return SavedObjectsUtils.createEmptyFindResponse(options); } throw err; } diff --git a/x-pack/test/saved_object_api_integration/common/suites/find.ts b/x-pack/test/saved_object_api_integration/common/suites/find.ts index 88cb9fce38615..9fbd1458a1530 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/find.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/find.ts @@ -170,40 +170,40 @@ export const getTestCases = ( typeUseInQueryField: 'dashboard', }, aggsWithNamespaceAgnosticType: { - title: 'aggs with namespace-agnostic type', + title: buildTitle('aggs with namespace-agnostic type'), query: `type=globaltype&aggs=${encodeURIComponent( JSON.stringify({ type_count: { max: { field: 'globaltype.attributes.version' } }, }) - )}`, + )}${namespacesQueryParam}`, successResult: { savedObjects: SAVED_OBJECT_TEST_CASES.NAMESPACE_AGNOSTIC }, typeUseInQueryField: 'globaltype', }, aggsWithHiddenType: { - title: 'aggs with hidden type', + title: buildTitle('aggs with hidden type'), query: `type=hiddentype&fields=name&aggs=${encodeURIComponent( JSON.stringify({ type_count: { max: { field: 'hiddentype.attributes.title' } }, }) - )}`, + )}${namespacesQueryParam}`, typeUseInQueryField: 'hiddentype', }, aggsWithUnknownType: { - title: 'aggs with unknown type', + title: buildTitle('aggs with unknown type'), query: `type=wigwags&aggs=${encodeURIComponent( JSON.stringify({ type_count: { max: { field: 'wigwags.attributes.version' } }, }) - )}`, + )}${namespacesQueryParam}`, typeUseInQueryField: 'wigwags', }, aggsWithDisallowedType: { - title: 'aggs with disallowed type', + title: buildTitle('aggs with disallowed type'), query: `type=globaltype&aggs=${encodeURIComponent( JSON.stringify({ type_count: { max: { field: 'dashboard.attributes.version' } }, }) - )}`, + )}${namespacesQueryParam}`, failure: { statusCode: 400, reason: 'bad_request', @@ -222,7 +222,6 @@ const getTestTitle = ({ failure, title }: FindTestCase) => `${failure?.reason || 'success'} ["${title}"]`; export function findTestSuiteFactory(esArchiver: any, supertest: SuperTest) { - const expectForbidden = expectResponses.forbiddenTypes('find'); const expectResponseBody = ( testCase: FindTestCase, user?: TestUser @@ -242,12 +241,9 @@ export function findTestSuiteFactory(esArchiver: any, supertest: SuperTest) } else { throw new Error(`Unexpected failure reason: ${failure.reason}`); } - } else if (failure?.statusCode === 403) { - const type = parsedQuery.type; - await expectForbidden(type)(response); } else if (failure?.statusCode === 400) { if (failure.reason === 'bad_request') { - const type = (parsedQuery.filter as string).split('.')[0]; + const type = testCase.typeUseInQueryField ?? 'unknown type'; expect(response.body.error).to.eql('Bad Request'); expect(response.body.statusCode).to.eql(failure.statusCode); expect(response.body.message).to.eql(`This type ${type} is not allowed: Bad Request`); From 2209d12f1456e4b477a738fd7c1f5bbcc7a2d5d6 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Tue, 10 Nov 2020 16:12:39 -0500 Subject: [PATCH 15/21] type fix + test --- test/api_integration/apis/saved_objects/find.js | 2 +- x-pack/test/saved_object_api_integration/common/suites/find.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/test/api_integration/apis/saved_objects/find.js b/test/api_integration/apis/saved_objects/find.js index 075abd9a14409..0ea95448f87b9 100644 --- a/test/api_integration/apis/saved_objects/find.js +++ b/test/api_integration/apis/saved_objects/find.js @@ -359,7 +359,7 @@ export default function ({ getService }) { expect(resp.body).to.eql({ error: 'Bad Request', message: - 'Invalid value {"type_count":{"max":{"field":"dashboard.attributes.version","script":"Oh yes I am going to a script"}}} supplied to : { [K in string]: ((Partial<{ filter: { term: { [K in string]: string } }, histogram: ({ field: string } & { interval: number } & Partial<{ min_doc_count: number, extended_bounds: { min: number, max: number }, keyed: boolean, missing: number, order: { [K in string]: desc } }>), terms: ({ field: string } & Partial<{ field: string, size: number, show_term_doc_count_error: boolean, order: { [K in string]: desc } }>) }> & Partial<{ avg: { field: string }, weighted_avg: ({ value: ({ field: string } & Partial<{ missing: number }>), weight: ({ field: string } & Partial<{ missing: number }>) } & Partial<{ format: string, value_type: string }>), cardinality: { field: string }, max: ({ field: string } & Partial<{ missing: number }>), min: ({ field: string } & Partial<{ missing: number }>), top_hits: Partial<{ explain: boolean, from: string, highlight: any, seq_no_primary_term: boolean, size: number, sort: any, stored_fields: Array, version: boolean, _name: string, _source: Partial<{ includes: Array, excludes: Array }> }> }>) & Partial<{ aggs: (Partial<{ filter: { term: { [K in string]: string } }, histogram: ({ field: string } & { interval: number } & Partial<{ min_doc_count: number, extended_bounds: { min: number, max: number }, keyed: boolean, missing: number, order: { [K in string]: desc } }>), terms: ({ field: string } & Partial<{ field: string, size: number, show_term_doc_count_error: boolean, order: { [K in string]: desc } }>) }> & Partial<{ avg: { field: string }, weighted_avg: ({ value: ({ field: string } & Partial<{ missing: number }>), weight: ({ field: string } & Partial<{ missing: number }>) } & Partial<{ format: string, value_type: string }>), cardinality: { field: string }, max: ({ field: string } & Partial<{ missing: number }>), min: ({ field: string } & Partial<{ missing: number }>), top_hits: Partial<{ explain: boolean, from: string, highlight: any, seq_no_primary_term: boolean, size: number, sort: any, stored_fields: Array, version: boolean, _name: string, _source: Partial<{ includes: Array, excludes: Array }> }> }>) }>) }, excess properties: ["script"]: Bad Request', + 'Invalid value {"type_count":{"max":{"field":"dashboard.attributes.version","script":"Oh yes I am going to a script"}}}, excess properties: ["script"]: Bad Request', statusCode: 400, }); })); diff --git a/x-pack/test/saved_object_api_integration/common/suites/find.ts b/x-pack/test/saved_object_api_integration/common/suites/find.ts index 9fbd1458a1530..918d3c9c7faf3 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/find.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/find.ts @@ -13,7 +13,6 @@ import { getUrlPrefix, isUserAuthorizedAtSpace, getRedactedNamespaces, - expectResponses, } from '../lib/saved_object_test_utils'; import { ExpectResponseBody, TestCase, TestDefinition, TestSuite, TestUser } from '../lib/types'; From 4f667d48233a63281ec2e814f3cbbbd0f2e79573 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Wed, 11 Nov 2020 11:38:58 -0500 Subject: [PATCH 16/21] Update src/core/server/saved_objects/types.ts Co-authored-by: Rudolf Meijering --- src/core/server/saved_objects/types.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index 5712c0d6fcab5..dda5f5d323edd 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -109,6 +109,11 @@ export interface SavedObjectsFindOptions { */ defaultSearchOperator?: 'AND' | 'OR'; filter?: string | KueryNode; + /** + * Specify an Elasticsearch aggregation to perform. This alpha API only supports a limited set of aggregation types: metrics, bucket. Additional aggregation types can be contributed to Core. + * @alpha + * @example // Can you add an code example of how to use the API that shows an aggregation on a root property and an type attribute + */ aggs?: SavedObjectAggs; namespaces?: string[]; /** From 433a71cbfc4a3ce95f8eaa8cb3f4a9d7ea0a5c6a Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Wed, 11 Nov 2020 15:54:57 -0500 Subject: [PATCH 17/21] review I --- .../core/public/kibana-plugin-core-public.md | 1 + ...ana-plugin-core-public.savedobjectsaggs.md | 11 +++++++++ ...n-core-public.savedobjectscreateoptions.md | 1 - ...ore-public.savedobjectsfindoptions.aggs.md | 2 +- ...gin-core-public.savedobjectsfindoptions.md | 2 +- .../core/server/kibana-plugin-core-server.md | 1 + ...ana-plugin-core-server.savedobjectsaggs.md | 11 +++++++++ ...ore-server.savedobjectsfindoptions.aggs.md | 2 +- ...gin-core-server.savedobjectsfindoptions.md | 2 +- src/core/public/index.ts | 1 + src/core/public/public.api.md | 12 +++++++--- src/core/public/saved_objects/index.ts | 1 + .../saved_objects/saved_objects_client.ts | 3 +++ src/core/server/index.ts | 1 + .../service/lib/aggs_utils.test.ts | 24 +++++++++---------- .../saved_objects/service/lib/aggs_utils.ts | 10 ++++---- .../server/saved_objects/service/lib/index.ts | 2 +- .../saved_objects/service/lib/repository.ts | 4 ++-- .../bucket_aggs/index.ts | 0 .../helpers.test.ts | 0 .../helpers.ts | 0 .../index.ts | 8 +++---- .../metrics_aggs/index.ts | 0 .../service/saved_objects_client.ts | 4 ++-- src/core/server/saved_objects/types.ts | 4 ++-- src/core/server/server.api.md | 10 +++++--- .../saved_objects/spaces/mappings.json | 1 - 27 files changed, 78 insertions(+), 40 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsaggs.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsaggs.md rename src/core/server/saved_objects/service/lib/{saved_object_aggs_types => saved_objects_aggs_types}/bucket_aggs/index.ts (100%) rename src/core/server/saved_objects/service/lib/{saved_object_aggs_types => saved_objects_aggs_types}/helpers.test.ts (100%) rename src/core/server/saved_objects/service/lib/{saved_object_aggs_types => saved_objects_aggs_types}/helpers.ts (100%) rename src/core/server/saved_objects/service/lib/{saved_object_aggs_types => saved_objects_aggs_types}/index.ts (89%) rename src/core/server/saved_objects/service/lib/{saved_object_aggs_types => saved_objects_aggs_types}/metrics_aggs/index.ts (100%) diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index b8b1bdcdee3be..9a9876682c702 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -158,6 +158,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PublicUiSettingsParams](./kibana-plugin-core-public.publicuisettingsparams.md) | A sub-set of [UiSettingsParams](./kibana-plugin-core-public.uisettingsparams.md) exposed to the client-side. | | [SavedObjectAttribute](./kibana-plugin-core-public.savedobjectattribute.md) | Type definition for a Saved Object attribute value | | [SavedObjectAttributeSingle](./kibana-plugin-core-public.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-core-public.savedobjectattribute.md) | +| [SavedObjectsAggs](./kibana-plugin-core-public.savedobjectsaggs.md) | | | [SavedObjectsClientContract](./kibana-plugin-core-public.savedobjectsclientcontract.md) | SavedObjectsClientContract as implemented by the [SavedObjectsClient](./kibana-plugin-core-public.savedobjectsclient.md) | | [SavedObjectsNamespaceType](./kibana-plugin-core-public.savedobjectsnamespacetype.md) | The namespace type dictates how a saved object can be interacted in relation to namespaces. Each type is mutually exclusive: \* single (default): this type of saved object is namespace-isolated, e.g., it exists in only one namespace. \* multiple: this type of saved object is shareable, e.g., it can exist in one or more namespaces. \* agnostic: this type of saved object is global. | | [StartServicesAccessor](./kibana-plugin-core-public.startservicesaccessor.md) | Allows plugins to get access to APIs available in start inside async handlers, such as [App.mount](./kibana-plugin-core-public.app.mount.md). Promise will not resolve until Core and plugin dependencies have completed start. | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsaggs.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsaggs.md new file mode 100644 index 0000000000000..2626086355a98 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsaggs.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsAggs](./kibana-plugin-core-public.savedobjectsaggs.md) + +## SavedObjectsAggs type + +Signature: + +```typescript +export declare type SavedObjectsAggs = rt.TypeOf; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectscreateoptions.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectscreateoptions.md index b1b93407d4ff1..85a34708cc0dc 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectscreateoptions.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectscreateoptions.md @@ -4,7 +4,6 @@ ## SavedObjectsCreateOptions interface - Signature: ```typescript diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.aggs.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.aggs.md index 5509baeb428ec..f09d7922e28b9 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.aggs.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.aggs.md @@ -7,5 +7,5 @@ Signature: ```typescript -aggs?: SavedObjectAggs; +aggs?: SavedObjectsAggs; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md index 29b77d5a19e54..80b3b2b5cbbfc 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md @@ -15,7 +15,7 @@ export interface SavedObjectsFindOptions | Property | Type | Description | | --- | --- | --- | -| [aggs](./kibana-plugin-core-public.savedobjectsfindoptions.aggs.md) | SavedObjectAggs | | +| [aggs](./kibana-plugin-core-public.savedobjectsfindoptions.aggs.md) | SavedObjectsAggs | | | [defaultSearchOperator](./kibana-plugin-core-public.savedobjectsfindoptions.defaultsearchoperator.md) | 'AND' | 'OR' | The search operator to use with the provided filter. Defaults to OR | | [fields](./kibana-plugin-core-public.savedobjectsfindoptions.fields.md) | string[] | An array of fields to include in the results | | [filter](./kibana-plugin-core-public.savedobjectsfindoptions.filter.md) | string | KueryNode | | diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 68f5e72915556..55a5cc86b7705 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -284,6 +284,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectAttribute](./kibana-plugin-core-server.savedobjectattribute.md) | Type definition for a Saved Object attribute value | | [SavedObjectAttributeSingle](./kibana-plugin-core-server.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-core-server.savedobjectattribute.md) | | [SavedObjectMigrationFn](./kibana-plugin-core-server.savedobjectmigrationfn.md) | A migration function for a [saved object type](./kibana-plugin-core-server.savedobjectstype.md) used to migrate it to a given version | +| [SavedObjectsAggs](./kibana-plugin-core-server.savedobjectsaggs.md) | | | [SavedObjectSanitizedDoc](./kibana-plugin-core-server.savedobjectsanitizeddoc.md) | Describes Saved Object documents that have passed through the migration framework and are guaranteed to have a references root property. | | [SavedObjectsClientContract](./kibana-plugin-core-server.savedobjectsclientcontract.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state.\#\# SavedObjectsClient errorsSince the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either:1. Caused by bad implementation (ie. undefined is not a function) and as such unpredictable 2. An error that has been classified and decorated appropriately by the decorators in [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md)Type 1 errors are inevitable, but since all expected/handle-able errors should be Type 2 the isXYZError() helpers exposed at SavedObjectsErrorHelpers should be used to understand and manage error responses from the SavedObjectsClient.Type 2 errors are decorated versions of the source error, so if the elasticsearch client threw an error it will be decorated based on its type. That means that rather than looking for error.body.error.type or doing substring checks on error.body.error.reason, just use the helpers to understand the meaning of the error:\`\`\`js if (SavedObjectsErrorHelpers.isNotFoundError(error)) { // handle 404 }if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { // 401 handling should be automatic, but in case you wanted to know }// always rethrow the error unless you handle it throw error; \`\`\`\#\#\# 404s from missing indexFrom the perspective of application code and APIs the SavedObjectsClient is a black box that persists objects. One of the internal details that users have no control over is that we use an elasticsearch index for persistance and that index might be missing.At the time of writing we are in the process of transitioning away from the operating assumption that the SavedObjects index is always available. Part of this transition is handling errors resulting from an index missing. These used to trigger a 500 error in most cases, and in others cause 404s with different error messages.From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing.See [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) See [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) | | [SavedObjectsClientFactory](./kibana-plugin-core-server.savedobjectsclientfactory.md) | Describes the factory used to create instances of the Saved Objects Client. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaggs.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsaggs.md new file mode 100644 index 0000000000000..84f9e138c5408 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsaggs.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsAggs](./kibana-plugin-core-server.savedobjectsaggs.md) + +## SavedObjectsAggs type + +Signature: + +```typescript +export declare type SavedObjectsAggs = rt.TypeOf; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.aggs.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.aggs.md index 6ad9fe06ef1fc..25588f623656a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.aggs.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.aggs.md @@ -7,5 +7,5 @@ Signature: ```typescript -aggs?: SavedObjectAggs; +aggs?: SavedObjectsAggs; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md index ab1a73962c211..3587d57069124 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md @@ -15,7 +15,7 @@ export interface SavedObjectsFindOptions | Property | Type | Description | | --- | --- | --- | -| [aggs](./kibana-plugin-core-server.savedobjectsfindoptions.aggs.md) | SavedObjectAggs | | +| [aggs](./kibana-plugin-core-server.savedobjectsfindoptions.aggs.md) | SavedObjectsAggs | | | [defaultSearchOperator](./kibana-plugin-core-server.savedobjectsfindoptions.defaultsearchoperator.md) | 'AND' | 'OR' | The search operator to use with the provided filter. Defaults to OR | | [fields](./kibana-plugin-core-server.savedobjectsfindoptions.fields.md) | string[] | An array of fields to include in the results | | [filter](./kibana-plugin-core-server.savedobjectsfindoptions.filter.md) | string | KueryNode | | diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 1393e69d55e51..b90bf20406d2c 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -115,6 +115,7 @@ export { } from './application'; export { + SavedObjectsAggs, SavedObjectsBatchResponse, SavedObjectsBulkCreateObject, SavedObjectsBulkCreateOptions, diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 3d84dac2a7029..e284c47460df6 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -980,6 +980,12 @@ export interface SavedObjectReference { type: string; } +// Warning: (ae-forgotten-export) The symbol "SavedObjectsAggsRt" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "SavedObjectsAggs" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type SavedObjectsAggs = rt.TypeOf; + // @public (undocumented) export interface SavedObjectsBaseOptions { namespace?: string; @@ -1047,6 +1053,8 @@ export class SavedObjectsClient { // @public export type SavedObjectsClientContract = PublicMethodsOf; +// Warning: (ae-missing-release-tag) "SavedObjectsCreateOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export interface SavedObjectsCreateOptions { id?: string; @@ -1058,10 +1066,8 @@ export interface SavedObjectsCreateOptions { // @public (undocumented) export interface SavedObjectsFindOptions { - // Warning: (ae-forgotten-export) The symbol "SavedObjectAggs" needs to be exported by the entry point index.d.ts - // // (undocumented) - aggs?: SavedObjectAggs; + aggs?: SavedObjectsAggs; defaultSearchOperator?: 'AND' | 'OR'; fields?: string[]; // Warning: (ae-forgotten-export) The symbol "KueryNode" needs to be exported by the entry point index.d.ts diff --git a/src/core/public/saved_objects/index.ts b/src/core/public/saved_objects/index.ts index cc8fce0884ddf..06f66fdad2483 100644 --- a/src/core/public/saved_objects/index.ts +++ b/src/core/public/saved_objects/index.ts @@ -18,6 +18,7 @@ */ export { + SavedObjectsAggs, SavedObjectsBatchResponse, SavedObjectsBulkCreateObject, SavedObjectsBulkCreateOptions, diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index 0ac4c6ab63af5..98b228150012e 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -24,6 +24,7 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import { SavedObject, SavedObjectReference, + SavedObjectsAggs, SavedObjectsClientContract as SavedObjectsApi, SavedObjectsFindOptions as SavedObjectFindOptionsServer, SavedObjectsMigrationVersion, @@ -40,6 +41,8 @@ type SavedObjectsFindOptions = Omit< type PromiseType> = T extends Promise ? U : never; /** @public */ +export { SavedObjectsAggs }; + export interface SavedObjectsCreateOptions { /** * (Not recommended) Specify an id instead of having the saved objects service generate one for you. diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 0adda4770639d..3585820c657b3 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -240,6 +240,7 @@ export { } from './plugins'; export { + SavedObjectsAggs, SavedObjectsBulkCreateObject, SavedObjectsBulkGetObject, SavedObjectsBulkUpdateObject, diff --git a/src/core/server/saved_objects/service/lib/aggs_utils.test.ts b/src/core/server/saved_objects/service/lib/aggs_utils.test.ts index fcc32d92b6363..3b9bb6332e8b9 100644 --- a/src/core/server/saved_objects/service/lib/aggs_utils.test.ts +++ b/src/core/server/saved_objects/service/lib/aggs_utils.test.ts @@ -17,15 +17,15 @@ * under the License. */ -import { validateGetSavedObjectAggs } from './aggs_utils'; +import { validateGetSavedObjectsAggs } from './aggs_utils'; import { mockMappings } from './filter_utils.test'; -import { SavedObjectAggs } from './saved_object_aggs_types'; +import { SavedObjectsAggs } from './saved_objects_aggs_types'; describe('Filter Utils', () => { - describe('#validateGetSavedObjectAggs', () => { + describe('#validateGetSavedObjectsAggs', () => { test('Validate a simple aggregations', () => { expect( - validateGetSavedObjectAggs( + validateGetSavedObjectsAggs( ['foo'], { aggName: { max: { field: 'foo.attributes.bytes' } } }, mockMappings @@ -40,7 +40,7 @@ describe('Filter Utils', () => { }); test('Validate a nested simple aggregations', () => { expect( - validateGetSavedObjectAggs( + validateGetSavedObjectsAggs( ['alert'], { aggName: { cardinality: { field: 'alert.attributes.actions.group' } } }, mockMappings @@ -56,7 +56,7 @@ describe('Filter Utils', () => { test('Throw an error when types is not allowed', () => { expect(() => { - validateGetSavedObjectAggs( + validateGetSavedObjectsAggs( ['alert'], { aggName: { @@ -68,15 +68,15 @@ describe('Filter Utils', () => { }).toThrowErrorMatchingInlineSnapshot(`"This type foo is not allowed: Bad Request"`); }); - test('Throw an error when aggregation is not defined in SavedObjectAggs', () => { + test('Throw an error when aggregation is not defined in SavedObjectsAggs', () => { expect(() => { - validateGetSavedObjectAggs( + validateGetSavedObjectsAggs( ['foo'], { aggName: { MySuperAgg: { field: 'foo.attributes.bytes' }, }, - } as SavedObjectAggs, + } as SavedObjectsAggs, mockMappings ); }).toThrowErrorMatchingInlineSnapshot( @@ -84,16 +84,16 @@ describe('Filter Utils', () => { ); }); - test('Throw an error when you add attributes who are not defined in SavedObjectAggs', () => { + test('Throw an error when you add attributes who are not defined in SavedObjectsAggs', () => { expect(() => { - validateGetSavedObjectAggs( + validateGetSavedObjectsAggs( ['alert'], { aggName: { cardinality: { field: 'alert.attributes.actions.group' }, script: 'I want to access that I should not', }, - } as SavedObjectAggs, + } as SavedObjectsAggs, mockMappings ); }).toThrowErrorMatchingInlineSnapshot( diff --git a/src/core/server/saved_objects/service/lib/aggs_utils.ts b/src/core/server/saved_objects/service/lib/aggs_utils.ts index b3850498ee958..b88ce5b50dc8b 100644 --- a/src/core/server/saved_objects/service/lib/aggs_utils.ts +++ b/src/core/server/saved_objects/service/lib/aggs_utils.ts @@ -21,13 +21,13 @@ import { IndexMapping } from '../../mappings'; import { SavedObjectsErrorHelpers } from './errors'; import { hasFilterKeyError } from './filter_utils'; -import { SavedObjectAggs, validateSavedObjectTypeAggs } from './saved_object_aggs_types'; +import { SavedObjectsAggs, validateSavedObjectTypeAggs } from './saved_objects_aggs_types'; -export const validateGetSavedObjectAggs = ( +export const validateGetSavedObjectsAggs = ( allowedTypes: string[], - aggs: SavedObjectAggs, + aggs: SavedObjectsAggs, indexMapping: IndexMapping -): SavedObjectAggs => { +): SavedObjectsAggs => { validateSavedObjectTypeAggs(aggs); return validateGetAggFieldValue(allowedTypes, aggs, indexMapping); }; @@ -36,7 +36,7 @@ const validateGetAggFieldValue = ( allowedTypes: string[], aggs: any, indexMapping: IndexMapping -): SavedObjectAggs => { +): SavedObjectsAggs => { return Object.keys(aggs).reduce((acc, key) => { if (key === 'field') { const error = hasFilterKeyError(aggs[key], allowedTypes, indexMapping); diff --git a/src/core/server/saved_objects/service/lib/index.ts b/src/core/server/saved_objects/service/lib/index.ts index 70a279b930a21..8972c57b2464a 100644 --- a/src/core/server/saved_objects/service/lib/index.ts +++ b/src/core/server/saved_objects/service/lib/index.ts @@ -31,5 +31,5 @@ export { export { SavedObjectsErrorHelpers } from './errors'; -export { SavedObjectAggs } from './saved_object_aggs_types'; +export { SavedObjectsAggs } from './saved_objects_aggs_types'; export { SavedObjectsUtils } from './utils'; diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index e381e990d0463..830b9cdd1136a 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -69,7 +69,7 @@ import { } from '../../types'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { validateConvertFilterToKueryNode } from './filter_utils'; -import { validateGetSavedObjectAggs } from './aggs_utils'; +import { validateGetSavedObjectsAggs } from './aggs_utils'; import { ALL_NAMESPACES_STRING, FIND_DEFAULT_PAGE, @@ -781,7 +781,7 @@ export class SavedObjectsRepository { let aggsObject = null; try { if (aggs) { - aggsObject = validateGetSavedObjectAggs(allowedTypes, aggs, this._mappings); + aggsObject = validateGetSavedObjectsAggs(allowedTypes, aggs, this._mappings); } } catch (e) { throw e; diff --git a/src/core/server/saved_objects/service/lib/saved_object_aggs_types/bucket_aggs/index.ts b/src/core/server/saved_objects/service/lib/saved_objects_aggs_types/bucket_aggs/index.ts similarity index 100% rename from src/core/server/saved_objects/service/lib/saved_object_aggs_types/bucket_aggs/index.ts rename to src/core/server/saved_objects/service/lib/saved_objects_aggs_types/bucket_aggs/index.ts diff --git a/src/core/server/saved_objects/service/lib/saved_object_aggs_types/helpers.test.ts b/src/core/server/saved_objects/service/lib/saved_objects_aggs_types/helpers.test.ts similarity index 100% rename from src/core/server/saved_objects/service/lib/saved_object_aggs_types/helpers.test.ts rename to src/core/server/saved_objects/service/lib/saved_objects_aggs_types/helpers.test.ts diff --git a/src/core/server/saved_objects/service/lib/saved_object_aggs_types/helpers.ts b/src/core/server/saved_objects/service/lib/saved_objects_aggs_types/helpers.ts similarity index 100% rename from src/core/server/saved_objects/service/lib/saved_object_aggs_types/helpers.ts rename to src/core/server/saved_objects/service/lib/saved_objects_aggs_types/helpers.ts diff --git a/src/core/server/saved_objects/service/lib/saved_object_aggs_types/index.ts b/src/core/server/saved_objects/service/lib/saved_objects_aggs_types/index.ts similarity index 89% rename from src/core/server/saved_objects/service/lib/saved_object_aggs_types/index.ts rename to src/core/server/saved_objects/service/lib/saved_objects_aggs_types/index.ts index 09c6d4cb204e6..afe8e3b8b4a18 100644 --- a/src/core/server/saved_objects/service/lib/saved_object_aggs_types/index.ts +++ b/src/core/server/saved_objects/service/lib/saved_objects_aggs_types/index.ts @@ -31,16 +31,16 @@ import { excess, throwErrors } from './helpers'; const AllAggsRt = rt.intersection([BucketAggsTypeRt, MetricsAggsTypeRt]); -const SavedObjectAggsRt = rt.record( +const SavedObjectsAggsRt = rt.record( rt.string, rt.intersection([AllAggsRt, rt.partial({ aggs: AllAggsRt })]) ); -export type SavedObjectAggs = rt.TypeOf; +export type SavedObjectsAggs = rt.TypeOf; -export const validateSavedObjectTypeAggs = (aggObjects: SavedObjectAggs) => { +export const validateSavedObjectTypeAggs = (aggObjects: SavedObjectsAggs) => { pipe( - excess(SavedObjectAggsRt).decode(aggObjects), + excess(SavedObjectsAggsRt).decode(aggObjects), fold(throwErrors(SavedObjectsErrorHelpers.createBadRequestError), identity) ); }; diff --git a/src/core/server/saved_objects/service/lib/saved_object_aggs_types/metrics_aggs/index.ts b/src/core/server/saved_objects/service/lib/saved_objects_aggs_types/metrics_aggs/index.ts similarity index 100% rename from src/core/server/saved_objects/service/lib/saved_object_aggs_types/metrics_aggs/index.ts rename to src/core/server/saved_objects/service/lib/saved_objects_aggs_types/metrics_aggs/index.ts diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts index 14db8adabc6e5..d6ef6a29a7b06 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -17,7 +17,7 @@ * under the License. */ -import { ISavedObjectsRepository, SavedObjectAggs } from './lib'; +import { ISavedObjectsRepository, SavedObjectsAggs } from './lib'; import { SavedObject, SavedObjectError, @@ -289,7 +289,7 @@ export interface SavedObjectsUpdateResponse * * @public */ -export { SavedObjectAggs }; +export { SavedObjectsAggs }; /** * diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index 5712c0d6fcab5..91bf2b5d64fad 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -17,7 +17,7 @@ * under the License. */ -import { SavedObjectsClient, SavedObjectAggs } from './service/saved_objects_client'; +import { SavedObjectsClient, SavedObjectsAggs } from './service/saved_objects_client'; import { SavedObjectsTypeMappingDefinition } from './mappings'; import { SavedObjectMigrationMap } from './migrations'; @@ -109,7 +109,7 @@ export interface SavedObjectsFindOptions { */ defaultSearchOperator?: 'AND' | 'OR'; filter?: string | KueryNode; - aggs?: SavedObjectAggs; + aggs?: SavedObjectsAggs; namespaces?: string[]; /** * This map defines each type to search for, and the namespace(s) to search for the type in; this is only intended to be used by a saved diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index da2ac0a7873b1..684920a678a2e 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1884,6 +1884,12 @@ export interface SavedObjectsAddToNamespacesResponse { namespaces: string[]; } +// Warning: (ae-forgotten-export) The symbol "SavedObjectsAggsRt" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "SavedObjectsAggs" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type SavedObjectsAggs = rt.TypeOf; + // Warning: (ae-forgotten-export) The symbol "SavedObjectDoc" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "Referencable" needs to be exported by the entry point index.d.ts // @@ -2180,10 +2186,8 @@ export type SavedObjectsFieldMapping = SavedObjectsCoreFieldMapping | SavedObjec // @public (undocumented) export interface SavedObjectsFindOptions { - // Warning: (ae-forgotten-export) The symbol "SavedObjectAggs" needs to be exported by the entry point index.d.ts - // // (undocumented) - aggs?: SavedObjectAggs; + aggs?: SavedObjectsAggs; defaultSearchOperator?: 'AND' | 'OR'; fields?: string[]; // Warning: (ae-forgotten-export) The symbol "KueryNode" needs to be exported by the entry point index.d.ts diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json index b89d7aa6b80e3..04fed1905ecc5 100644 --- a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json +++ b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json @@ -81,7 +81,6 @@ } }, "globaltype": { - "dynamic": "true", "properties": { "title": { "fields": { From b23eac7fc7cd97865f7835d61bb79a85817a0b3e Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Sun, 22 Nov 2020 22:04:54 -0500 Subject: [PATCH 18/21] change our validation to match discussion with Pierre and Rudolph --- .../core/public/kibana-plugin-core-public.md | 1 - ...ana-plugin-core-public.savedobjectsaggs.md | 11 - ...ore-public.savedobjectsfindoptions.aggs.md | 11 - ...gin-core-public.savedobjectsfindoptions.md | 1 - .../core/server/kibana-plugin-core-server.md | 1 - ...ana-plugin-core-server.savedobjectsaggs.md | 11 - ...ore-server.savedobjectsfindoptions.aggs.md | 11 - ...gin-core-server.savedobjectsfindoptions.md | 1 - src/core/public/index.ts | 1 - src/core/public/public.api.md | 11 +- src/core/public/saved_objects/index.ts | 1 - .../saved_objects/saved_objects_client.ts | 10 +- src/core/server/index.ts | 1 - src/core/server/saved_objects/routes/find.ts | 2 +- .../service/lib/aggs_utils.test.ts | 161 +++++++++++++- .../saved_objects/service/lib/aggs_utils.ts | 74 +++++-- .../server/saved_objects/service/lib/index.ts | 1 - .../saved_objects/service/lib/repository.ts | 25 ++- .../bucket_aggs/index.ts | 53 ++--- .../saved_objects_aggs_types/helpers.test.ts | 196 ------------------ .../lib/saved_objects_aggs_types/helpers.ts | 88 +------- .../lib/saved_objects_aggs_types/index.ts | 22 +- .../metrics_aggs/index.ts | 42 ++-- .../service/saved_objects_client.ts | 8 +- src/core/server/saved_objects/types.ts | 4 +- src/core/server/server.api.md | 11 +- src/plugins/data/public/public.api.md | 1 - src/plugins/embeddable/public/public.api.md | 1 - test/api_integration/apis/index.js | 28 +-- .../apis/saved_objects/find.js | 4 +- .../apis/saved_objects/index.js | 22 +- 31 files changed, 323 insertions(+), 492 deletions(-) delete mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsaggs.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.aggs.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsaggs.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.aggs.md delete mode 100644 src/core/server/saved_objects/service/lib/saved_objects_aggs_types/helpers.test.ts diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index f2cf0f5f811dd..6a90fd49f1d66 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -163,7 +163,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PublicUiSettingsParams](./kibana-plugin-core-public.publicuisettingsparams.md) | A sub-set of [UiSettingsParams](./kibana-plugin-core-public.uisettingsparams.md) exposed to the client-side. | | [SavedObjectAttribute](./kibana-plugin-core-public.savedobjectattribute.md) | Type definition for a Saved Object attribute value | | [SavedObjectAttributeSingle](./kibana-plugin-core-public.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-core-public.savedobjectattribute.md) | -| [SavedObjectsAggs](./kibana-plugin-core-public.savedobjectsaggs.md) | | | [SavedObjectsClientContract](./kibana-plugin-core-public.savedobjectsclientcontract.md) | SavedObjectsClientContract as implemented by the [SavedObjectsClient](./kibana-plugin-core-public.savedobjectsclient.md) | | [SavedObjectsNamespaceType](./kibana-plugin-core-public.savedobjectsnamespacetype.md) | The namespace type dictates how a saved object can be interacted in relation to namespaces. Each type is mutually exclusive: \* single (default): this type of saved object is namespace-isolated, e.g., it exists in only one namespace. \* multiple: this type of saved object is shareable, e.g., it can exist in one or more namespaces. \* agnostic: this type of saved object is global. | | [StartServicesAccessor](./kibana-plugin-core-public.startservicesaccessor.md) | Allows plugins to get access to APIs available in start inside async handlers, such as [App.mount](./kibana-plugin-core-public.app.mount.md). Promise will not resolve until Core and plugin dependencies have completed start. | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsaggs.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsaggs.md deleted file mode 100644 index 2626086355a98..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsaggs.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsAggs](./kibana-plugin-core-public.savedobjectsaggs.md) - -## SavedObjectsAggs type - -Signature: - -```typescript -export declare type SavedObjectsAggs = rt.TypeOf; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.aggs.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.aggs.md deleted file mode 100644 index f09d7922e28b9..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.aggs.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsFindOptions](./kibana-plugin-core-public.savedobjectsfindoptions.md) > [aggs](./kibana-plugin-core-public.savedobjectsfindoptions.aggs.md) - -## SavedObjectsFindOptions.aggs property - -Signature: - -```typescript -aggs?: SavedObjectsAggs; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md index 80b3b2b5cbbfc..8bd87c2f6ea35 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md @@ -15,7 +15,6 @@ export interface SavedObjectsFindOptions | Property | Type | Description | | --- | --- | --- | -| [aggs](./kibana-plugin-core-public.savedobjectsfindoptions.aggs.md) | SavedObjectsAggs | | | [defaultSearchOperator](./kibana-plugin-core-public.savedobjectsfindoptions.defaultsearchoperator.md) | 'AND' | 'OR' | The search operator to use with the provided filter. Defaults to OR | | [fields](./kibana-plugin-core-public.savedobjectsfindoptions.fields.md) | string[] | An array of fields to include in the results | | [filter](./kibana-plugin-core-public.savedobjectsfindoptions.filter.md) | string | KueryNode | | diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 178393baad8c6..adbb2460dc80a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -285,7 +285,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectAttribute](./kibana-plugin-core-server.savedobjectattribute.md) | Type definition for a Saved Object attribute value | | [SavedObjectAttributeSingle](./kibana-plugin-core-server.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-core-server.savedobjectattribute.md) | | [SavedObjectMigrationFn](./kibana-plugin-core-server.savedobjectmigrationfn.md) | A migration function for a [saved object type](./kibana-plugin-core-server.savedobjectstype.md) used to migrate it to a given version | -| [SavedObjectsAggs](./kibana-plugin-core-server.savedobjectsaggs.md) | | | [SavedObjectSanitizedDoc](./kibana-plugin-core-server.savedobjectsanitizeddoc.md) | Describes Saved Object documents that have passed through the migration framework and are guaranteed to have a references root property. | | [SavedObjectsClientContract](./kibana-plugin-core-server.savedobjectsclientcontract.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state.\#\# SavedObjectsClient errorsSince the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either:1. Caused by bad implementation (ie. undefined is not a function) and as such unpredictable 2. An error that has been classified and decorated appropriately by the decorators in [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md)Type 1 errors are inevitable, but since all expected/handle-able errors should be Type 2 the isXYZError() helpers exposed at SavedObjectsErrorHelpers should be used to understand and manage error responses from the SavedObjectsClient.Type 2 errors are decorated versions of the source error, so if the elasticsearch client threw an error it will be decorated based on its type. That means that rather than looking for error.body.error.type or doing substring checks on error.body.error.reason, just use the helpers to understand the meaning of the error:\`\`\`js if (SavedObjectsErrorHelpers.isNotFoundError(error)) { // handle 404 }if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { // 401 handling should be automatic, but in case you wanted to know }// always rethrow the error unless you handle it throw error; \`\`\`\#\#\# 404s from missing indexFrom the perspective of application code and APIs the SavedObjectsClient is a black box that persists objects. One of the internal details that users have no control over is that we use an elasticsearch index for persistance and that index might be missing.At the time of writing we are in the process of transitioning away from the operating assumption that the SavedObjects index is always available. Part of this transition is handling errors resulting from an index missing. These used to trigger a 500 error in most cases, and in others cause 404s with different error messages.From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing.See [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) See [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) | | [SavedObjectsClientFactory](./kibana-plugin-core-server.savedobjectsclientfactory.md) | Describes the factory used to create instances of the Saved Objects Client. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaggs.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsaggs.md deleted file mode 100644 index 84f9e138c5408..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaggs.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsAggs](./kibana-plugin-core-server.savedobjectsaggs.md) - -## SavedObjectsAggs type - -Signature: - -```typescript -export declare type SavedObjectsAggs = rt.TypeOf; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.aggs.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.aggs.md deleted file mode 100644 index 25588f623656a..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.aggs.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsFindOptions](./kibana-plugin-core-server.savedobjectsfindoptions.md) > [aggs](./kibana-plugin-core-server.savedobjectsfindoptions.aggs.md) - -## SavedObjectsFindOptions.aggs property - -Signature: - -```typescript -aggs?: SavedObjectsAggs; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md index 3587d57069124..d393d579dbdd2 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md @@ -15,7 +15,6 @@ export interface SavedObjectsFindOptions | Property | Type | Description | | --- | --- | --- | -| [aggs](./kibana-plugin-core-server.savedobjectsfindoptions.aggs.md) | SavedObjectsAggs | | | [defaultSearchOperator](./kibana-plugin-core-server.savedobjectsfindoptions.defaultsearchoperator.md) | 'AND' | 'OR' | The search operator to use with the provided filter. Defaults to OR | | [fields](./kibana-plugin-core-server.savedobjectsfindoptions.fields.md) | string[] | An array of fields to include in the results | | [filter](./kibana-plugin-core-server.savedobjectsfindoptions.filter.md) | string | KueryNode | | diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 8fb838f4d5f17..564bbd712c535 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -115,7 +115,6 @@ export { } from './application'; export { - SavedObjectsAggs, SavedObjectsBatchResponse, SavedObjectsBulkCreateObject, SavedObjectsBulkCreateOptions, diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 6defd281180f8..b7cbd22c870b0 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -31,7 +31,6 @@ import { PublicMethodsOf } from '@kbn/utility-types'; import { PublicUiSettingsParams as PublicUiSettingsParams_2 } from 'src/core/server/types'; import React from 'react'; import { RecursiveReadonly } from '@kbn/utility-types'; -import * as rt from 'io-ts'; import * as Rx from 'rxjs'; import { ShallowPromise } from '@kbn/utility-types'; import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport'; @@ -1030,12 +1029,6 @@ export interface SavedObjectReference { type: string; } -// Warning: (ae-forgotten-export) The symbol "SavedObjectsAggsRt" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "SavedObjectsAggs" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type SavedObjectsAggs = rt.TypeOf; - // @public (undocumented) export interface SavedObjectsBaseOptions { namespace?: string; @@ -1116,8 +1109,8 @@ export interface SavedObjectsCreateOptions { // @public (undocumented) export interface SavedObjectsFindOptions { - // (undocumented) - aggs?: SavedObjectsAggs; + // @alpha + aggs?: Record; defaultSearchOperator?: 'AND' | 'OR'; fields?: string[]; // Warning: (ae-forgotten-export) The symbol "KueryNode" needs to be exported by the entry point index.d.ts diff --git a/src/core/public/saved_objects/index.ts b/src/core/public/saved_objects/index.ts index 06f66fdad2483..cc8fce0884ddf 100644 --- a/src/core/public/saved_objects/index.ts +++ b/src/core/public/saved_objects/index.ts @@ -18,7 +18,6 @@ */ export { - SavedObjectsAggs, SavedObjectsBatchResponse, SavedObjectsBulkCreateObject, SavedObjectsBulkCreateOptions, diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index 98b228150012e..777a2647d9e85 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -24,7 +24,6 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import { SavedObject, SavedObjectReference, - SavedObjectsAggs, SavedObjectsClientContract as SavedObjectsApi, SavedObjectsFindOptions as SavedObjectFindOptionsServer, SavedObjectsMigrationVersion, @@ -40,9 +39,6 @@ type SavedObjectsFindOptions = Omit< type PromiseType> = T extends Promise ? U : never; -/** @public */ -export { SavedObjectsAggs }; - export interface SavedObjectsCreateOptions { /** * (Not recommended) Specify an id instead of having the saved objects service generate one for you. @@ -355,6 +351,12 @@ export class SavedObjectsClient { query.has_reference = JSON.stringify(query.has_reference); } + // `aggs` is a structured object. we need to stringify it before sending it, as `fetch` + // is not doing it implicitly. + if (query.aggs) { + query.aggs = JSON.stringify(query.aggs); + } + const request: ReturnType = this.savedObjectsFetch(path, { method: 'GET', query, diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 9d780c1de8622..7b19c3a686757 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -241,7 +241,6 @@ export { } from './plugins'; export { - SavedObjectsAggs, SavedObjectsBulkCreateObject, SavedObjectsBulkGetObject, SavedObjectsBulkUpdateObject, diff --git a/src/core/server/saved_objects/routes/find.ts b/src/core/server/saved_objects/routes/find.ts index d703f0103d411..1b2e7a445548a 100644 --- a/src/core/server/saved_objects/routes/find.ts +++ b/src/core/server/saved_objects/routes/find.ts @@ -75,7 +75,7 @@ export const registerFindRoute = (router: IRouter) => { hasReferenceOperator: query.has_reference_operator, fields: typeof query.fields === 'string' ? [query.fields] : query.fields, filter: query.filter, - aggs: query.aggs != null ? JSON.parse(query.aggs) : undefined, + aggs: query.aggs ? JSON.parse(query.aggs) : undefined, namespaces, }); diff --git a/src/core/server/saved_objects/service/lib/aggs_utils.test.ts b/src/core/server/saved_objects/service/lib/aggs_utils.test.ts index 3b9bb6332e8b9..a273772367a38 100644 --- a/src/core/server/saved_objects/service/lib/aggs_utils.test.ts +++ b/src/core/server/saved_objects/service/lib/aggs_utils.test.ts @@ -19,7 +19,6 @@ import { validateGetSavedObjectsAggs } from './aggs_utils'; import { mockMappings } from './filter_utils.test'; -import { SavedObjectsAggs } from './saved_objects_aggs_types'; describe('Filter Utils', () => { describe('#validateGetSavedObjectsAggs', () => { @@ -38,7 +37,8 @@ describe('Filter Utils', () => { }, }); }); - test('Validate a nested simple aggregations', () => { + + test('Validate a nested field in simple aggregations', () => { expect( validateGetSavedObjectsAggs( ['alert'], @@ -54,6 +54,72 @@ describe('Filter Utils', () => { }); }); + test('Validate a nested aggregations', () => { + expect( + validateGetSavedObjectsAggs( + ['alert'], + { + aggName: { + cardinality: { + field: 'alert.attributes.actions.group', + aggs: { + aggName: { + max: { field: 'alert.attributes.actions.group' }, + }, + }, + }, + }, + }, + mockMappings + ) + ).toEqual({ + aggName: { + cardinality: { + field: 'alert.actions.group', + aggs: { + aggName: { + max: { + field: 'alert.actions.group', + }, + }, + }, + }, + }, + }); + }); + + test('Validate an aggregation without the attribute field', () => { + expect( + validateGetSavedObjectsAggs( + ['alert'], + { aggName: { terms: { 'alert.attributes.actions.group': ['myFriend', 'snoopy'] } } }, + mockMappings + ) + ).toEqual({ + aggName: { + terms: { + 'alert.actions.group': ['myFriend', 'snoopy'], + }, + }, + }); + }); + + test('Validate a filter term aggregations', () => { + expect( + validateGetSavedObjectsAggs( + ['foo'], + { aggName: { filter: { term: { 'foo.attributes.bytes': 10 } } } }, + mockMappings + ) + ).toEqual({ + aggName: { + filter: { + term: { 'foo.attributes.bytes': 10 }, + }, + }, + }); + }); + test('Throw an error when types is not allowed', () => { expect(() => { validateGetSavedObjectsAggs( @@ -68,7 +134,37 @@ describe('Filter Utils', () => { }).toThrowErrorMatchingInlineSnapshot(`"This type foo is not allowed: Bad Request"`); }); - test('Throw an error when aggregation is not defined in SavedObjectsAggs', () => { + test('Throw an error when add an invalid attributes ', () => { + expect(() => { + validateGetSavedObjectsAggs( + ['foo'], + { + aggName: { + max: { field: 'foo.attributes.bytes', notValid: 'yesIamNotValid' }, + }, + }, + mockMappings + ); + }).toThrowErrorMatchingInlineSnapshot( + `"notValid attribute is not supported in max saved objects aggregation: Bad Request"` + ); + }); + + test('Throw an error when an attributes is not defined correctly', () => { + expect(() => + validateGetSavedObjectsAggs( + ['alert'], + { + aggName: { + terms: { 'alert.attributes.actions.group': ['myFriend', 'snoopy'], missing: 0 }, + }, + }, + mockMappings + ) + ).toThrowErrorMatchingInlineSnapshot(`"Invalid value 0 supplied to : string: Bad Request"`); + }); + + test('Throw an error when aggregation is not defined in SavedObjectsAggs', () => { expect(() => { validateGetSavedObjectsAggs( ['foo'], @@ -76,15 +172,38 @@ describe('Filter Utils', () => { aggName: { MySuperAgg: { field: 'foo.attributes.bytes' }, }, - } as SavedObjectsAggs, + }, mockMappings ); }).toThrowErrorMatchingInlineSnapshot( - `"Invalid value {\\"aggName\\":{\\"MySuperAgg\\":{\\"field\\":\\"foo.attributes.bytes\\"}}}, excess properties: [\\"MySuperAgg\\"]: Bad Request"` + `"This aggregation MySuperAgg is not valid or we did not defined it yet: Bad Request"` ); }); - test('Throw an error when you add attributes who are not defined in SavedObjectsAggs', () => { + test('Throw an error when children aggregation is not defined in SavedObjectsAggs', () => { + expect(() => { + validateGetSavedObjectsAggs( + ['foo'], + { + aggName: { + cardinality: { + field: 'foo.attributes.bytes', + aggs: { + aggName: { + MySuperAgg: { field: 'alert.attributes.actions.group' }, + }, + }, + }, + }, + }, + mockMappings + ); + }).toThrowErrorMatchingInlineSnapshot( + `"This aggregation MySuperAgg is not valid or we did not defined it yet: Bad Request"` + ); + }); + + test('Throw an error when you add the script attribute who are not defined in SavedObjectsAggs', () => { expect(() => { validateGetSavedObjectsAggs( ['alert'], @@ -93,11 +212,37 @@ describe('Filter Utils', () => { cardinality: { field: 'alert.attributes.actions.group' }, script: 'I want to access that I should not', }, - } as SavedObjectsAggs, + }, + mockMappings + ); + }).toThrowErrorMatchingInlineSnapshot( + `"script attribute is not supported in saved objects aggregation: Bad Request"` + ); + }); + + test('Throw an error when you add the script attribute in a nested aggregations who are not defined in SavedObjectsAggs', () => { + expect(() => { + validateGetSavedObjectsAggs( + ['alert'], + { + aggName: { + cardinality: { + field: 'alert.attributes.actions.group', + aggs: { + aggName: { + max: { + field: 'alert.attributes.actions.group', + script: 'I want to access that I should not', + }, + }, + }, + }, + }, + }, mockMappings ); }).toThrowErrorMatchingInlineSnapshot( - `"Invalid value {\\"aggName\\":{\\"cardinality\\":{\\"field\\":\\"alert.attributes.actions.group\\"},\\"script\\":\\"I want to access that I should not\\"}}, excess properties: [\\"script\\"]: Bad Request"` + `"script attribute is not supported in saved objects aggregation: Bad Request"` ); }); }); diff --git a/src/core/server/saved_objects/service/lib/aggs_utils.ts b/src/core/server/saved_objects/service/lib/aggs_utils.ts index b88ce5b50dc8b..2642da56f6b82 100644 --- a/src/core/server/saved_objects/service/lib/aggs_utils.ts +++ b/src/core/server/saved_objects/service/lib/aggs_utils.ts @@ -21,38 +21,82 @@ import { IndexMapping } from '../../mappings'; import { SavedObjectsErrorHelpers } from './errors'; import { hasFilterKeyError } from './filter_utils'; -import { SavedObjectsAggs, validateSavedObjectTypeAggs } from './saved_objects_aggs_types'; +import { savedObjectsAggs, validateSavedObjectsTypeAggs } from './saved_objects_aggs_types'; export const validateGetSavedObjectsAggs = ( allowedTypes: string[], - aggs: SavedObjectsAggs, + aggs: Record, indexMapping: IndexMapping -): SavedObjectsAggs => { - validateSavedObjectTypeAggs(aggs); +) => { return validateGetAggFieldValue(allowedTypes, aggs, indexMapping); }; const validateGetAggFieldValue = ( allowedTypes: string[], aggs: any, - indexMapping: IndexMapping -): SavedObjectsAggs => { + indexMapping: IndexMapping, + lastKey?: string, + aggType?: string +): unknown => { return Object.keys(aggs).reduce((acc, key) => { - if (key === 'field') { - const error = hasFilterKeyError(aggs[key], allowedTypes, indexMapping); + if (key === 'script') { + throw SavedObjectsErrorHelpers.createBadRequestError( + 'script attribute is not supported in saved objects aggregation' + ); + } + if (typeof aggs[key] === 'object' && aggType === undefined && savedObjectsAggs[key]) { + return { + ...acc, + [key]: validateGetAggFieldValue(allowedTypes, aggs[key], indexMapping, key, key), + }; + } else if ( + typeof aggs[key] === 'object' && + (['aggs', 'aggregations'].includes(key) || aggType === undefined) + ) { + return { + ...acc, + [key]: validateGetAggFieldValue(allowedTypes, aggs[key], indexMapping, key, undefined), + }; + } else if ( + key !== 'field' && + aggType && + savedObjectsAggs[aggType] !== undefined && + savedObjectsAggs[aggType][key] !== undefined + ) { + validateSavedObjectsTypeAggs(savedObjectsAggs[aggType][key], aggs[key]); + return { + ...acc, + [key]: aggs[key], + }; + } else { + if (aggType === undefined || savedObjectsAggs[aggType] === undefined) { + throw SavedObjectsErrorHelpers.createBadRequestError( + `This aggregation ${lastKey} is not valid or we did not defined it yet` + ); + } + const error = hasFilterKeyError( + key === 'field' ? aggs[key] : key, + allowedTypes, + indexMapping + ); if (error != null) { + if ( + aggType !== undefined && + savedObjectsAggs[aggType] !== undefined && + savedObjectsAggs[aggType][key] === undefined + ) { + throw SavedObjectsErrorHelpers.createBadRequestError( + `${key} attribute is not supported in ${aggType} saved objects aggregation` + ); + } throw SavedObjectsErrorHelpers.createBadRequestError(error); } return { ...acc, - [key]: aggs[key].replace('.attributes', ''), + ...(key === 'field' + ? { [key]: aggs[key].replace('.attributes', '') } + : { [key.replace('.attributes', '')]: aggs[key] }), }; - } else if (typeof aggs[key] === 'object') { - return { ...acc, [key]: validateGetAggFieldValue(allowedTypes, aggs[key], indexMapping) }; } - return { - ...acc, - [key]: aggs[key], - }; }, {}); }; diff --git a/src/core/server/saved_objects/service/lib/index.ts b/src/core/server/saved_objects/service/lib/index.ts index 8972c57b2464a..eae8c5ef2e10c 100644 --- a/src/core/server/saved_objects/service/lib/index.ts +++ b/src/core/server/saved_objects/service/lib/index.ts @@ -31,5 +31,4 @@ export { export { SavedObjectsErrorHelpers } from './errors'; -export { SavedObjectsAggs } from './saved_objects_aggs_types'; export { SavedObjectsUtils } from './utils'; diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 830b9cdd1136a..8fcd4cbc4f3cf 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -765,26 +765,25 @@ export class SavedObjectsRepository { } let kueryNode; - - try { - if (filter) { + if (filter) { + try { kueryNode = validateConvertFilterToKueryNode(allowedTypes, filter, this._mappings); - } - } catch (e) { - if (e.name === 'KQLSyntaxError') { - throw SavedObjectsErrorHelpers.createBadRequestError('KQLSyntaxError: ' + e.message); - } else { - throw e; + } catch (e) { + if (e.name === 'KQLSyntaxError') { + throw SavedObjectsErrorHelpers.createBadRequestError('KQLSyntaxError: ' + e.message); + } else { + throw e; + } } } let aggsObject = null; - try { - if (aggs) { + if (aggs) { + try { aggsObject = validateGetSavedObjectsAggs(allowedTypes, aggs, this._mappings); + } catch (e) { + throw e; } - } catch (e) { - throw e; } const esOptions = { diff --git a/src/core/server/saved_objects/service/lib/saved_objects_aggs_types/bucket_aggs/index.ts b/src/core/server/saved_objects/service/lib/saved_objects_aggs_types/bucket_aggs/index.ts index 8be958a94b4c1..cb9c5febce6e3 100644 --- a/src/core/server/saved_objects/service/lib/saved_objects_aggs_types/bucket_aggs/index.ts +++ b/src/core/server/saved_objects/service/lib/saved_objects_aggs_types/bucket_aggs/index.ts @@ -19,30 +19,31 @@ import * as rt from 'io-ts'; -import { FieldBasicRT } from '../helpers'; +import { fieldBasic } from '../helpers'; -export const BucketAggsTypeRt = rt.partial({ - filter: rt.type({ - term: rt.record(rt.string, rt.string), - }), - histogram: rt.intersection([ - FieldBasicRT, - rt.type({ interval: rt.number }), - rt.partial({ - min_doc_count: rt.number, - extended_bounds: rt.type({ min: rt.number, max: rt.number }), - keyed: rt.boolean, - missing: rt.number, - order: rt.record(rt.string, rt.literal('asc', 'desc')), - }), - ]), - terms: rt.intersection([ - FieldBasicRT, - rt.partial({ - field: rt.string, - size: rt.number, - show_term_doc_count_error: rt.boolean, - order: rt.record(rt.string, rt.literal('asc', 'desc')), - }), - ]), -}); +export const bucketAggsType: Record> = { + filter: { + term: rt.record(rt.string, rt.any), + }, + histogram: { + ...fieldBasic, + interval: rt.number, + min_doc_count: rt.number, + extended_bounds: rt.type({ min: rt.number, max: rt.number }), + keyed: rt.boolean, + missing: rt.number, + order: rt.record(rt.string, rt.literal('asc', 'desc')), + }, + terms: { + ...fieldBasic, + collect_mode: rt.string, + exclude: rt.unknown, + execution_hint: rt.string, + include: rt.unknown, + missing: rt.string, + min_doc_count: rt.number, + size: rt.number, + show_term_doc_count_error: rt.boolean, + order: rt.record(rt.string, rt.literal('asc', 'desc')), + }, +}; diff --git a/src/core/server/saved_objects/service/lib/saved_objects_aggs_types/helpers.test.ts b/src/core/server/saved_objects/service/lib/saved_objects_aggs_types/helpers.test.ts deleted file mode 100644 index 2feb7fa6c7f7e..0000000000000 --- a/src/core/server/saved_objects/service/lib/saved_objects_aggs_types/helpers.test.ts +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import * as rt from 'io-ts'; -import { PathReporter } from 'io-ts/lib/PathReporter'; - -import { excess } from './helpers'; - -const runDecode = (codec: rt.Type, data: any) => { - const result = codec.decode(data); - return PathReporter.report(result); -}; - -describe('Saved object aggs helpers', () => { - describe('happy path', () => { - test('excess Record', () => { - const codec = excess( - rt.record( - rt.string, - rt.partial({ - max: rt.type({ field: rt.string }), - }) - ) - ); - - expect(runDecode(codec, { aggName: { max: { field: 'hi' } } })).toEqual(['No errors!']); - }); - - test('excess Record', () => { - const codec = excess( - rt.record( - rt.string, - rt.intersection([ - rt.partial({ - max: rt.type({ field: rt.string }), - }), - rt.partial({ - min: rt.type({ field: rt.string }), - }), - ]) - ) - ); - - expect(runDecode(codec, { aggName: { min: { field: 'hi' } } })).toEqual(['No errors!']); - }); - - test('When you intersection as a DictionnaryType', () => { - const codec = excess( - rt.record( - rt.string, - rt.intersection([ - rt.partial({ - max: rt.type({ field: rt.string }), - }), - rt.partial({ - filter: rt.type({ field: rt.string }), - aggs: rt.record( - rt.string, - rt.partial({ - min: rt.type({ field: rt.string }), - }) - ), - }), - ]) - ) - ); - - expect( - runDecode(codec, { - aggName: { filter: { field: 'hi' }, aggs: { aggNewName: { min: { field: 'low' } } } }, - }) - ).toEqual(['No errors!']); - }); - }); - - describe('Errors', () => { - test('throw error when you add an attributes who is not expected for Record', () => { - const codec = excess( - rt.record( - rt.string, - rt.partial({ - max: rt.type({ field: rt.string }), - }) - ) - ); - - expect(runDecode(codec, { aggName: { max: { field: 'hi', script: '' } } })).toEqual([ - 'Invalid value {"aggName":{"max":{"field":"hi","script":""}}}, excess properties: ["script"]', - ]); - }); - - test('throw error when you add an attributes who is not expected for Record', () => { - const codec = excess( - rt.record( - rt.string, - rt.intersection([ - rt.partial({ - max: rt.type({ field: rt.string }), - }), - rt.partial({ - min: rt.type({ field: rt.string }), - }), - ]) - ) - ); - - expect(runDecode(codec, { aggName: { min: { field: 'hi', script: 'field' } } })).toEqual([ - 'Invalid value {"aggName":{"min":{"field":"hi","script":"field"}}}, excess properties: ["script"]', - ]); - }); - - test('throw error when you do not match types for Record', () => { - const codec = excess( - rt.record( - rt.string, - rt.partial({ - max: rt.type({ field: rt.string }), - }) - ) - ); - - expect(runDecode(codec, { aggName: { max: { field: 33 } } })).toEqual([ - 'Invalid value 33 supplied to : { [K in string]: Partial<{ max: { field: string } }> }/aggName: Partial<{ max: { field: string } }>/max: { field: string }/field: string', - ]); - }); - - test('throw error when when you do not match types for Record', () => { - const codec = excess( - rt.record( - rt.string, - rt.intersection([ - rt.partial({ - max: rt.type({ field: rt.string }), - }), - rt.partial({ - min: rt.type({ field: rt.string }), - }), - ]) - ) - ); - - expect(runDecode(codec, { aggName: { min: { field: 33 } } })).toEqual([ - 'Invalid value 33 supplied to : { [K in string]: (Partial<{ max: { field: string } }> & Partial<{ min: { field: string } }>) }/aggName: (Partial<{ max: { field: string } }> & Partial<{ min: { field: string } }>)/1: Partial<{ min: { field: string } }>/min: { field: string }/field: string', - ]); - }); - - test('throw error when you add an attributes in your second agg who is not expected for Record', () => { - const codec = excess( - rt.record( - rt.string, - rt.intersection([ - rt.partial({ - max: rt.type({ field: rt.string }), - }), - rt.partial({ - filter: rt.type({ field: rt.string }), - aggs: rt.record( - rt.string, - rt.partial({ - min: rt.type({ field: rt.string }), - }) - ), - }), - ]) - ) - ); - - expect( - runDecode(codec, { - aggName: { - filter: { field: 'hi' }, - aggs: { aggNewName: { min: { field: 'low' }, script: 'error' } }, - }, - }) - ).toEqual([ - 'Invalid value {"aggName":{"filter":{"field":"hi"},"aggs":{"aggNewName":{"min":{"field":"low"},"script":"error"}}}}, excess properties: ["script"]', - ]); - }); - }); -}); diff --git a/src/core/server/saved_objects/service/lib/saved_objects_aggs_types/helpers.ts b/src/core/server/saved_objects/service/lib/saved_objects_aggs_types/helpers.ts index 6475d97ec6e79..6485639636ec3 100644 --- a/src/core/server/saved_objects/service/lib/saved_objects_aggs_types/helpers.ts +++ b/src/core/server/saved_objects/service/lib/saved_objects_aggs_types/helpers.ts @@ -17,100 +17,14 @@ * under the License. */ -import { either } from 'fp-ts/lib/Either'; import * as rt from 'io-ts'; import { failure } from 'io-ts/lib/PathReporter'; -import { get } from 'lodash'; type ErrorFactory = (message: string) => Error; +export const fieldBasic = { field: rt.string }; export const FieldBasicRT = rt.type({ field: rt.string }); export const throwErrors = (createError: ErrorFactory) => (errors: rt.Errors) => { throw createError(failure(errors).join('\n')); }; - -const getProps = (codec: rt.HasProps | rt.RecordC): rt.Props | null => { - if (codec == null) { - return null; - } - switch (codec._tag) { - case 'DictionaryType': - if (codec.codomain.props != null) { - return codec.codomain.props; - } - const types: rt.HasProps[] = codec.codomain.types; - return types.reduce((props, type) => Object.assign(props, getProps(type)), {}); - case 'RefinementType': - case 'ReadonlyType': - return getProps(codec.type); - case 'InterfaceType': - case 'StrictType': - case 'PartialType': - return codec.props; - case 'IntersectionType': - return codec.types.reduce( - (props, type) => Object.assign(props, getProps(type)), - {} - ); - default: - return null; - } -}; - -const getExcessProps = ( - props: rt.Props | rt.RecordC, - r: Record -): string[] => - Object.keys(r).reduce((acc, k) => { - const codecChildren = get(props, [k]); - const childrenProps = getProps(codecChildren); - const childrenObject = r[k] as Record; - if (codecChildren != null && childrenProps != null && codecChildren._tag === 'DictionaryType') { - const keys = Object.keys(childrenObject); - return [ - ...acc, - ...keys.reduce( - (kAcc, i) => [...kAcc, ...getExcessProps(childrenProps, childrenObject[i])], - [] - ), - ]; - } - if (props.hasOwnProperty(k) && childrenProps != null) { - return [...acc, ...getExcessProps(childrenProps, childrenObject)]; - } else if (!props.hasOwnProperty(k)) { - return [...acc, k]; - } - return acc; - }, []); - -export const excess = (codec: rt.RecordC): rt.InterfaceType => { - const codecProps = getProps(codec); - - const r = new rt.DictionaryType( - codec.name, - codec.is, - (u, c) => - either.chain(codec.validate(u, c), (o: Record) => { - if (codecProps == null) { - return rt.failure(u, c, `Invalid Aggs object ${JSON.stringify(u)}`); - } - const keys = Object.keys(o); - const ex = keys.reduce((acc, k) => { - return [...acc, ...getExcessProps(codecProps, o[k])]; - }, []); - - return ex.length > 0 - ? rt.failure( - u, - c, - `Invalid value ${JSON.stringify(u)}, excess properties: ${JSON.stringify(ex)}` - ) - : codec.validate(u, c); - }), - codec.encode, - codec.domain, - codec.codomain - ); - return r as any; -}; diff --git a/src/core/server/saved_objects/service/lib/saved_objects_aggs_types/index.ts b/src/core/server/saved_objects/service/lib/saved_objects_aggs_types/index.ts index afe8e3b8b4a18..a60b38ed4db45 100644 --- a/src/core/server/saved_objects/service/lib/saved_objects_aggs_types/index.ts +++ b/src/core/server/saved_objects/service/lib/saved_objects_aggs_types/index.ts @@ -23,24 +23,20 @@ import { pipe } from 'fp-ts/lib/pipeable'; import * as rt from 'io-ts'; -import { BucketAggsTypeRt } from './bucket_aggs'; -import { MetricsAggsTypeRt } from './metrics_aggs'; +import { bucketAggsType } from './bucket_aggs'; +import { metricsAggsType } from './metrics_aggs'; import { SavedObjectsErrorHelpers } from '../errors'; -import { excess, throwErrors } from './helpers'; +import { throwErrors } from './helpers'; -const AllAggsRt = rt.intersection([BucketAggsTypeRt, MetricsAggsTypeRt]); - -const SavedObjectsAggsRt = rt.record( - rt.string, - rt.intersection([AllAggsRt, rt.partial({ aggs: AllAggsRt })]) -); - -export type SavedObjectsAggs = rt.TypeOf; +export const savedObjectsAggs = { + ...metricsAggsType, + ...bucketAggsType, +}; -export const validateSavedObjectTypeAggs = (aggObjects: SavedObjectsAggs) => { +export const validateSavedObjectsTypeAggs = (rtType: rt.Any, aggObject: unknown) => { pipe( - excess(SavedObjectsAggsRt).decode(aggObjects), + rtType.decode(aggObject), fold(throwErrors(SavedObjectsErrorHelpers.createBadRequestError), identity) ); }; diff --git a/src/core/server/saved_objects/service/lib/saved_objects_aggs_types/metrics_aggs/index.ts b/src/core/server/saved_objects/service/lib/saved_objects_aggs_types/metrics_aggs/index.ts index 169d8d5c426a0..7de8ed12dcc69 100644 --- a/src/core/server/saved_objects/service/lib/saved_objects_aggs_types/metrics_aggs/index.ts +++ b/src/core/server/saved_objects/service/lib/saved_objects_aggs_types/metrics_aggs/index.ts @@ -19,7 +19,7 @@ import * as rt from 'io-ts'; -import { FieldBasicRT } from '../helpers'; +import { fieldBasic, FieldBasicRT } from '../helpers'; /* * Types for Metrics Aggregations @@ -39,24 +39,24 @@ import { FieldBasicRT } from '../helpers'; * - Median Absolute Deviation Aggregation https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-median-absolute-deviation-aggregation.html */ -const MinMaxAggBodyRt = rt.intersection([FieldBasicRT, rt.partial({ missing: rt.number })]); - -export const MetricsAggsTypeRt = rt.partial({ - avg: FieldBasicRT, - weighted_avg: rt.intersection([ - rt.type({ - value: rt.intersection([FieldBasicRT, rt.partial({ missing: rt.number })]), - weight: rt.intersection([FieldBasicRT, rt.partial({ missing: rt.number })]), - }), - rt.partial({ - format: rt.string, - value_type: rt.string, - }), - ]), - cardinality: FieldBasicRT, - max: MinMaxAggBodyRt, - min: MinMaxAggBodyRt, - top_hits: rt.partial({ +export const metricsAggsType: Record> = { + avg: fieldBasic, + weighted_avg: { + value: rt.intersection([FieldBasicRT, rt.partial({ missing: rt.number })]), + weight: rt.intersection([FieldBasicRT, rt.partial({ missing: rt.number })]), + format: rt.string, + value_type: rt.string, + }, + cardinality: fieldBasic, + max: { + ...fieldBasic, + missing: rt.number, + }, + min: { + ...fieldBasic, + missing: rt.number, + }, + top_hits: { explain: rt.boolean, from: rt.string, highlight: rt.any, @@ -70,5 +70,5 @@ export const MetricsAggsTypeRt = rt.partial({ includes: rt.array(rt.string), excludes: rt.array(rt.string), }), - }), -}); + }, +}; diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts index d6ef6a29a7b06..601ac8f891158 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -17,7 +17,7 @@ * under the License. */ -import { ISavedObjectsRepository, SavedObjectsAggs } from './lib'; +import { ISavedObjectsRepository } from './lib'; import { SavedObject, SavedObjectError, @@ -285,12 +285,6 @@ export interface SavedObjectsUpdateResponse references: SavedObjectReference[] | undefined; } -/** - * - * @public - */ -export { SavedObjectsAggs }; - /** * * @public diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index a2cf8c5a593ee..a9b0fd5ee9e87 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -17,7 +17,7 @@ * under the License. */ -import { SavedObjectsClient, SavedObjectsAggs } from './service/saved_objects_client'; +import { SavedObjectsClient } from './service/saved_objects_client'; import { SavedObjectsTypeMappingDefinition } from './mappings'; import { SavedObjectMigrationMap } from './migrations'; @@ -116,7 +116,7 @@ export interface SavedObjectsFindOptions { * aggs = { type_count: { max: { field: 'dashboard.attributes.version' } } }; * SavedObjects.find({type: 'dashboard', aggs: '%7B%22type_count%22%3A%7B%22max%22%3A%7B%22field%22%3A%22dashboard.attributes.version%22%7D%7D%7D'}) */ - aggs?: SavedObjectsAggs; + aggs?: Record; namespaces?: string[]; /** * This map defines each type to search for, and the namespace(s) to search for the type in; this is only intended to be used by a saved diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 0c2984c54c34c..fbfe177c65a9d 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -132,7 +132,6 @@ import { RenderSearchTemplateParams } from 'elasticsearch'; import { Request } from '@hapi/hapi'; import { ResponseObject } from '@hapi/hapi'; import { ResponseToolkit } from '@hapi/hapi'; -import * as rt from 'io-ts'; import { SchemaTypeError } from '@kbn/config-schema'; import { ScrollParams } from 'elasticsearch'; import { SearchParams } from 'elasticsearch'; @@ -1893,12 +1892,6 @@ export interface SavedObjectsAddToNamespacesResponse { namespaces: string[]; } -// Warning: (ae-forgotten-export) The symbol "SavedObjectsAggsRt" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "SavedObjectsAggs" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type SavedObjectsAggs = rt.TypeOf; - // Warning: (ae-forgotten-export) The symbol "SavedObjectDoc" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "Referencable" needs to be exported by the entry point index.d.ts // @@ -2195,8 +2188,8 @@ export type SavedObjectsFieldMapping = SavedObjectsCoreFieldMapping | SavedObjec // @public (undocumented) export interface SavedObjectsFindOptions { - // (undocumented) - aggs?: SavedObjectsAggs; + // @alpha + aggs?: Record; defaultSearchOperator?: 'AND' | 'OR'; fields?: string[]; // Warning: (ae-forgotten-export) The symbol "KueryNode" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 46a45dc10665e..fc9b8d4839ea3 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -69,7 +69,6 @@ import { Reporter } from '@kbn/analytics'; import { RequestAdapter } from 'src/plugins/inspector/common'; import { RequestStatistics as RequestStatistics_2 } from 'src/plugins/inspector/common'; import { Required } from '@kbn/utility-types'; -import * as rt from 'io-ts'; import * as Rx from 'rxjs'; import { SavedObject } from 'kibana/server'; import { SavedObject as SavedObject_2 } from 'src/core/server'; diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 03838b870d4fb..f3f3682404e32 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -62,7 +62,6 @@ import React from 'react'; import { RecursiveReadonly } from '@kbn/utility-types'; import { RequestAdapter as RequestAdapter_2 } from 'src/plugins/inspector/common'; import { Required } from '@kbn/utility-types'; -import * as rt from 'io-ts'; import * as Rx from 'rxjs'; import { SavedObject as SavedObject_2 } from 'kibana/server'; import { SavedObject as SavedObject_3 } from 'src/core/server'; diff --git a/test/api_integration/apis/index.js b/test/api_integration/apis/index.js index d07c099634005..65a388d5a52e8 100644 --- a/test/api_integration/apis/index.js +++ b/test/api_integration/apis/index.js @@ -19,20 +19,20 @@ export default function ({ loadTestFile }) { describe('apis', () => { - loadTestFile(require.resolve('./core')); - loadTestFile(require.resolve('./general')); - loadTestFile(require.resolve('./home')); - loadTestFile(require.resolve('./index_patterns')); - loadTestFile(require.resolve('./kql_telemetry')); - loadTestFile(require.resolve('./saved_objects_management')); + // loadTestFile(require.resolve('./core')); + // loadTestFile(require.resolve('./general')); + // loadTestFile(require.resolve('./home')); + // loadTestFile(require.resolve('./index_patterns')); + // loadTestFile(require.resolve('./kql_telemetry')); + // loadTestFile(require.resolve('./saved_objects_management')); loadTestFile(require.resolve('./saved_objects')); - loadTestFile(require.resolve('./scripts')); - loadTestFile(require.resolve('./search')); - loadTestFile(require.resolve('./shorten')); - loadTestFile(require.resolve('./suggestions')); - loadTestFile(require.resolve('./status')); - loadTestFile(require.resolve('./stats')); - loadTestFile(require.resolve('./ui_metric')); - loadTestFile(require.resolve('./telemetry')); + // loadTestFile(require.resolve('./scripts')); + // loadTestFile(require.resolve('./search')); + // loadTestFile(require.resolve('./shorten')); + // loadTestFile(require.resolve('./suggestions')); + // loadTestFile(require.resolve('./status')); + // loadTestFile(require.resolve('./stats')); + // loadTestFile(require.resolve('./ui_metric')); + // loadTestFile(require.resolve('./telemetry')); }); } diff --git a/test/api_integration/apis/saved_objects/find.js b/test/api_integration/apis/saved_objects/find.js index 0ea95448f87b9..7e0ca6d84e33d 100644 --- a/test/api_integration/apis/saved_objects/find.js +++ b/test/api_integration/apis/saved_objects/find.js @@ -346,7 +346,7 @@ export default function ({ getService }) { JSON.stringify({ type_count: { max: { - field: 'dashboard.attributes.version', + field: 'visualization.attributes.version', script: 'Oh yes I am going to a script', }, }, @@ -359,7 +359,7 @@ export default function ({ getService }) { expect(resp.body).to.eql({ error: 'Bad Request', message: - 'Invalid value {"type_count":{"max":{"field":"dashboard.attributes.version","script":"Oh yes I am going to a script"}}}, excess properties: ["script"]: Bad Request', + 'script attribute is not supported in saved objects aggregation: Bad Request', statusCode: 400, }); })); diff --git a/test/api_integration/apis/saved_objects/index.js b/test/api_integration/apis/saved_objects/index.js index ad6c3749181dd..bcdaa80b6334e 100644 --- a/test/api_integration/apis/saved_objects/index.js +++ b/test/api_integration/apis/saved_objects/index.js @@ -19,17 +19,17 @@ export default function ({ loadTestFile }) { describe('saved_objects', () => { - loadTestFile(require.resolve('./bulk_create')); - loadTestFile(require.resolve('./bulk_get')); - loadTestFile(require.resolve('./create')); - loadTestFile(require.resolve('./delete')); - loadTestFile(require.resolve('./export')); + // loadTestFile(require.resolve('./bulk_create')); + // loadTestFile(require.resolve('./bulk_get')); + // loadTestFile(require.resolve('./create')); + // loadTestFile(require.resolve('./delete')); + // loadTestFile(require.resolve('./export')); loadTestFile(require.resolve('./find')); - loadTestFile(require.resolve('./get')); - loadTestFile(require.resolve('./import')); - loadTestFile(require.resolve('./resolve_import_errors')); - loadTestFile(require.resolve('./update')); - loadTestFile(require.resolve('./bulk_update')); - loadTestFile(require.resolve('./migrations')); + // loadTestFile(require.resolve('./get')); + // loadTestFile(require.resolve('./import')); + // loadTestFile(require.resolve('./resolve_import_errors')); + // loadTestFile(require.resolve('./update')); + // loadTestFile(require.resolve('./bulk_update')); + // loadTestFile(require.resolve('./migrations')); }); } From 4e319b3886c5c52354ed48b17ef089298d840d2d Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Mon, 23 Nov 2020 10:32:12 -0500 Subject: [PATCH 19/21] Validate multiple items nested filter query through KueryNode --- .../service/lib/filter_utils.test.ts | 28 +++++++++++++++++++ .../saved_objects/service/lib/filter_utils.ts | 12 +++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/core/server/saved_objects/service/lib/filter_utils.test.ts b/src/core/server/saved_objects/service/lib/filter_utils.test.ts index 3e4845f526df7..bad5a10acffa0 100644 --- a/src/core/server/saved_objects/service/lib/filter_utils.test.ts +++ b/src/core/server/saved_objects/service/lib/filter_utils.test.ts @@ -494,5 +494,33 @@ describe('Filter Utils', () => { }, ]); }); + + test('Validate multiple items nested filter query through KueryNode', () => { + const validationObject = validateFilterKueryNode({ + astFilter: esKuery.fromKueryExpression( + 'alert.attributes.actions:{ actionTypeId: ".server-log" AND actionRef: "foo" }' + ), + types: ['alert'], + indexMapping: mockMappings, + }); + + // nodes will have errors in the array + expect(validationObject).toEqual([ + { + astPath: 'arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'alert.attributes.actions.actionTypeId', + type: 'alert', + }, + { + astPath: 'arguments.1.arguments.1', + error: null, + isSavedObjectAttr: false, + key: 'alert.attributes.actions.actionRef', + type: 'alert', + }, + ]); + }); }); }); diff --git a/src/core/server/saved_objects/service/lib/filter_utils.ts b/src/core/server/saved_objects/service/lib/filter_utils.ts index be36807f0d02b..6e46fc3eff008 100644 --- a/src/core/server/saved_objects/service/lib/filter_utils.ts +++ b/src/core/server/saved_objects/service/lib/filter_utils.ts @@ -19,6 +19,7 @@ import { set } from '@elastic/safer-lodash-set'; import { get } from 'lodash'; +import { string } from 'joi'; import { SavedObjectsErrorHelpers } from './errors'; import { IndexMapping } from '../../mappings'; // @ts-expect-error no ts @@ -120,7 +121,15 @@ export const validateFilterKueryNode = ({ return astFilter.arguments.reduce((kueryNode: string[], ast: KueryNode, index: number) => { if (hasNestedKey && ast.type === 'literal' && ast.value != null) { localNestedKeys = ast.value; + } else if (ast.type === 'literal' && ast.value && typeof ast.value === 'string') { + const key = ast.value.replace('.attributes', ''); + const mappingKey = 'properties.' + key.split('.').join('.properties.'); + const field = get(indexMapping, mappingKey); + if (field != null && field.type === 'nested') { + localNestedKeys = ast.value; + } } + if (ast.arguments) { const myPath = `${path}.${index}`; return [ @@ -132,7 +141,7 @@ export const validateFilterKueryNode = ({ storeValue: ast.type === 'function' && astFunctionType.includes(ast.function), path: `${myPath}.arguments`, hasNestedKey: ast.type === 'function' && ast.function === 'nested', - nestedKeys: localNestedKeys, + nestedKeys: localNestedKeys || nestedKeys, }), ]; } @@ -186,6 +195,7 @@ export const hasFilterKeyError = ( types: string[], indexMapping: IndexMapping ): string | null => { + // console.log('hasFilterKeyError', indexMapping, key) if (key == null) { return `The key is empty and needs to be wrapped by a saved object type like ${types.join()}`; } From 16de8751ab3143e76de7c6ea10bcd6f855263037 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Mon, 23 Nov 2020 10:34:27 -0500 Subject: [PATCH 20/21] remove unused import --- src/core/server/saved_objects/service/lib/filter_utils.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/core/server/saved_objects/service/lib/filter_utils.ts b/src/core/server/saved_objects/service/lib/filter_utils.ts index 6e46fc3eff008..5c7e9dae70d7f 100644 --- a/src/core/server/saved_objects/service/lib/filter_utils.ts +++ b/src/core/server/saved_objects/service/lib/filter_utils.ts @@ -19,7 +19,6 @@ import { set } from '@elastic/safer-lodash-set'; import { get } from 'lodash'; -import { string } from 'joi'; import { SavedObjectsErrorHelpers } from './errors'; import { IndexMapping } from '../../mappings'; // @ts-expect-error no ts @@ -195,7 +194,6 @@ export const hasFilterKeyError = ( types: string[], indexMapping: IndexMapping ): string | null => { - // console.log('hasFilterKeyError', indexMapping, key) if (key == null) { return `The key is empty and needs to be wrapped by a saved object type like ${types.join()}`; } From cb7ada373acb516b2bd29cf8a01850d491b35678 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Mon, 23 Nov 2020 10:52:02 -0500 Subject: [PATCH 21/21] review + put back test --- docs/api/saved-objects/find.asciidoc | 4 +- .../service/lib/filter_utils.test.ts | 42 +++++++++---------- .../saved_objects/service/lib/repository.ts | 1 - src/core/server/saved_objects/types.ts | 4 +- test/api_integration/apis/index.js | 28 ++++++------- .../apis/saved_objects/index.js | 22 +++++----- 6 files changed, 50 insertions(+), 51 deletions(-) diff --git a/docs/api/saved-objects/find.asciidoc b/docs/api/saved-objects/find.asciidoc index 03e2b3414a8df..73a8fbf0a820f 100644 --- a/docs/api/saved-objects/find.asciidoc +++ b/docs/api/saved-objects/find.asciidoc @@ -54,12 +54,12 @@ experimental[] Retrieve a paginated set of {kib} saved objects by various condit `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`, + It should look like that savedObjectType.attributes.title: "myTitle". However, If you used a direct attribute of a saved object like `updated_at`, you will have to define your filter like that savedObjectType.updatedAt > 2018-12-22. `aggs`:: (Optional, string) The aggs will support aggregation string with the caveat that your field from the aggregation will have the attribute from your type saved object, - it should look like this: savedObjectType.attributes.field. However, If you use a direct attribute of a saved object like updatedAt, you will have to define your filter like this: savedObjectType.updatedAt. + it should look like this: savedObjectType.attributes.field. However, If you use a direct attribute of a saved object like updatedAt, you will have to define your filter like this: savedObjectType.updated_at. 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. diff --git a/src/core/server/saved_objects/service/lib/filter_utils.test.ts b/src/core/server/saved_objects/service/lib/filter_utils.test.ts index bad5a10acffa0..df7cbda3210f6 100644 --- a/src/core/server/saved_objects/service/lib/filter_utils.test.ts +++ b/src/core/server/saved_objects/service/lib/filter_utils.test.ts @@ -23,7 +23,7 @@ import { validateFilterKueryNode, validateConvertFilterToKueryNode } from './fil export const mockMappings = { properties: { - updatedAt: { + updated_at: { type: 'date', }, foo: { @@ -104,12 +104,12 @@ describe('Filter Utils', () => { 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 :*)', + 'foo.updated_at: 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 :*)', mockMappings ) ).toEqual( esKuery.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 :*)' + '(type: foo and updated_at: 5678654567) and foo.bytes > 1000 and foo.bytes < 8000 and foo.title: "best" and (foo.description: t* or foo.description :*)' ) ); }); @@ -118,12 +118,12 @@ describe('Filter Utils', () => { 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 :*)', + 'foo.updated_at: 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 :*)', mockMappings ) ).toEqual( esKuery.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 :*)' + '(type: foo and updated_at: 5678654567) and foo.bytes > 1000 and foo.bytes < 8000 and foo.title: "best" and (foo.description: t* or foo.description :*)' ) ); }); @@ -132,12 +132,12 @@ describe('Filter Utils', () => { 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 :*)', + '(bar.updated_at: 5678654567 OR foo.updated_at: 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 :*)', mockMappings ) ).toEqual( esKuery.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 :*)' + '((type: bar and updated_at: 5678654567) or (type: foo and updated_at: 5678654567)) and foo.bytes > 1000 and foo.bytes < 8000 and foo.title: "best" and (foo.description: t* or bar.description :*)' ) ); }); @@ -156,11 +156,11 @@ describe('Filter Utils', () => { 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 :*)', + 'updated_at: 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 :*)', mockMappings ); }).toThrowErrorMatchingInlineSnapshot( - `"This key 'updatedAt' need to be wrapped by a saved object type like foo,bar: Bad Request"` + `"This key 'updated_at' need to be wrapped by a saved object type like foo,bar: Bad Request"` ); }); @@ -175,7 +175,7 @@ describe('Filter Utils', () => { test('Validate filter query through KueryNode - happy path', () => { const validationObject = validateFilterKueryNode({ astFilter: esKuery.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.updated_at: 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 :*)' ), types: ['foo'], indexMapping: mockMappings, @@ -186,7 +186,7 @@ describe('Filter Utils', () => { astPath: 'arguments.0', error: null, isSavedObjectAttr: true, - key: 'foo.updatedAt', + key: 'foo.updated_at', type: 'foo', }, { @@ -250,7 +250,7 @@ describe('Filter Utils', () => { test('Return Error if key is not wrapper by a saved object type', () => { const validationObject = validateFilterKueryNode({ astFilter: esKuery.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 :*)' + 'updated_at: 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 :*)' ), types: ['foo'], indexMapping: mockMappings, @@ -259,9 +259,9 @@ describe('Filter Utils', () => { expect(validationObject).toEqual([ { astPath: 'arguments.0', - error: "This key 'updatedAt' need to be wrapped by a saved object type like foo", + error: "This key 'updated_at' need to be wrapped by a saved object type like foo", isSavedObjectAttr: true, - key: 'updatedAt', + key: 'updated_at', type: null, }, { @@ -305,7 +305,7 @@ describe('Filter Utils', () => { test('Return Error if key of a saved object type is not wrapped with attributes', () => { const validationObject = validateFilterKueryNode({ astFilter: esKuery.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.updated_at: 5678654567 and foo.attributes.bytes > 1000 and foo.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.description :*)' ), types: ['foo'], indexMapping: mockMappings, @@ -316,7 +316,7 @@ describe('Filter Utils', () => { astPath: 'arguments.0', error: null, isSavedObjectAttr: true, - key: 'foo.updatedAt', + key: 'foo.updated_at', type: 'foo', }, { @@ -362,7 +362,7 @@ describe('Filter Utils', () => { test('Return Error if filter is not using an allowed type', () => { const validationObject = validateFilterKueryNode({ astFilter: esKuery.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 :*)' + 'bar.updated_at: 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 :*)' ), types: ['foo'], indexMapping: mockMappings, @@ -373,7 +373,7 @@ describe('Filter Utils', () => { astPath: 'arguments.0', error: 'This type bar is not allowed', isSavedObjectAttr: true, - key: 'bar.updatedAt', + key: 'bar.updated_at', type: 'bar', }, { @@ -417,7 +417,7 @@ describe('Filter Utils', () => { test('Return Error if filter is using an non-existing key in the index patterns of the saved object type', () => { const validationObject = validateFilterKueryNode({ astFilter: esKuery.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.updated_at33: 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 :*)' ), types: ['foo'], indexMapping: mockMappings, @@ -426,9 +426,9 @@ describe('Filter Utils', () => { expect(validationObject).toEqual([ { astPath: 'arguments.0', - error: "This key 'foo.updatedAt33' does NOT exist in foo saved object index patterns", + error: "This key 'foo.updated_at33' does NOT exist in foo saved object index patterns", isSavedObjectAttr: false, - key: 'foo.updatedAt33', + key: 'foo.updated_at33', type: 'foo', }, { diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 8fcd4cbc4f3cf..fde2f39ee287b 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -701,7 +701,6 @@ export class SavedObjectsRepository { * @property {Array} [options.fields] * @property {string} [options.namespace] * @property {object} [options.hasReference] - { type, id } - * @property {object} [options.aggs] - see ./saved_object_aggs for more insight * @property {string} [options.preference] * @returns {promise} - { saved_objects: [{ id, type, version, attributes }], total, per_page, page } */ diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index a9b0fd5ee9e87..7bb919b9198c0 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -113,8 +113,8 @@ export interface SavedObjectsFindOptions { * Specify an Elasticsearch aggregation to perform. This alpha API only supports a limited set of aggregation types: metrics, bucket. Additional aggregation types can be contributed to Core. * @alpha * @example - * aggs = { type_count: { max: { field: 'dashboard.attributes.version' } } }; - * SavedObjects.find({type: 'dashboard', aggs: '%7B%22type_count%22%3A%7B%22max%22%3A%7B%22field%22%3A%22dashboard.attributes.version%22%7D%7D%7D'}) + * aggs = { latest_version: { max: { field: 'dashboard.attributes.version' } } }; + * SavedObjects.find({type: 'dashboard', aggs: '%7B%22latest_version%22%3A%7B%22max%22%3A%7B%22field%22%3A%22dashboard.attributes.version%22%7D%7D%7D'}) */ aggs?: Record; namespaces?: string[]; diff --git a/test/api_integration/apis/index.js b/test/api_integration/apis/index.js index 65a388d5a52e8..d07c099634005 100644 --- a/test/api_integration/apis/index.js +++ b/test/api_integration/apis/index.js @@ -19,20 +19,20 @@ export default function ({ loadTestFile }) { describe('apis', () => { - // loadTestFile(require.resolve('./core')); - // loadTestFile(require.resolve('./general')); - // loadTestFile(require.resolve('./home')); - // loadTestFile(require.resolve('./index_patterns')); - // loadTestFile(require.resolve('./kql_telemetry')); - // loadTestFile(require.resolve('./saved_objects_management')); + loadTestFile(require.resolve('./core')); + loadTestFile(require.resolve('./general')); + loadTestFile(require.resolve('./home')); + loadTestFile(require.resolve('./index_patterns')); + loadTestFile(require.resolve('./kql_telemetry')); + loadTestFile(require.resolve('./saved_objects_management')); loadTestFile(require.resolve('./saved_objects')); - // loadTestFile(require.resolve('./scripts')); - // loadTestFile(require.resolve('./search')); - // loadTestFile(require.resolve('./shorten')); - // loadTestFile(require.resolve('./suggestions')); - // loadTestFile(require.resolve('./status')); - // loadTestFile(require.resolve('./stats')); - // loadTestFile(require.resolve('./ui_metric')); - // loadTestFile(require.resolve('./telemetry')); + loadTestFile(require.resolve('./scripts')); + loadTestFile(require.resolve('./search')); + loadTestFile(require.resolve('./shorten')); + loadTestFile(require.resolve('./suggestions')); + loadTestFile(require.resolve('./status')); + loadTestFile(require.resolve('./stats')); + loadTestFile(require.resolve('./ui_metric')); + loadTestFile(require.resolve('./telemetry')); }); } diff --git a/test/api_integration/apis/saved_objects/index.js b/test/api_integration/apis/saved_objects/index.js index bcdaa80b6334e..ad6c3749181dd 100644 --- a/test/api_integration/apis/saved_objects/index.js +++ b/test/api_integration/apis/saved_objects/index.js @@ -19,17 +19,17 @@ export default function ({ loadTestFile }) { describe('saved_objects', () => { - // loadTestFile(require.resolve('./bulk_create')); - // loadTestFile(require.resolve('./bulk_get')); - // loadTestFile(require.resolve('./create')); - // loadTestFile(require.resolve('./delete')); - // loadTestFile(require.resolve('./export')); + loadTestFile(require.resolve('./bulk_create')); + loadTestFile(require.resolve('./bulk_get')); + loadTestFile(require.resolve('./create')); + loadTestFile(require.resolve('./delete')); + loadTestFile(require.resolve('./export')); loadTestFile(require.resolve('./find')); - // loadTestFile(require.resolve('./get')); - // loadTestFile(require.resolve('./import')); - // loadTestFile(require.resolve('./resolve_import_errors')); - // loadTestFile(require.resolve('./update')); - // loadTestFile(require.resolve('./bulk_update')); - // loadTestFile(require.resolve('./migrations')); + loadTestFile(require.resolve('./get')); + loadTestFile(require.resolve('./import')); + loadTestFile(require.resolve('./resolve_import_errors')); + loadTestFile(require.resolve('./update')); + loadTestFile(require.resolve('./bulk_update')); + loadTestFile(require.resolve('./migrations')); }); }