rhizome-node/src/views/resolvers/aggregation-resolvers.ts

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);
}
}