Skip to content

Commit a20e037

Browse files
committed
Add fallback to client-side aggregation when backend drops measure
1 parent bd12758 commit a20e037

2 files changed

Lines changed: 47 additions & 0 deletions

File tree

packages/data-objectstack/src/aggregate.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,4 +274,33 @@ describe('ObjectStackAdapter aggregate()', () => {
274274
expect(result.find((r: any) => r.stage === 'Prospect')?.amount).toBe(300);
275275
expect(result.find((r: any) => r.stage === 'Closed Won')?.amount).toBe(500);
276276
});
277+
278+
it('should fall back to client-side aggregation when backend returns rows missing the measure', async () => {
279+
mockAnalyticsQuery.mockResolvedValue({
280+
rows: [
281+
{ stage: 'Prospect' },
282+
{ stage: 'Closed Won' },
283+
],
284+
fields: [{ name: 'stage', type: 'string' }],
285+
});
286+
287+
(adapter as any).client.data.find = vi.fn().mockResolvedValue({
288+
records: [
289+
{ stage: 'Prospect', amount: 100 },
290+
{ stage: 'Prospect', amount: 200 },
291+
{ stage: 'Closed Won', amount: 500 },
292+
],
293+
total: 3,
294+
});
295+
296+
const result = await adapter.aggregate('opportunity', {
297+
field: 'amount',
298+
function: 'sum',
299+
groupBy: 'stage',
300+
});
301+
302+
expect(result).toHaveLength(2);
303+
expect(result.find((r: any) => r.stage === 'Prospect')?.amount).toBe(300);
304+
expect(result.find((r: any) => r.stage === 'Closed Won')?.amount).toBe(500);
305+
});
277306
});

packages/data-objectstack/src/index.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,6 +1055,24 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
10551055
: data?.results && Array.isArray(data.results) ? data.results
10561056
: [];
10571057

1058+
// Defensive guard: if the backend silently dropped the requested measure
1059+
// (e.g. it doesn't recognise the `${field}_${function}` alias and the
1060+
// canonical measure is named differently), the rows come back without
1061+
// any measure value. Detect this and fall back to client-side
1062+
// aggregation so charts still render.
1063+
const measureMissing = rawRows.length > 0 && rawRows.every((row: any) => {
1064+
if (row == null) return true;
1065+
if (measureName in row && row[measureName] != null) return false;
1066+
if (params.field in row && row[params.field] != null) return false;
1067+
return true;
1068+
});
1069+
if (measureMissing) {
1070+
const result = await this.find(resource as any);
1071+
const records = result.data || [];
1072+
if (records.length === 0) return [];
1073+
return this.aggregateClientSide(records, params);
1074+
}
1075+
10581076
// Map measure keys back to the original field name so that consumers
10591077
// (ObjectChart, DashboardRenderer, etc.) can access values by field name.
10601078
// This includes count → field (e.g. 'count' → 'amount') to match the

0 commit comments

Comments
 (0)