Skip to content

Commit 94c7c6e

Browse files
xuyushun441-sysos-zhuangclaude
authored
test(example-crm): add a lookup-dimension report fixture (sales by account) (#2107)
Demonstrates + exercises the raw-value lookup drill (ADR-0021 D2) end to end, the path that had no example coverage: - opportunity_metrics gains an `account` LOOKUP dimension. The analytics layer resolves the FK to the account's display name in `rows` but exposes the raw FK via drillRawRows + dimensionFields, so a drill filters by the stored id. - A new `SalesByAccountReport` groups `total_amount` (USD, currency-aware) by account — so this one report exercises BOTH render paths: Intl currency formatting AND raw-value lookup drill. - service-analytics test: a LOOKUP dimension is drillable (mirror of the date-dimension exclusion test) — asserts object + dimensionFields + raw FK. Example is private; the service-analytics change is test-only (no shipped API change) → no changeset. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 0a121b1 commit 94c7c6e

4 files changed

Lines changed: 48 additions & 0 deletions

File tree

examples/app-crm/src/datasets/opportunity.dataset.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ export const OpportunityDataset = defineDataset({
1818

1919
dimensions: [
2020
{ name: 'stage', label: 'Stage', field: 'stage', type: 'string' },
21+
// Lookup dimension — the analytics layer resolves the account FK to its
22+
// display name in `rows`, but exposes the raw FK via drillRawRows so a
23+
// report drill filters by the stored id (ADR-0021 D2), not the name.
24+
{ name: 'account', label: 'Account', field: 'account', type: 'lookup' },
2125
// ADR-0021 single-form: the monthly bucketing the trend widget used to carry
2226
// as `categoryGranularity: 'month'` now lives on the dimension itself.
2327
{ name: 'close_date', label: 'Close Date', field: 'close_date', type: 'date', dateGranularity: 'month' },
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

33
export { SalesByStageReport } from './sales-by-stage.report.js';
4+
export { SalesByAccountReport } from './sales-by-account.report.js';
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import type * as UI from '@objectstack/spec/ui';
4+
5+
/**
6+
* Example report — total opportunity amount grouped by ACCOUNT.
7+
*
8+
* Bound to `opportunity_metrics` with rows = `account`, a LOOKUP dimension.
9+
* The analytics layer resolves each account FK to its display name in `rows`,
10+
* but exposes the raw FK via `drillRawRows` + `dimensionFields` (ADR-0021 D2),
11+
* so drilling a row filters the opportunity list by the account's stored id —
12+
* not its (possibly non-unique) display name. Paired with the currency-aware
13+
* `total_amount` measure (USD), this exercises both render paths — Intl
14+
* currency formatting AND raw-value lookup drill — end to end.
15+
*/
16+
export const SalesByAccountReport: UI.ReportInput = {
17+
name: 'crm_sales_by_account',
18+
label: 'Sales by Account',
19+
description: 'Total opportunity amount grouped by account (lookup-dimension drill).',
20+
type: 'summary',
21+
dataset: 'opportunity_metrics',
22+
rows: ['account'],
23+
values: ['total_amount'],
24+
};

packages/services/service-analytics/src/__tests__/query-dataset.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,4 +154,23 @@ describe('AnalyticsService.queryDataset', () => {
154154
expect(result.object).toBeUndefined();
155155
expect(result.drillRawRows).toBeUndefined();
156156
});
157+
158+
it('marks a LOOKUP dimension drillable, exposing the raw FK for exact-match drill', async () => {
159+
const byAccount = DatasetSchema.parse({
160+
name: 'sales_acct', label: 'Sales', object: 'opportunity', include: [],
161+
dimensions: [{ name: 'account', field: 'account', type: 'lookup', label: 'Account' }],
162+
measures: [{ name: 'revenue', aggregate: 'sum', field: 'amount', certified: true }],
163+
});
164+
const svc = new AnalyticsService({
165+
queryCapabilities: () => ({ nativeSql: true, objectqlAggregate: false, inMemory: false }),
166+
executeRawSql: async () => [{ account: 'acc_123', revenue: 1000 }],
167+
getReadScope: (_o, ctx?: ExecutionContext) => (ctx?.tenantId ? { organization_id: ctx.tenantId } : undefined),
168+
});
169+
const result = await svc.queryDataset(byAccount, { dimensions: ['account'], measures: ['revenue'] }, { tenantId: 'org_A' } as ExecutionContext) as any;
170+
// A lookup dim IS drillable (unlike a date bucket): its raw FK is exposed so
171+
// the report drill filters by the stored id, not the resolved display name.
172+
expect(result.object).toBe('opportunity');
173+
expect(result.dimensionFields).toEqual({ account: 'account' });
174+
expect(result.drillRawRows).toEqual([{ account: 'acc_123' }]);
175+
});
157176
});

0 commit comments

Comments
 (0)