542 lines
14 KiB
TypeScript
542 lines
14 KiB
TypeScript
import {RhizomeNode} from "../src/node";
|
|
import {Lossless} from "../src/lossless";
|
|
import {Delta} from "../src/delta";
|
|
import {
|
|
AggregationResolver,
|
|
MinResolver,
|
|
MaxResolver,
|
|
SumResolver,
|
|
AverageResolver,
|
|
CountResolver,
|
|
AggregationType
|
|
} from "../src/aggregation-resolvers";
|
|
|
|
describe('Aggregation Resolvers', () => {
|
|
let node: RhizomeNode;
|
|
let lossless: Lossless;
|
|
|
|
beforeEach(() => {
|
|
node = new RhizomeNode();
|
|
lossless = new Lossless(node);
|
|
});
|
|
|
|
describe('Basic Aggregation', () => {
|
|
test('should aggregate numbers using min resolver', () => {
|
|
// Add first entity with score 10
|
|
lossless.ingestDelta(new Delta({
|
|
creator: 'test',
|
|
host: 'host1',
|
|
pointers: [{
|
|
localContext: "collection",
|
|
target: "entity1",
|
|
targetContext: "score"
|
|
}, {
|
|
localContext: "score",
|
|
target: 10
|
|
}]
|
|
}));
|
|
|
|
// Add second entity with score 5
|
|
lossless.ingestDelta(new Delta({
|
|
creator: 'test',
|
|
host: 'host1',
|
|
pointers: [{
|
|
localContext: "collection",
|
|
target: "entity2",
|
|
targetContext: "score"
|
|
}, {
|
|
localContext: "score",
|
|
target: 5
|
|
}]
|
|
}));
|
|
|
|
// Add third entity with score 15
|
|
lossless.ingestDelta(new Delta({
|
|
creator: 'test',
|
|
host: 'host1',
|
|
pointers: [{
|
|
localContext: "collection",
|
|
target: "entity3",
|
|
targetContext: "score"
|
|
}, {
|
|
localContext: "score",
|
|
target: 15
|
|
}]
|
|
}));
|
|
|
|
const minResolver = new MinResolver(lossless, ['score']);
|
|
const result = minResolver.resolve();
|
|
|
|
expect(result).toBeDefined();
|
|
expect(Object.keys(result!)).toHaveLength(3);
|
|
expect(result!['entity1'].properties.score).toBe(10);
|
|
expect(result!['entity2'].properties.score).toBe(5);
|
|
expect(result!['entity3'].properties.score).toBe(15);
|
|
});
|
|
|
|
test('should aggregate numbers using max resolver', () => {
|
|
// Add deltas for entities
|
|
lossless.ingestDelta(new Delta({
|
|
creator: 'test',
|
|
host: 'host1',
|
|
pointers: [{
|
|
localContext: "collection",
|
|
target: "entity1",
|
|
targetContext: "score"
|
|
}, {
|
|
localContext: "score",
|
|
target: 10
|
|
}]
|
|
}));
|
|
|
|
lossless.ingestDelta(new Delta({
|
|
creator: 'test',
|
|
host: 'host1',
|
|
pointers: [{
|
|
localContext: "collection",
|
|
target: "entity2",
|
|
targetContext: "score"
|
|
}, {
|
|
localContext: "score",
|
|
target: 5
|
|
}]
|
|
}));
|
|
|
|
lossless.ingestDelta(new Delta({
|
|
creator: 'test',
|
|
host: 'host1',
|
|
pointers: [{
|
|
localContext: "collection",
|
|
target: "entity3",
|
|
targetContext: "score"
|
|
}, {
|
|
localContext: "score",
|
|
target: 15
|
|
}]
|
|
}));
|
|
|
|
const maxResolver = new MaxResolver(lossless, ['score']);
|
|
const result = maxResolver.resolve();
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result!['entity1'].properties.score).toBe(10);
|
|
expect(result!['entity2'].properties.score).toBe(5);
|
|
expect(result!['entity3'].properties.score).toBe(15);
|
|
});
|
|
|
|
test('should aggregate numbers using sum resolver', () => {
|
|
// Add first value for entity1
|
|
lossless.ingestDelta(new Delta({
|
|
creator: 'test',
|
|
host: 'host1',
|
|
pointers: [{
|
|
localContext: "collection",
|
|
target: "entity1",
|
|
targetContext: "value"
|
|
}, {
|
|
localContext: "value",
|
|
target: 10
|
|
}]
|
|
}));
|
|
|
|
// Add second value for entity1 (should sum)
|
|
lossless.ingestDelta(new Delta({
|
|
creator: 'test',
|
|
host: 'host1',
|
|
pointers: [{
|
|
localContext: "collection",
|
|
target: "entity1",
|
|
targetContext: "value"
|
|
}, {
|
|
localContext: "value",
|
|
target: 20
|
|
}]
|
|
}));
|
|
|
|
// Add value for entity2
|
|
lossless.ingestDelta(new Delta({
|
|
creator: 'test',
|
|
host: 'host1',
|
|
pointers: [{
|
|
localContext: "collection",
|
|
target: "entity2",
|
|
targetContext: "value"
|
|
}, {
|
|
localContext: "value",
|
|
target: 5
|
|
}]
|
|
}));
|
|
|
|
const sumResolver = new SumResolver(lossless, ['value']);
|
|
const result = sumResolver.resolve();
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result!['entity1'].properties.value).toBe(30); // 10 + 20
|
|
expect(result!['entity2'].properties.value).toBe(5);
|
|
});
|
|
|
|
test('should aggregate numbers using average resolver', () => {
|
|
// Add multiple values for entity1
|
|
lossless.ingestDelta(new Delta({
|
|
creator: 'test',
|
|
host: 'host1',
|
|
pointers: [{
|
|
localContext: "collection",
|
|
target: "entity1",
|
|
targetContext: "score"
|
|
}, {
|
|
localContext: "score",
|
|
target: 10
|
|
}]
|
|
}));
|
|
|
|
lossless.ingestDelta(new Delta({
|
|
creator: 'test',
|
|
host: 'host1',
|
|
pointers: [{
|
|
localContext: "collection",
|
|
target: "entity1",
|
|
targetContext: "score"
|
|
}, {
|
|
localContext: "score",
|
|
target: 20
|
|
}]
|
|
}));
|
|
|
|
// Single value for entity2
|
|
lossless.ingestDelta(new Delta({
|
|
creator: 'test',
|
|
host: 'host1',
|
|
pointers: [{
|
|
localContext: "collection",
|
|
target: "entity2",
|
|
targetContext: "score"
|
|
}, {
|
|
localContext: "score",
|
|
target: 30
|
|
}]
|
|
}));
|
|
|
|
const avgResolver = new AverageResolver(lossless, ['score']);
|
|
const result = avgResolver.resolve();
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result!['entity1'].properties.score).toBe(15); // (10 + 20) / 2
|
|
expect(result!['entity2'].properties.score).toBe(30);
|
|
});
|
|
|
|
test('should count values using count resolver', () => {
|
|
// Add multiple visit deltas for entity1
|
|
lossless.ingestDelta(new Delta({
|
|
creator: 'test',
|
|
host: 'host1',
|
|
pointers: [{
|
|
localContext: "collection",
|
|
target: "entity1",
|
|
targetContext: "visits"
|
|
}, {
|
|
localContext: "visits",
|
|
target: 1
|
|
}]
|
|
}));
|
|
|
|
lossless.ingestDelta(new Delta({
|
|
creator: 'test',
|
|
host: 'host1',
|
|
pointers: [{
|
|
localContext: "collection",
|
|
target: "entity1",
|
|
targetContext: "visits"
|
|
}, {
|
|
localContext: "visits",
|
|
target: 1
|
|
}]
|
|
}));
|
|
|
|
// Single visit for entity2
|
|
lossless.ingestDelta(new Delta({
|
|
creator: 'test',
|
|
host: 'host1',
|
|
pointers: [{
|
|
localContext: "collection",
|
|
target: "entity2",
|
|
targetContext: "visits"
|
|
}, {
|
|
localContext: "visits",
|
|
target: 1
|
|
}]
|
|
}));
|
|
|
|
const countResolver = new CountResolver(lossless, ['visits']);
|
|
const result = countResolver.resolve();
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result!['entity1'].properties.visits).toBe(2); // count of 2 deltas
|
|
expect(result!['entity2'].properties.visits).toBe(1); // count of 1 delta
|
|
});
|
|
});
|
|
|
|
describe('Custom Aggregation Configuration', () => {
|
|
test('should handle mixed aggregation types', () => {
|
|
// Add first set of values
|
|
lossless.ingestDelta(new Delta({
|
|
creator: 'test',
|
|
host: 'host1',
|
|
pointers: [{
|
|
localContext: "collection",
|
|
target: "entity1",
|
|
targetContext: "min_val"
|
|
}, {
|
|
localContext: "min_val",
|
|
target: 10
|
|
}]
|
|
}));
|
|
|
|
lossless.ingestDelta(new Delta({
|
|
creator: 'test',
|
|
host: 'host1',
|
|
pointers: [{
|
|
localContext: "collection",
|
|
target: "entity1",
|
|
targetContext: "max_val"
|
|
}, {
|
|
localContext: "max_val",
|
|
target: 5
|
|
}]
|
|
}));
|
|
|
|
lossless.ingestDelta(new Delta({
|
|
creator: 'test',
|
|
host: 'host1',
|
|
pointers: [{
|
|
localContext: "collection",
|
|
target: "entity1",
|
|
targetContext: "sum_val"
|
|
}, {
|
|
localContext: "sum_val",
|
|
target: 3
|
|
}]
|
|
}));
|
|
|
|
// Add second set of values
|
|
lossless.ingestDelta(new Delta({
|
|
creator: 'test',
|
|
host: 'host1',
|
|
pointers: [{
|
|
localContext: "collection",
|
|
target: "entity1",
|
|
targetContext: "min_val"
|
|
}, {
|
|
localContext: "min_val",
|
|
target: 5
|
|
}]
|
|
}));
|
|
|
|
lossless.ingestDelta(new Delta({
|
|
creator: 'test',
|
|
host: 'host1',
|
|
pointers: [{
|
|
localContext: "collection",
|
|
target: "entity1",
|
|
targetContext: "max_val"
|
|
}, {
|
|
localContext: "max_val",
|
|
target: 15
|
|
}]
|
|
}));
|
|
|
|
lossless.ingestDelta(new Delta({
|
|
creator: 'test',
|
|
host: 'host1',
|
|
pointers: [{
|
|
localContext: "collection",
|
|
target: "entity1",
|
|
targetContext: "sum_val"
|
|
}, {
|
|
localContext: "sum_val",
|
|
target: 7
|
|
}]
|
|
}));
|
|
|
|
const resolver = new AggregationResolver(lossless, {
|
|
min_val: 'min' as AggregationType,
|
|
max_val: 'max' as AggregationType,
|
|
sum_val: 'sum' as AggregationType
|
|
});
|
|
|
|
const result = resolver.resolve();
|
|
expect(result).toBeDefined();
|
|
|
|
const entity = result!['entity1'];
|
|
expect(entity.properties.min_val).toBe(5); // min of 10, 5
|
|
expect(entity.properties.max_val).toBe(15); // max of 5, 15
|
|
expect(entity.properties.sum_val).toBe(10); // sum of 3, 7
|
|
});
|
|
|
|
test('should ignore non-numeric values', () => {
|
|
// Add numeric value
|
|
lossless.ingestDelta(new Delta({
|
|
creator: 'test',
|
|
host: 'host1',
|
|
pointers: [{
|
|
localContext: "collection",
|
|
target: "entity1",
|
|
targetContext: "score"
|
|
}, {
|
|
localContext: "score",
|
|
target: 10
|
|
}]
|
|
}));
|
|
|
|
// Add non-numeric value (string)
|
|
lossless.ingestDelta(new Delta({
|
|
creator: 'test',
|
|
host: 'host1',
|
|
pointers: [{
|
|
localContext: "collection",
|
|
target: "entity1",
|
|
targetContext: "name"
|
|
}, {
|
|
localContext: "name",
|
|
target: 'test'
|
|
}]
|
|
}));
|
|
|
|
// Add another numeric value
|
|
lossless.ingestDelta(new Delta({
|
|
creator: 'test',
|
|
host: 'host1',
|
|
pointers: [{
|
|
localContext: "collection",
|
|
target: "entity1",
|
|
targetContext: "score"
|
|
}, {
|
|
localContext: "score",
|
|
target: 20
|
|
}]
|
|
}));
|
|
|
|
const sumResolver = new SumResolver(lossless, ['score', 'name']);
|
|
const result = sumResolver.resolve();
|
|
|
|
expect(result).toBeDefined();
|
|
const entity = result!['entity1'];
|
|
expect(entity.properties.score).toBe(30); // sum of 10, 20
|
|
expect(entity.properties.name).toBe(0); // ignored non-numeric, defaults to 0
|
|
});
|
|
|
|
test('should handle empty value arrays', () => {
|
|
// Create entity with non-aggregated property
|
|
lossless.ingestDelta(new Delta({
|
|
creator: 'test',
|
|
host: 'host1',
|
|
pointers: [{
|
|
localContext: "collection",
|
|
target: "entity1",
|
|
targetContext: "name"
|
|
}, {
|
|
localContext: "name",
|
|
target: 'test'
|
|
}]
|
|
}));
|
|
|
|
const sumResolver = new SumResolver(lossless, ['score']);
|
|
const result = sumResolver.resolve();
|
|
|
|
expect(result).toBeDefined();
|
|
// Should not have entity1 since no 'score' property was found
|
|
expect(result!['entity1']).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('Edge Cases', () => {
|
|
test('should handle single value aggregations', () => {
|
|
lossless.ingestDelta(new Delta({
|
|
creator: 'test',
|
|
host: 'host1',
|
|
pointers: [{
|
|
localContext: "collection",
|
|
target: "entity1",
|
|
targetContext: "value"
|
|
}, {
|
|
localContext: "value",
|
|
target: 42
|
|
}]
|
|
}));
|
|
|
|
const avgResolver = new AverageResolver(lossless, ['value']);
|
|
const result = avgResolver.resolve();
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result!['entity1'].properties.value).toBe(42);
|
|
});
|
|
|
|
test('should handle zero values', () => {
|
|
lossless.ingestDelta(new Delta({
|
|
creator: 'test',
|
|
host: 'host1',
|
|
pointers: [{
|
|
localContext: "collection",
|
|
target: "entity1",
|
|
targetContext: "value"
|
|
}, {
|
|
localContext: "value",
|
|
target: 0
|
|
}]
|
|
}));
|
|
|
|
lossless.ingestDelta(new Delta({
|
|
creator: 'test',
|
|
host: 'host1',
|
|
pointers: [{
|
|
localContext: "collection",
|
|
target: "entity1",
|
|
targetContext: "value"
|
|
}, {
|
|
localContext: "value",
|
|
target: 10
|
|
}]
|
|
}));
|
|
|
|
const sumResolver = new SumResolver(lossless, ['value']);
|
|
const result = sumResolver.resolve();
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result!['entity1'].properties.value).toBe(10); // 0 + 10
|
|
});
|
|
|
|
test('should handle negative values', () => {
|
|
lossless.ingestDelta(new Delta({
|
|
creator: 'test',
|
|
host: 'host1',
|
|
pointers: [{
|
|
localContext: "collection",
|
|
target: "entity1",
|
|
targetContext: "value"
|
|
}, {
|
|
localContext: "value",
|
|
target: -5
|
|
}]
|
|
}));
|
|
|
|
lossless.ingestDelta(new Delta({
|
|
creator: 'test',
|
|
host: 'host1',
|
|
pointers: [{
|
|
localContext: "collection",
|
|
target: "entity1",
|
|
targetContext: "value"
|
|
}, {
|
|
localContext: "value",
|
|
target: 10
|
|
}]
|
|
}));
|
|
|
|
const minResolver = new MinResolver(lossless, ['value']);
|
|
const result = minResolver.resolve();
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result!['entity1'].properties.value).toBe(-5);
|
|
});
|
|
});
|
|
}); |