diff --git a/modules/server/src/network/aggregations/AggregationAccumulator.ts b/modules/server/src/network/aggregations/AggregationAccumulator.ts index 96faf10fd..31f0c1588 100644 --- a/modules/server/src/network/aggregations/AggregationAccumulator.ts +++ b/modules/server/src/network/aggregations/AggregationAccumulator.ts @@ -1,41 +1,33 @@ import { SUPPORTED_AGGREGATIONS } from '../common'; import { Aggregations, Bucket, NumericAggregations } from '../types/aggregations'; import { AllAggregations } from '../types/types'; -import { RequestedFieldsMap } from '../util'; type ResolveAggregationInput = { aggregationsMap: AllAggregations; - requestedAggregationFields: string[]; accumulator: AllAggregations; }; +type AggregationsTuple = [Aggregations, Aggregations]; +type NumericAggregationsTuple = [NumericAggregations, NumericAggregations]; + /** * Resolves returned aggregations from network queries into single accumulated aggregation * * @param */ -const resolveAggregations = ({ - aggregationsMap, - requestedAggregationFields, - accumulator, -}: ResolveAggregationInput) => { - requestedAggregationFields.forEach((fieldName) => { +const resolveAggregations = ({ aggregationsMap, accumulator }: ResolveAggregationInput) => { + Object.keys(aggregationsMap).forEach((fieldName) => { const aggregation = aggregationsMap[fieldName]; const aggregationType = aggregation?.__typename || ''; - const accumulatedFieldAggregation = accumulator[fieldName]; - if (aggregation && accumulatedFieldAggregation) { - const resolvedAggregation = resolveToNetworkAggregation(aggregationType, [ - aggregation, - accumulatedFieldAggregation, - ]); + const accumulatedFieldAggregation = accumulator[fieldName]; - // mutation - update a single aggregations field in the accumulator - accumulator[fieldName] = resolvedAggregation; - } + // mutation - update a single aggregations field in the accumulator + // if first aggregation, nothing to resolve yet + accumulator[fieldName] = !accumulatedFieldAggregation + ? aggregation + : resolveToNetworkAggregation(aggregationType, [aggregation, accumulatedFieldAggregation]); }); - - return accumulator; }; /** @@ -46,12 +38,12 @@ const resolveAggregations = ({ */ const resolveToNetworkAggregation = ( type: string, - aggregations: Aggregations[] | NumericAggregations[], + aggregations: AggregationsTuple | NumericAggregationsTuple, ): Aggregations | NumericAggregations => { if (type === SUPPORTED_AGGREGATIONS.Aggregations) { - return resolveAggregation(aggregations as Aggregations[]); + return resolveAggregation(aggregations as AggregationsTuple); } else if (type === SUPPORTED_AGGREGATIONS.NumericAggregations) { - return resolveNumericAggregation(aggregations as NumericAggregations); + return resolveNumericAggregation(aggregations as NumericAggregationsTuple); } else { // no types match throw Error('No matching aggregation type'); @@ -153,7 +145,7 @@ const updateComputedBuckets = (bucket: Bucket, computedBuckets: Bucket[]) => { * } * ``` */ -export const resolveAggregation = (aggregations: Aggregations[]): Aggregations => { +export const resolveAggregation = (aggregations: AggregationsTuple): Aggregations => { const resolvedAggregation = aggregations.reduce((resolvedAggregation, agg) => { const computedBuckets = resolvedAggregation.buckets; agg.buckets.forEach((bucket) => updateComputedBuckets(bucket, computedBuckets)); @@ -163,34 +155,34 @@ export const resolveAggregation = (aggregations: Aggregations[]): Aggregations = return resolvedAggregation; }; -const resolveNumericAggregation = (aggregations: NumericAggregations) => { - // TODO: implement - throw Error('Not implemented'); -}; +const resolveNumericAggregation = (aggregations: NumericAggregationsTuple): NumericAggregations => { + return aggregations.reduce((resolvedAggregation, agg) => { + // max + if (agg.stats.max > resolvedAggregation.stats.max) { + resolvedAggregation.stats.max = agg.stats.max; + } + // min + if (agg.stats.min < resolvedAggregation.stats.min) { + resolvedAggregation.stats.min = agg.stats.min; + } + // count + resolvedAggregation.stats.count += agg.stats.count; + // sum + resolvedAggregation.stats.sum += agg.stats.sum; + // avg + resolvedAggregation.stats.avg = resolvedAggregation.stats.sum / resolvedAggregation.stats.count; -const emptyAggregation: Aggregations = { bucket_count: 0, buckets: [] }; + return resolvedAggregation; + }); +}; export class AggregationAccumulator { - requestedFields: string[]; - totalAgg: AllAggregations; - - constructor(requestedFieldsMap: RequestedFieldsMap) { - const requestedFields = Object.keys(requestedFieldsMap); - this.requestedFields = requestedFields; - /* - * seed accumulator with the requested field keys - * this will make it easier to add to using key lookup instead of Array.find - */ - this.totalAgg = requestedFields.reduce((accumulator, field) => { - return { ...accumulator, [field]: emptyAggregation }; - }, {}); - } + totalAgg: AllAggregations = {}; resolve(data: AllAggregations) { resolveAggregations({ accumulator: this.totalAgg, - aggregationsMap: data, - requestedAggregationFields: this.requestedFields, + aggregationsMap: structuredClone(data), }); } diff --git a/modules/server/src/network/aggregations/tests/aggregation.test.ts b/modules/server/src/network/aggregations/tests/aggregation.test.ts index 8d57d30b2..d076c94de 100644 --- a/modules/server/src/network/aggregations/tests/aggregation.test.ts +++ b/modules/server/src/network/aggregations/tests/aggregation.test.ts @@ -1,29 +1,24 @@ import { AggregationAccumulator } from '../AggregationAccumulator'; import { aggregation as fixture } from './fixture'; -describe('Network aggregation resolution', () => { - it('should compute requested keys from info map constructor param', () => { - const requestedFields = { donors_age: {}, donors_gender: {} }; - const totalAggs = new AggregationAccumulator(requestedFields); - expect(totalAggs.requestedFields.sort()).toEqual(Object.keys(requestedFields).sort()); - }); +// expected values +const maleCount = 835; +const femaleCount = 812; +const unknownCount = 2; +const bucketCount = 3; + +const expectedStats = { max: 100, min: 1, count: 15, avg: 56, sum: 840 }; +describe('Network aggregation resolution', () => { describe('resolves multiple aggregations into a single aggregation:', () => { it('should resolve multiple Aggregations type fields', () => { - const requestedFields = { donors_gender: {} }; - const totalAggs = new AggregationAccumulator(requestedFields); + const totalAggs = new AggregationAccumulator(); const aggregationsToResolve = [ { donors_gender: fixture.inputA }, { donors_gender: fixture.inputC }, ]; aggregationsToResolve.forEach((agg) => totalAggs.resolve(agg)); - // expected values - const maleCount = 835; - const femaleCount = 812; - const unknownCount = 2; - const bucketCount = 3; - const result = totalAggs.result(); const aggregation = result['donors_gender']; @@ -38,5 +33,44 @@ describe('Network aggregation resolution', () => { unknownCount, ); }); + it('should resolve multiple NumericAggregations type fields', () => { + const totalAggs = new AggregationAccumulator(); + const aggregationsToResolve = [ + { donors_weight: fixture.inputD }, + { donors_weight: fixture.inputE }, + ]; + aggregationsToResolve.forEach((agg) => totalAggs.resolve(agg)); + + const result = totalAggs.result(); + const aggregation = result['donors_weight']; + expect(aggregation.stats).toEqual(expectedStats); + }); + it('should resolve a combination of Aggregations and NumericAggregations type fields', () => { + const aggregationsToResolve = [ + { donors_gender: fixture.inputA, donors_weight: fixture.inputE }, + { donors_gender: fixture.inputC, donors_weight: fixture.inputD }, + ]; + + const totalAggs = new AggregationAccumulator(); + aggregationsToResolve.forEach((agg) => { + totalAggs.resolve(agg); + }); + + const result = totalAggs.result(); + const donorsGenderAgg = result['donors_gender']; + const donorsWeightAgg = result['donors_weight']; + + expect(donorsGenderAgg.bucket_count).toEqual(bucketCount); + expect(donorsGenderAgg.buckets.find((bucket) => bucket.key === 'Male')?.doc_count).toEqual( + maleCount, + ); + expect(donorsGenderAgg.buckets.find((bucket) => bucket.key === 'Female')?.doc_count).toEqual( + femaleCount, + ); + expect(donorsGenderAgg.buckets.find((bucket) => bucket.key === 'Unknown')?.doc_count).toEqual( + unknownCount, + ); + expect(donorsWeightAgg.stats).toEqual(expectedStats); + }); }); }); diff --git a/modules/server/src/network/aggregations/tests/fixture.ts b/modules/server/src/network/aggregations/tests/fixture.ts index 8d4608158..56c5a6c02 100644 --- a/modules/server/src/network/aggregations/tests/fixture.ts +++ b/modules/server/src/network/aggregations/tests/fixture.ts @@ -1,4 +1,4 @@ -import { Aggregations } from '@/network/types/aggregations'; +import { Aggregations, NumericAggregations } from '@/network/types/aggregations'; const inputA: Aggregations = { __typename: 'Aggregations', @@ -49,8 +49,21 @@ const inputC: Aggregations = { ], }; +// NumericAggregations +const inputD: NumericAggregations = { + __typename: 'NumericAggregations', + stats: { max: 100, min: 12, count: 10, avg: 42, sum: 420 }, +}; + +const inputE: NumericAggregations = { + __typename: 'NumericAggregations', + stats: { max: 75, min: 1, count: 5, avg: 84, sum: 420 }, +}; + export const aggregation = { inputA, inputB, inputC, + inputD, + inputE, }; diff --git a/modules/server/src/network/types/aggregations.ts b/modules/server/src/network/types/aggregations.ts index fd47ab216..5781c7d09 100644 --- a/modules/server/src/network/types/aggregations.ts +++ b/modules/server/src/network/types/aggregations.ts @@ -8,9 +8,17 @@ export type Bucket = { }; export type Aggregations = { - __typename?: string; + __typename: 'Aggregations'; bucket_count: number; buckets: Bucket[]; }; -export type NumericAggregations = { __typename?: string }; +type Stats = { + max: number; + min: number; + count: number; + avg: number; + sum: number; +}; + +export type NumericAggregations = { __typename: 'NumericAggregations'; stats: Stats };