151 lines
4.3 KiB
TypeScript
151 lines
4.3 KiB
TypeScript
import { Hyperview, HyperviewOne } from "../hyperview";
|
|
import { Lossy } from '../view';
|
|
import { DomainEntityID, PropertyID, ViewMany } from "../../core/types";
|
|
import { valueFromDelta } from "../hyperview";
|
|
import { EntityRecord, EntityRecordMany } from "@src/core/entity";
|
|
|
|
export type AggregationType = 'min' | 'max' | 'sum' | 'average' | 'count';
|
|
|
|
export type AggregationConfig = {
|
|
[propertyId: PropertyID]: AggregationType;
|
|
};
|
|
|
|
type AggregatedProperty = {
|
|
values: number[];
|
|
type: AggregationType;
|
|
result?: number;
|
|
};
|
|
|
|
type AggregatedProperties = {
|
|
[key: PropertyID]: AggregatedProperty;
|
|
};
|
|
|
|
export type AggregatedViewOne = {
|
|
id: DomainEntityID;
|
|
properties: AggregatedProperties;
|
|
};
|
|
|
|
export type AggregatedViewMany = ViewMany<AggregatedViewOne>;
|
|
|
|
type Accumulator = AggregatedViewMany;
|
|
type Result = EntityRecordMany;
|
|
|
|
function aggregateValues(values: number[], type: AggregationType): number {
|
|
if (values.length === 0) return 0;
|
|
|
|
switch (type) {
|
|
case 'min':
|
|
return Math.min(...values);
|
|
case 'max':
|
|
return Math.max(...values);
|
|
case 'sum':
|
|
return values.reduce((sum, val) => sum + val, 0);
|
|
case 'average':
|
|
return values.reduce((sum, val) => sum + val, 0) / values.length;
|
|
case 'count':
|
|
// For count, we want to count all values, including duplicates
|
|
// So we use the length of the values array directly
|
|
return values.length;
|
|
default:
|
|
throw new Error(`Unknown aggregation type: ${type}`);
|
|
}
|
|
}
|
|
|
|
export class AggregationResolver extends Lossy<Accumulator, Result> {
|
|
constructor(
|
|
hyperview: Hyperview,
|
|
private config: AggregationConfig
|
|
) {
|
|
super(hyperview);
|
|
}
|
|
|
|
reducer(acc: Accumulator, cur: HyperviewOne): Accumulator {
|
|
if (!acc[cur.id]) {
|
|
acc[cur.id] = { id: cur.id, properties: {} };
|
|
}
|
|
|
|
for (const [propertyId, deltas] of Object.entries(cur.propertyDeltas)) {
|
|
const aggregationType = this.config[propertyId];
|
|
if (!aggregationType) continue;
|
|
|
|
if (!acc[cur.id].properties[propertyId]) {
|
|
acc[cur.id].properties[propertyId] = {
|
|
values: [],
|
|
type: aggregationType
|
|
};
|
|
}
|
|
|
|
// Extract numeric values from all deltas for this property
|
|
for (const delta of deltas) {
|
|
const value = valueFromDelta(propertyId, delta);
|
|
|
|
if (typeof value === 'number') {
|
|
acc[cur.id].properties[propertyId].values.push(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
return acc;
|
|
}
|
|
|
|
resolver(cur: Accumulator): Result {
|
|
const res: Result = {};
|
|
|
|
for (const [id, entity] of Object.entries(cur)) {
|
|
const entityResult: EntityRecord = { id, properties: {} };
|
|
|
|
for (const [propertyId, aggregatedProp] of Object.entries(entity.properties)) {
|
|
const result = aggregateValues(aggregatedProp.values, aggregatedProp.type);
|
|
entityResult.properties[propertyId] = result;
|
|
}
|
|
|
|
// Only include entities that have at least one aggregated property
|
|
if (Object.keys(entityResult.properties).length > 0) {
|
|
res[id] = entityResult;
|
|
}
|
|
}
|
|
|
|
return res;
|
|
}
|
|
}
|
|
|
|
// Convenience classes for common aggregation types
|
|
export class MinResolver extends AggregationResolver {
|
|
constructor(hyperview: Hyperview, properties: PropertyID[]) {
|
|
const config: AggregationConfig = {};
|
|
properties.forEach(prop => config[prop] = 'min');
|
|
super(hyperview, config);
|
|
}
|
|
}
|
|
|
|
export class MaxResolver extends AggregationResolver {
|
|
constructor(hyperview: Hyperview, properties: PropertyID[]) {
|
|
const config: AggregationConfig = {};
|
|
properties.forEach(prop => config[prop] = 'max');
|
|
super(hyperview, config);
|
|
}
|
|
}
|
|
|
|
export class SumResolver extends AggregationResolver {
|
|
constructor(hyperview: Hyperview, properties: PropertyID[]) {
|
|
const config: AggregationConfig = {};
|
|
properties.forEach(prop => config[prop] = 'sum');
|
|
super(hyperview, config);
|
|
}
|
|
}
|
|
|
|
export class AverageResolver extends AggregationResolver {
|
|
constructor(hyperview: Hyperview, properties: PropertyID[]) {
|
|
const config: AggregationConfig = {};
|
|
properties.forEach(prop => config[prop] = 'average');
|
|
super(hyperview, config);
|
|
}
|
|
}
|
|
|
|
export class CountResolver extends AggregationResolver {
|
|
constructor(hyperview: Hyperview, properties: PropertyID[]) {
|
|
const config: AggregationConfig = {};
|
|
properties.forEach(prop => config[prop] = 'count');
|
|
super(hyperview, config);
|
|
}
|
|
} |