diff --git a/x-pack/legacy/plugins/apm/public/es/aggregations/index.ts b/x-pack/legacy/plugins/apm/public/es/aggregations/index.ts new file mode 100644 index 0000000000000..16abdce0eea16 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/es/aggregations/index.ts @@ -0,0 +1,184 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import * as t from 'io-ts'; + +const numberOrNull = t.union([t.number, t.null]); +const sortOrder = t.union([t.literal('asc'), t.literal('desc')]); + +const sortInstruction = t.dictionary( + t.string, + t.union([ + sortOrder, + t.type({ + order: sortOrder + }) + ]) +); + +const sort = t.union([sortInstruction, t.array(sortInstruction)]); + +const script = t.type({ + lang: t.literal('expression'), + source: t.string +}); + +const metricsRt = { + in: t.union([ + t.type({ + field: t.string + }), + t.type({ + script + }) + ]), + out: t.type({ + value: numberOrNull + }) +}; + +export const aggregationRts = { + terms: { + in: t.intersection([ + t.type({ + field: t.string + }), + t.partial({ + size: t.number, + missing: t.string, + order: sort + }) + ]), + out: t.type({ + buckets: t.array( + t.type({ + doc_count: t.number, + key: t.string + }) + ) + }), + aggs: 'bucket' as const + }, + date_histogram: { + in: t.intersection([ + t.type({ + field: t.string + }), + t.union([ + t.type({ + calendar_interval: t.string + }), + t.type({ + fixed_interval: t.string + }) + ]), + t.partial({ + format: t.string, + min_doc_count: t.number, + extended_bounds: t.type({ + min: t.number, + max: t.number + }) + }) + ]), + out: t.type({ + buckets: t.array( + t.type({ + doc_count: t.number, + key: t.number, + key_as_string: t.string + }) + ) + }), + aggs: 'bucket' as const + }, + histogram: { + in: t.intersection([ + t.type({ + field: t.string, + interval: t.number + }), + t.partial({ + min_doc_count: t.number, + extended_bounds: t.type({ + min: t.number, + max: t.number + }) + }) + ]), + out: t.type({ + buckets: t.array( + t.type({ + doc_count: t.number, + key: t.number + }) + ) + }), + aggs: 'bucket' as const + }, + avg: metricsRt, + max: metricsRt, + min: metricsRt, + sum: metricsRt, + extended_stats: { + in: t.type({ + field: t.string + }), + out: t.type({ + count: t.number, + min: numberOrNull, + max: numberOrNull, + avg: numberOrNull, + sum: t.number, + sum_of_squares: numberOrNull, + variance: numberOrNull, + std_deviation: numberOrNull, + std_deviation_bounds: t.type({ + upper: numberOrNull, + lower: numberOrNull + }) + }) + }, + top_hits: { + in: t.partial({ + from: t.number, + size: t.number, + sort, + _source: t.union([t.string, t.array(t.string)]) + }), + out: t.type({ + hits: t.type({ + total: t.type({ + value: t.number, + relation: t.keyof({ + eq: null, + gte: null + }) + }), + max_score: numberOrNull, + hits: t.array( + t.type({ + _source: t.undefined + }) + ) + }) + }) + }, + percentiles: { + in: t.union([ + t.type({ + field: t.string + }), + t.partial({ + percents: t.array(t.number) + }) + ]), + out: t.type({ + values: t.dictionary(t.string, t.number) + }) + } +}; + +export type Aggregations = typeof aggregationRts; diff --git a/x-pack/legacy/plugins/apm/server/lib/errors/get_error_groups.ts b/x-pack/legacy/plugins/apm/server/lib/errors/get_error_groups.ts index 33a93fa986db3..e5c5046ac156c 100644 --- a/x-pack/legacy/plugins/apm/server/lib/errors/get_error_groups.ts +++ b/x-pack/legacy/plugins/apm/server/lib/errors/get_error_groups.ts @@ -58,11 +58,13 @@ export async function getErrorGroups({ terms: { field: ERROR_GROUP_ID, size: 500, - order: sortByLatestOccurrence + order: (sortByLatestOccurrence ? { max_timestamp: sortDirection } - : { _count: sortDirection } + : { _count: sortDirection }) as ({ + [key: string]: 'asc' | 'desc'; + }) }, aggs: { sample: { @@ -75,7 +77,7 @@ export async function getErrorGroups({ ERROR_GROUP_ID, '@timestamp' ], - sort: [{ '@timestamp': 'desc' }], + sort: [{ '@timestamp': 'desc' as const }], size: 1 } }, @@ -115,7 +117,8 @@ export async function getErrorGroups({ // this is an exception rather than the rule so the ES type does not account for this. const hits = (idx(resp, _ => _.aggregations.error_groups.buckets) || []).map( bucket => { - const source = bucket.sample.hits.hits[0]._source as SampleError; + const source = (bucket.sample.hits.hits[0] + ._source as unknown) as SampleError; const message = idx(source, _ => _.error.log.message) || idx(source, _ => _.error.exception[0].message); diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts index 896c558121992..4a2649f636334 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts @@ -10,10 +10,12 @@ import { IndexDocumentParams, IndicesDeleteParams, IndicesCreateParams, - AggregationSearchResponse + APMSearchParams, + APMSearchResponse } from 'elasticsearch'; import { Legacy } from 'kibana'; import { cloneDeep, has, isString, set } from 'lodash'; +import * as t from 'io-ts'; import { OBSERVER_VERSION_MAJOR } from '../../../common/elasticsearch_fieldnames'; import { StringMap } from '../../../typings/common'; @@ -85,15 +87,33 @@ interface APMOptions { includeLegacyData: boolean; } +// function search>( +// params: T +// ): APMSearchResponse { +// return (null as unknown) as APMSearchResponse; +// } + +// const response = search({ +// body: { +// aggs: { +// foo: { +// terms: { +// field: 'bar' +// } +// } +// } +// } +// }); + export function getESClient(req: Legacy.Request) { const cluster = req.server.plugins.elasticsearch.getCluster('data'); const query = req.query as StringMap; return { - search: async ( + search: async ( params: U, apmOptions?: APMOptions - ): Promise> => { + ) => { const nextParams = await getParamsForSearchRequest( req, params, @@ -111,8 +131,10 @@ export function getESClient(req: Legacy.Request) { console.log(JSON.stringify(nextParams.body, null, 4)); } + type HitType = t.Type; + return cluster.callWithRequest(req, 'search', nextParams) as Promise< - AggregationSearchResponse + APMSearchResponse >; }, index: (params: IndexDocumentParams) => { diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts index bd45cec316dfc..83130fc15243b 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts @@ -30,7 +30,7 @@ describe('setupRequest', () => { it('should call callWithRequest with default args', async () => { const { mockRequest, callWithRequestSpy } = getMockRequest(); const { client } = await setupRequest(mockRequest); - await client.search({ index: 'apm-*', body: { foo: 'bar' } }); + await client.search({ index: 'apm-*', body: { foo: 'bar' } } as any); expect(callWithRequestSpy).toHaveBeenCalledWith(mockRequest, 'search', { index: 'apm-*', body: { @@ -71,7 +71,7 @@ describe('setupRequest', () => { it('should add `observer.version_major` filter if none exists', async () => { const { mockRequest, callWithRequestSpy } = getMockRequest(); const { client } = await setupRequest(mockRequest); - await client.search({ index: 'apm-*' }); + await client.search({ index: 'apm-*' } as any); const params = callWithRequestSpy.mock.calls[0][2]; expect(params.body).toEqual({ query: { @@ -128,7 +128,7 @@ describe('setupRequest', () => { // mock includeFrozen to return false mockRequest.getUiSettingsService = () => ({ get: async () => false }); const { client } = await setupRequest(mockRequest); - await client.search({}); + await client.search({} as any); const params = callWithRequestSpy.mock.calls[0][2]; expect(params.ignore_throttled).toBe(true); }); @@ -139,7 +139,7 @@ describe('setupRequest', () => { // mock includeFrozen to return true mockRequest.getUiSettingsService = () => ({ get: async () => true }); const { client } = await setupRequest(mockRequest); - await client.search({}); + await client.search({} as any); const params = callWithRequestSpy.mock.calls[0][2]; expect(params.ignore_throttled).toBe(false); }); diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts index e372a62a7ce05..96a28ffa90f55 100644 --- a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts @@ -40,7 +40,7 @@ const chartBase: ChartBase = { }; const percentUsedScript = { - lang: 'expression', + lang: 'expression' as const, source: `1 - doc['${METRIC_SYSTEM_FREE_MEMORY}'] / doc['${METRIC_SYSTEM_TOTAL_MEMORY}']` }; diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts index eefa1e0ef201a..ac4bf11d47f71 100644 --- a/x-pack/legacy/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts +++ b/x-pack/legacy/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts @@ -11,18 +11,9 @@ import { import { Setup } from '../helpers/setup_request'; import { getMetricsDateHistogramParams } from '../helpers/metrics'; import { rangeFilter } from '../helpers/range_filter'; -import { ChartBase } from './types'; +import { ChartBase, SearchParams } from './types'; import { transformDataToMetricsChart } from './transform_metrics_chart'; -interface Aggs { - [key: string]: { - min?: any; - max?: any; - sum?: any; - avg?: any; - }; -} - interface Filter { exists?: { field: string; @@ -32,7 +23,7 @@ interface Filter { }; } -export async function fetchAndTransformMetrics({ +export async function fetchAndTransformMetrics({ setup, serviceName, chartBase, @@ -42,12 +33,12 @@ export async function fetchAndTransformMetrics({ setup: Setup; serviceName: string; chartBase: ChartBase; - aggs: T; + aggs: SearchParams['body']['aggs']['timeseriesData']['aggs']; additionalFilters?: Filter[]; }) { const { start, end, uiFiltersES, client, config } = setup; - const params = { + const params: SearchParams = { index: config.get('apm_oss.metricsIndices'), body: { size: 0, diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/transform_metrics_chart.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/transform_metrics_chart.ts index 1acac008c8bf8..3ba3d60db6e8c 100644 --- a/x-pack/legacy/plugins/apm/server/lib/metrics/transform_metrics_chart.ts +++ b/x-pack/legacy/plugins/apm/server/lib/metrics/transform_metrics_chart.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import theme from '@elastic/eui/dist/eui_theme_light.json'; -import { AggregationSearchResponse, AggregatedValue } from 'elasticsearch'; -import { ChartBase } from './types'; +import { APMSearchResponse } from 'elasticsearch'; +import { ChartBase, SearchParams } from './types'; const colors = [ theme.euiColorVis0, @@ -21,31 +21,8 @@ export type GenericMetricsChart = ReturnType< typeof transformDataToMetricsChart >; -interface AggregatedParams { - body: { - aggs: { - timeseriesData: { - date_histogram: any; - aggs: { - min?: any; - max?: any; - sum?: any; - avg?: any; - }; - }; - } & { - [key: string]: { - min?: any; - max?: any; - sum?: any; - avg?: any; - }; - }; - }; -} - -export function transformDataToMetricsChart( - result: AggregationSearchResponse, +export function transformDataToMetricsChart( + result: APMSearchResponse, chartBase: ChartBase ) { const { aggregations, hits } = result; @@ -56,8 +33,8 @@ export function transformDataToMetricsChart( key: chartBase.key, yUnit: chartBase.yUnit, totalHits: hits.total, - series: Object.keys(chartBase.series).map((seriesKey, i) => { - const agg = aggregations[seriesKey]; + series: (Object.keys(chartBase.series) as T[]).map((seriesKey, i) => { + const agg = aggregations[seriesKey] as { value: number | null }; return { title: chartBase.series[seriesKey].title, @@ -66,7 +43,7 @@ export function transformDataToMetricsChart( color: chartBase.series[seriesKey].color || colors[i], overallValue: agg.value, data: timeseriesData.buckets.map(bucket => { - const { value } = bucket[seriesKey] as AggregatedValue; + const { value } = bucket[seriesKey] as { value: number | null }; const y = value === null || isNaN(value) ? null : value; return { x: bucket.key, diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/types.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/types.ts index 622b37162d9dc..71806f5fcb318 100644 --- a/x-pack/legacy/plugins/apm/server/lib/metrics/types.ts +++ b/x-pack/legacy/plugins/apm/server/lib/metrics/types.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { APMAggregationInputTypes, APMSearchParams } from 'elasticsearch'; import { ChartType, YUnit } from '../../../typings/timeseries'; export interface ChartBase { @@ -17,3 +18,22 @@ export interface ChartBase { }; }; } + +interface AllowedAggregationTypes { + min?: APMAggregationInputTypes['min']; + max?: APMAggregationInputTypes['max']; + avg?: APMAggregationInputTypes['avg']; + sum?: APMAggregationInputTypes['sum']; + date_histogram?: APMAggregationInputTypes['date_histogram']; +} + +export type SearchParams = Omit & { + body: Omit & { + aggs: { + timeseriesData: { + date_histogram: APMAggregationInputTypes['date_histogram']; + aggs: Record; + }; + } & Record; + }; +}; diff --git a/x-pack/legacy/plugins/apm/server/lib/services/get_services/get_agent_status.ts b/x-pack/legacy/plugins/apm/server/lib/services/get_services/get_agent_status.ts index 68e5afd995203..634c5607441ff 100644 --- a/x-pack/legacy/plugins/apm/server/lib/services/get_services/get_agent_status.ts +++ b/x-pack/legacy/plugins/apm/server/lib/services/get_services/get_agent_status.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SearchParams } from 'elasticsearch'; import { PROCESSOR_EVENT } from '../../../../common/elasticsearch_fieldnames'; import { Setup } from '../../helpers/setup_request'; @@ -12,13 +11,13 @@ import { Setup } from '../../helpers/setup_request'; export async function getAgentStatus(setup: Setup) { const { client, config } = setup; - const params: SearchParams = { + const params = { terminateAfter: 1, index: [ - config.get('apm_oss.errorIndices'), - config.get('apm_oss.metricsIndices'), - config.get('apm_oss.sourcemapIndices'), - config.get('apm_oss.transactionIndices') + config.get('apm_oss.errorIndices'), + config.get('apm_oss.metricsIndices'), + config.get('apm_oss.sourcemapIndices'), + config.get('apm_oss.transactionIndices') ], body: { size: 0, diff --git a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.ts b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.ts index 3b32776739c81..a6c671c620017 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.ts @@ -64,7 +64,7 @@ export function transactionGroupsFetcher(options: Options, setup: Setup) { transactions: { terms: { field: TRANSACTION_NAME, - order: { sum: 'desc' }, + order: { sum: 'desc' as const }, size: config.get('xpack.apm.ui.transactionGroupBucketSize') }, aggs: { @@ -74,7 +74,8 @@ export function transactionGroupsFetcher(options: Options, setup: Setup) { sort: [ { _score: 'desc' }, // sort by _score to ensure that buckets with sampled:true ends up on top { '@timestamp': { order: 'desc' } } - ] + // TS complains about missing an index signature + ] as Array<{ [key: string]: any }> } }, avg: { avg: { field: TRANSACTION_DURATION } }, diff --git a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/transform.ts b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/transform.ts index 62d212d07d2a7..512f94705c257 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/transform.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/transform.ts @@ -34,7 +34,7 @@ function getTransactionGroup( const averageResponseTime = bucket.avg.value; const transactionsPerMinute = bucket.doc_count / minutes; const impact = bucket.sum.value; - const sample = bucket.sample.hits.hits[0]._source as Transaction; + const sample = (bucket.sample.hits.hits[0]._source as unknown) as Transaction; return { name: bucket.key, diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.ts index 3f09aa55e3f0e..e269ff1e73c77 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.ts @@ -53,7 +53,7 @@ export async function getTransactionBreakdown({ field: SPAN_TYPE, size: 20, order: { - _count: 'desc' + _count: 'desc' as const } }, aggs: { @@ -63,7 +63,7 @@ export async function getTransactionBreakdown({ missing: '', size: 20, order: { - _count: 'desc' + _count: 'desc' as const } }, aggs: { diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/calculate_bucket_size.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/calculate_bucket_size.ts index 20f69a0bd4d8c..106b668af79e9 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/calculate_bucket_size.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/calculate_bucket_size.ts @@ -59,7 +59,7 @@ export async function calculateBucketSize( const minBucketSize: number = config.get('xpack.apm.minimumBucketSize'); const bucketTargetCount: number = config.get('xpack.apm.bucketTargetCount'); - const max = resp.aggregations.stats.max; + const max = resp.aggregations.stats.max || 0; const bucketSize = Math.floor(max / bucketTargetCount); return bucketSize > minBucketSize ? bucketSize : minBucketSize; } diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts index d265aa5173d2f..eb38af47c4314 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts @@ -17,7 +17,7 @@ import { import { rangeFilter } from '../../../helpers/range_filter'; import { Setup } from '../../../helpers/setup_request'; -export function bucketFetcher( +export async function bucketFetcher( serviceName: string, transactionName: string, transactionType: string, @@ -74,5 +74,7 @@ export function bucketFetcher( } }; - return client.search(params); + const response = await client.search(params); + + return response; } diff --git a/x-pack/legacy/plugins/apm/typings/elasticsearch.ts b/x-pack/legacy/plugins/apm/typings/elasticsearch.ts index d8a0ff18ba66b..fee552b51a8de 100644 --- a/x-pack/legacy/plugins/apm/typings/elasticsearch.ts +++ b/x-pack/legacy/plugins/apm/typings/elasticsearch.ts @@ -4,7 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { StringMap, IndexAsString } from './common'; +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; +import { StringMap } from './common'; +import { Aggregations } from '../public/es/aggregations'; export interface BoolQuery { must_not: Array>; @@ -15,107 +18,32 @@ export interface BoolQuery { declare module 'elasticsearch' { // extending SearchResponse to be able to have typed aggregations - type AggregationType = - | 'date_histogram' - | 'histogram' - | 'terms' - | 'avg' - | 'top_hits' - | 'max' - | 'min' - | 'percentiles' - | 'sum' - | 'extended_stats'; + // type AggregationType = + // | 'percentiles' - type AggOptions = AggregationOptionMap & { - [key: string]: any; - }; - - // eslint-disable-next-line @typescript-eslint/prefer-interface - export type AggregationOptionMap = { - aggs?: { - [aggregationName: string]: { - [T in AggregationType]?: AggOptions & AggregationOptionMap; - }; + export type APMSearchParams = Omit & { + body: { + query?: any; + size?: number; + aggs?: AggsInput; }; }; - // eslint-disable-next-line @typescript-eslint/prefer-interface - type BucketAggregation = { - buckets: Array< - { - key: KeyType; - key_as_string: string; - doc_count: number; - } & (SubAggregationMap extends { aggs: any } - ? AggregationResultMap - : {}) - >; - }; - - interface AggregatedValue { - value: number | null; - } - - type AggregationResultMap = IndexAsString< - { - [AggregationName in keyof AggregationOption]: { - avg: AggregatedValue; - max: AggregatedValue; - min: AggregatedValue; - sum: AggregatedValue; - terms: BucketAggregation; - date_histogram: BucketAggregation< - AggregationOption[AggregationName], - number - >; - histogram: BucketAggregation< - AggregationOption[AggregationName], - number - >; - top_hits: { - hits: { - total: number; - max_score: number | null; - hits: Array<{ - _source: AggregationOption[AggregationName] extends { - Mapping: any; - } - ? AggregationOption[AggregationName]['Mapping'] - : never; - }>; - }; - }; - percentiles: { - values: { - [key: string]: number; - }; - }; - extended_stats: { - count: number; - min: number; - max: number; - avg: number; - sum: number; - sum_of_squares: number; - variance: number; - std_deviation: number; - std_deviation_bounds: { - upper: number; - lower: number; - }; - }; - }[AggregationType & keyof AggregationOption[AggregationName]]; - } - >; - - export type AggregationSearchResponse = Pick< - SearchResponse, - Exclude, 'aggregations'> - > & - (SearchParams extends { body: Required } + export type APMSearchResponse< + T extends APMSearchParams, + U extends t.Type = t.NullType + > = (Omit< + SearchResponse>>, + 'aggregations' + >) & + ({ + hits: { + hits: Array<{ _source: GetRight> }>; + }; + }) & + (T extends { body: { aggs: AggsInput } } ? { - aggregations: AggregationResultMap; + aggregations: AggregationResponseMap; } : {}); @@ -124,4 +52,57 @@ declare module 'elasticsearch' { [key: string]: string | string[] | number | StringMap | ESFilter[]; }; } + + export type APMAggregationInput = AggsInput; + export type APMAggregationInputTypes = AggregationInputTypes; } + +interface SubAggregationMergeStrategies { + bucket: U extends { buckets: Array } + ? (Omit & { + buckets: Array>; + }) + : never; +} + +type AggregationInputTypes = { + [key in keyof Aggregations]: t.TypeOf; +}; + +type AggsInput = Partial< + Record & { aggs?: AggsInput }> +>; + +type GetRight = T extends Either ? R : never; + +type AggregationResponseValue = GetRight< + ReturnType +>; + +type AggregationResponse< + T extends keyof Aggregations, + U extends { aggs?: AggsInput } | undefined +> = U extends { aggs: AggsInput } + ? (Aggregations[T] extends { + aggs: keyof SubAggregationMergeStrategies< + U['aggs'], + AggregationResponseValue + >; + } + ? SubAggregationMergeStrategies< + U['aggs'], + AggregationResponseValue + >[Aggregations[T]['aggs']] + : never) + : AggregationResponseValue; + +type AggregationResponseMap< + T extends AggsInput | undefined +> = T extends AggsInput + ? { + [aggregationName in keyof T]: AggregationResponse< + keyof Aggregations & keyof T[aggregationName], + T[aggregationName] + >; + } + : {};