import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey' ; import { ObjectType } from '@kbn/config-schema' ; import { isPlainObject } from 'lodash' ; import { IndexMapping } from '../../../mappings' ; import { isObjectTypeAttribute , rewriteObjectTypeAttribute , isRootLevelAttribute , rewriteRootLevelAttribute , } from './validation_utils' ; import { aggregationSchemas } from './aggs_types' ; const aggregationKeys = [ 'aggs' , 'aggregations' ] ; interface ValidationContext { allowedTypes : string [ ] ; indexMapping : IndexMapping ; currentPath : string [ ] ; } /**
 * Validate an aggregation structure against the declared mappings and
 * aggregation schemas, and rewrite the attribute fields using the KQL-like syntax
 * - `{type}.attributes.{attribute}` to `{type}.{attribute}`
 * - `{type}.{rootField}` to `{rootField}`
 *
 * throws on the first validation error if any is encountered.
 */ export const validateAndConvertAggregations = ( allowedTypes , aggs , indexMapping ) : Record < string , estypes . AggregationsAggregationContainer > => { return validateAggregations ( aggs , { allowedTypes , indexMapping , currentPath : [ ] , } ) ; } ; /**
 * Validate a record of aggregation containers,
 * Which can either be the root level aggregations (`SearchRequest.body.aggs`)
 * Or a nested record of aggregation (`SearchRequest.body.aggs.myAggregation.aggs`)
 */ const validateAggregations = ( aggregations , context ) => { return Object . entries ( aggregations ) . reduce < Record < string , estypes . AggregationsAggregationContainer > > ( ( memo , [ aggrName , aggrContainer ] ) => { memo [ aggrName ] = validateAggregation ( aggrContainer , childContext ( context , aggrName ) ) ; return memo ; } , { } ) ; } ; /**
 * Validate an aggregation container, e.g an entry of `SearchRequest.body.aggs`, or
 * from a nested aggregation record, including its potential nested aggregations.
 */ const validateAggregation = ( aggregation , context ) => { const container = validateAggregationContainer ( aggregation , context ) ; if ( aggregation . aggregations ) { container . aggregations = validateAggregations ( aggregation . aggregations , childContext ( context , 'aggregations' ) ) ; } if ( aggregation . aggs ) { container . aggs = validateAggregations ( aggregation . aggs , childContext ( context , 'aggs' ) ) ; } return container ; } ; /**
 * Validates root-level aggregation of given aggregation container
 * (ignoring its nested aggregations)
 */ const validateAggregationContainer = ( container , context ) => { return Object . entries ( container ) . reduce < estypes . AggregationsAggregationContainer > ( ( memo , [ aggName , aggregation ] ) => { if ( aggregationKeys . includes ( aggName ) ) { return memo ; } return { ... memo , [ aggName ] : validateAggregationType ( aggName , aggregation , childContext ( context , aggName ) ) , } ; } , { } ) ; } ; const validateAggregationType = ( aggregationType , aggregation , context ) => { const aggregationSchema = aggregationSchemas [ aggregationType ] ; if ( ! aggregationSchema ) { throw new Error ( `[${ context . currentPath . join ( '.' ) }] ${ aggregationType } aggregation is not valid (or not registered yet)` ) ; } validateAggregationStructure ( aggregationSchema , aggregation , context ) ; return validateAndRewriteFieldAttributes ( aggregation , context ) ; } ; /**
 * Validate an aggregation structure against its declared schema.
 */ const validateAggregationStructure = ( schema , aggObject , context ) => { return schema . validate ( aggObject , { } , context . currentPath . join ( '.' ) ) ; } ; example const attributeFields = [ 'field' , 'path' ] ; example const attributeMaps = [ 'term' ] ; const validateAndRewriteFieldAttributes = ( aggregation , context ) => { return recursiveRewrite ( aggregation , context , [ ] ) ; } ; const recursiveRewrite = ( currentLevel , context , parents ) : Record < string , any > => { return Object . entries ( currentLevel ) . reduce ( ( memo , [ key , value ] ) => { const rewriteKey = isAttributeKey ( parents ) ; const rewriteValue = isAttributeValue ( key , value ) ; const nestedContext = childContext ( context , key ) ; const newKey = rewriteKey ? validateAndRewriteAttributePath ( key , nestedContext ) : key ; const newValue = rewriteValue ? validateAndRewriteAttributePath ( value , nestedContext ) : isPlainObject ( value ) ? recursiveRewrite ( value , nestedContext , [ ... parents , key ] ) : value ; return { ... memo , [ newKey ] : newValue , } ; } , { } ) ; } ; const childContext = ( context , path ) : ValidationContext => { return { ... context , currentPath : [ ... context . currentPath , path ] , } ; } ; const lastParent = ( parents ) => { if ( parents . length ) { return parents [ parents . length - 1 ] ; } return undefined ; } ; const isAttributeKey = ( parents ) => { const last = lastParent ( parents ) ; if ( last ) { return attributeMaps . includes ( last ) ; } return false ; } ; const isAttributeValue = ( fieldName , fieldValue ) : boolean => { return attributeFields . includes ( fieldName ) && typeof fieldValue === 'string' ; } ; const validateAndRewriteAttributePath = ( attributePath , { allowedTypes , indexMapping , currentPath } ) => { if ( isRootLevelAttribute ( attributePath , indexMapping , allowedTypes ) ) { return rewriteRootLevelAttribute ( attributePath ) ; } if ( isObjectTypeAttribute ( attributePath , indexMapping , allowedTypes ) ) { return rewriteObjectTypeAttribute ( attributePath ) ; } throw new Error ( `[${ currentPath . join ( '.' ) }] Invalid attribute path: ${ attributePath }` ) ; } ;