Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions examples/app-crm/src/datasets/opportunity.dataset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export const OpportunityDataset = defineDataset({

dimensions: [
{ name: 'stage', label: 'Stage', field: 'stage', type: 'string' },
// Lookup dimension — the analytics layer resolves the account FK to its
// display name in `rows`, but exposes the raw FK via drillRawRows so a
// report drill filters by the stored id (ADR-0021 D2), not the name.
{ name: 'account', label: 'Account', field: 'account', type: 'lookup' },
// ADR-0021 single-form: the monthly bucketing the trend widget used to carry
// as `categoryGranularity: 'month'` now lives on the dimension itself.
{ name: 'close_date', label: 'Close Date', field: 'close_date', type: 'date', dateGranularity: 'month' },
Expand Down
1 change: 1 addition & 0 deletions examples/app-crm/src/reports/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

export { SalesByStageReport } from './sales-by-stage.report.js';
export { SalesByAccountReport } from './sales-by-account.report.js';
24 changes: 24 additions & 0 deletions examples/app-crm/src/reports/sales-by-account.report.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

import type * as UI from '@objectstack/spec/ui';

/**
* Example report — total opportunity amount grouped by ACCOUNT.
*
* Bound to `opportunity_metrics` with rows = `account`, a LOOKUP dimension.
* The analytics layer resolves each account FK to its display name in `rows`,
* but exposes the raw FK via `drillRawRows` + `dimensionFields` (ADR-0021 D2),
* so drilling a row filters the opportunity list by the account's stored id —
* not its (possibly non-unique) display name. Paired with the currency-aware
* `total_amount` measure (USD), this exercises both render paths — Intl
* currency formatting AND raw-value lookup drill — end to end.
*/
export const SalesByAccountReport: UI.ReportInput = {

Check failure on line 16 in examples/app-crm/src/reports/sales-by-account.report.ts

View workflow job for this annotation

GitHub Actions / ESLint

Author this metadata through its defineX factory (e.g. `definePage({ ... })`) instead of a bare `: Type` literal. The factory validates at parse time and a broken value import fails loudly instead of degrading to `any` — see issue #2035
name: 'crm_sales_by_account',
label: 'Sales by Account',
description: 'Total opportunity amount grouped by account (lookup-dimension drill).',
type: 'summary',
dataset: 'opportunity_metrics',
rows: ['account'],
values: ['total_amount'],
};
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,23 @@ describe('AnalyticsService.queryDataset', () => {
expect(result.object).toBeUndefined();
expect(result.drillRawRows).toBeUndefined();
});

it('marks a LOOKUP dimension drillable, exposing the raw FK for exact-match drill', async () => {
const byAccount = DatasetSchema.parse({
name: 'sales_acct', label: 'Sales', object: 'opportunity', include: [],
dimensions: [{ name: 'account', field: 'account', type: 'lookup', label: 'Account' }],
measures: [{ name: 'revenue', aggregate: 'sum', field: 'amount', certified: true }],
});
const svc = new AnalyticsService({
queryCapabilities: () => ({ nativeSql: true, objectqlAggregate: false, inMemory: false }),
executeRawSql: async () => [{ account: 'acc_123', revenue: 1000 }],
getReadScope: (_o, ctx?: ExecutionContext) => (ctx?.tenantId ? { organization_id: ctx.tenantId } : undefined),
});
const result = await svc.queryDataset(byAccount, { dimensions: ['account'], measures: ['revenue'] }, { tenantId: 'org_A' } as ExecutionContext) as any;
// A lookup dim IS drillable (unlike a date bucket): its raw FK is exposed so
// the report drill filters by the stored id, not the resolved display name.
expect(result.object).toBe('opportunity');
expect(result.dimensionFields).toEqual({ account: 'account' });
expect(result.drillRawRows).toEqual([{ account: 'acc_123' }]);
});
});
Loading