@happyvertical/smrt-reports 0.57.32 → 0.57.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -0
- package/README.md +64 -3
- package/dist/adapter.d.ts +18 -1
- package/dist/adapter.d.ts.map +1 -1
- package/dist/chunks/compiler-IUB2YNgM.js +341 -0
- package/dist/chunks/compiler-IUB2YNgM.js.map +1 -0
- package/dist/compiler.d.ts +2 -0
- package/dist/compiler.d.ts.map +1 -1
- package/dist/compiler.js +2 -171
- package/dist/decorators.d.ts +22 -22
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +16 -8
- package/dist/decorators.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +54 -20
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/refresh.d.ts +0 -1
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +6 -18
- package/dist/refresh.js.map +1 -1
- package/dist/registry-columns.d.ts +14 -0
- package/dist/registry-columns.d.ts.map +1 -0
- package/dist/runtime-compiler.d.ts.map +1 -1
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +2 -2
- package/dist/scheduler.js.map +1 -1
- package/dist/smrt-knowledge.json +7 -7
- package/dist/state.js +1 -1
- package/dist/state.js.map +1 -1
- package/dist/types.d.ts +64 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/views.d.ts.map +1 -1
- package/package.json +9 -9
- package/dist/compiler.js.map +0 -1
package/AGENTS.md
CHANGED
|
@@ -122,6 +122,7 @@ See [core recipes](../core/agents/recipes.md).
|
|
|
122
122
|
|
|
123
123
|
- Report cache tables are normal `@smrt()` tables. Runtime refresh must not create schema.
|
|
124
124
|
- Store report metadata under field `_meta.__report`; scanner and runtime decorators must stay aligned.
|
|
125
|
+
- Joins, row expressions and filtered measures (#3903) compile to the SDK's `AggregateSpec` (`fromAlias`, `joins`, `expr`, `filter`); expressions are literal data nodes because the scanner reads decorator arguments statically. Qualify every source column with `src` once a report joins (refresh filters do), put the tenant match in the join's `ON`, and joined reports rebuild (an incremental request is run as a rebuild), and drilldown refuses joined or computed group keys.
|
|
125
126
|
- Keep SQL generation portable. Use the SDK `buildAggregate()`/`bucketExpr()` helpers for time buckets and `$N` placeholders.
|
|
126
127
|
- Do not add a local aggregate SQL builder here; the implementation lives in `@happyvertical/sql`.
|
|
127
128
|
- Refresh runtime tables are schema-managed. Runtime refresh must fail clearly when `_smrt_report_runs`, `_smrt_report_watermarks`, or `_smrt_report_locks` have not been migrated.
|
package/README.md
CHANGED
|
@@ -62,16 +62,77 @@ const rows = await reports.list({ orderBy: 'issued_month DESC' });
|
|
|
62
62
|
| Decorator | Meaning |
|
|
63
63
|
| --- | --- |
|
|
64
64
|
| `@report({ source, ... })` | Declare the source and refresh policy |
|
|
65
|
-
| `@groupBy(column?)` | Group by a source column |
|
|
65
|
+
| `@groupBy(column?)` | Group by a source column, a joined column (`'wo.title'`), or `{ expr }` |
|
|
66
66
|
| `@minute()` through `@year()` | Bucket a source timestamp |
|
|
67
|
-
| `@sum()`, `@avg()`, `@min()`, `@max()` | Aggregate a source column |
|
|
68
|
-
| `@count()` | Count rows or distinct values |
|
|
67
|
+
| `@sum()`, `@avg()`, `@min()`, `@max()` | Aggregate a source or joined column, or a row `{ expr }`, optionally `{ filter }`ed |
|
|
68
|
+
| `@count()` | Count rows or distinct values, optionally `{ filter }`ed |
|
|
69
69
|
| `@aggregate()` | Define a custom supported aggregate shape |
|
|
70
70
|
|
|
71
71
|
Every non-system report field must be a grouping key, time bucket, or aggregate.
|
|
72
72
|
Metadata is stored under the field's `_meta.__report` contract so scanner and
|
|
73
73
|
runtime compilation stay aligned.
|
|
74
74
|
|
|
75
|
+
## Joins, row expressions and filtered measures
|
|
76
|
+
|
|
77
|
+
A report can read a related model, compute a value from each row, and count
|
|
78
|
+
only the rows a condition holds for. Each compiles to the SQL aggregate API in
|
|
79
|
+
`@happyvertical/sql`; nothing is built as SQL text here.
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
@report({
|
|
83
|
+
source: AttendanceSegment,
|
|
84
|
+
// Many-to-one: AttendanceSegment.workRefId holds a WorkOrder id.
|
|
85
|
+
joins: { wo: { source: WorkOrder, on: 'workRefId' } },
|
|
86
|
+
})
|
|
87
|
+
class LabourByWorkOrder extends SmrtReport {
|
|
88
|
+
@groupBy() workRefId = '';
|
|
89
|
+
@groupBy('wo.title') workOrder = '';
|
|
90
|
+
@max('wo.estimatedHours') estimatedHours = 0;
|
|
91
|
+
|
|
92
|
+
// A row expression: seconds from createdAt to claimedAt, averaged.
|
|
93
|
+
@avg({ expr: { op: 'secondsBetween', later: { col: 'claimedAt' }, earlier: { col: 'createdAt' } } })
|
|
94
|
+
avgResponseSeconds = 0;
|
|
95
|
+
|
|
96
|
+
// A filtered count: one number rather than a two-row grouping.
|
|
97
|
+
@count({ filter: { op: 'eq', left: { col: 'halted' }, right: { value: true } } })
|
|
98
|
+
haltedSegments = 0;
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
- **Joins** are many-to-one, keyed `alias: { source, on, to?, type? }`. `on` is a
|
|
103
|
+
field of the report source; `to` is the related field it points at (default
|
|
104
|
+
`id`) and must identify one related row per tenant, or source rows repeat.
|
|
105
|
+
`type` is `left` (default, keeps a source row whose related row is gone) or
|
|
106
|
+
`inner`. Aliases are lower-case identifiers and not `src`. Reference a joined
|
|
107
|
+
field as `alias.field` in `@groupBy`, the aggregate and bucket decorators,
|
|
108
|
+
expressions and `where`.
|
|
109
|
+
- **Tenancy is built into the join.** When the related model is tenant-scoped the
|
|
110
|
+
join also matches `tenant_id` between the two rows, so an id that points at
|
|
111
|
+
another tenant's row never matches. A tenant-scoped join from a source with no
|
|
112
|
+
tenant field is refused. Soft-deleted related rows never match.
|
|
113
|
+
- **Field policy follows the data.** A report column that reads a joined field
|
|
114
|
+
marked sensitive or `readPermission`-gated is withheld from the adapter
|
|
115
|
+
descriptor exactly as that field would be.
|
|
116
|
+
- **Expressions are data, written as literals** (the manifest scanner reads
|
|
117
|
+
decorator arguments statically, so builder calls would be dropped): `{ col }`,
|
|
118
|
+
`{ value }`, `add`/`sub`/`mul`/`div`, `secondsBetween`, `eq`/`ne`/`lt`/`lte`/
|
|
119
|
+
`gt`/`gte`, `and`/`or`/`not`, `isNull`/`isNotNull` and `coalesce`. A `col`
|
|
120
|
+
names a model field, never a SQL column. `div` returns NULL for a zero
|
|
121
|
+
divisor. `expr` and `filter` work with `count`, `sum`, `avg`, `min` and `max`.
|
|
122
|
+
- **Joined reports refresh by rebuild only.** A change to a joined row does not
|
|
123
|
+
move the source's watermark, so declaring `mode: 'incremental'` is refused and
|
|
124
|
+
any trigger that asks for an incremental refresh (a schedule, `onChange`, a
|
|
125
|
+
job) is run as a rebuild. List the joined models in `refresh.onChange` to
|
|
126
|
+
rebuild when they change. Rows whose tenant is `NULL` (the global slice) do not
|
|
127
|
+
match tenant-scoped related rows, because `NULL = NULL` is not true in SQL.
|
|
128
|
+
- **Saved views and snapshots notice definition changes.** A column's expression, filter and the joins it reads are part of its descriptor (`semantics`) and so of the report definition fingerprint; changing one invalidates stored views and export snapshots. Reports without joins or expressions keep their existing fingerprints.
|
|
129
|
+
- **No drilldown by joined or computed keys.** A row grouped by a joined column or
|
|
130
|
+
a computed value cannot be bound back to source rows by a source column, so
|
|
131
|
+
`buildReportDrilldownQuery()` refuses such reports.
|
|
132
|
+
|
|
133
|
+
The runtime-report grammar stays closed: runtime reports have no joins or
|
|
134
|
+
expressions.
|
|
135
|
+
|
|
75
136
|
## Refresh modes
|
|
76
137
|
|
|
77
138
|
- `rebuild` replaces the materialized result.
|
package/dist/adapter.d.ts
CHANGED
|
@@ -13,7 +13,11 @@ export type ReportColumnKind = 'identity' | 'group' | 'bucket' | 'aggregate';
|
|
|
13
13
|
* (HAVING) when constructing drilldowns, saved views, or a live query.
|
|
14
14
|
*/
|
|
15
15
|
export type ReportFilterScope = 'where' | 'having';
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Report columns do not carry data-query allowed values (#4381): the adapter
|
|
18
|
+
* builds its schema from report definitions and never emits them.
|
|
19
|
+
*/
|
|
20
|
+
export interface ReportColumnDescriptor extends Omit<DataQueryFieldDescriptor, 'values' | 'valuesEnforced'> {
|
|
17
21
|
/** Stable output column id (never a property path). */
|
|
18
22
|
id: string;
|
|
19
23
|
fieldName: string;
|
|
@@ -28,6 +32,13 @@ export interface ReportColumnDescriptor extends DataQueryFieldDescriptor {
|
|
|
28
32
|
bucket?: ReportTimeBucketUnit;
|
|
29
33
|
aggregate?: ReportAggregateFn;
|
|
30
34
|
distinct?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* A canonical rendering of what the column computes beyond a plain source
|
|
37
|
+
* column: its row expression and filter, and the joins it reads. It exists
|
|
38
|
+
* so a changed definition changes the report fingerprint; consumers treat it
|
|
39
|
+
* as opaque.
|
|
40
|
+
*/
|
|
41
|
+
semantics?: string;
|
|
31
42
|
}
|
|
32
43
|
export interface ReportRefreshDescriptor {
|
|
33
44
|
mode: 'rebuild' | 'incremental';
|
|
@@ -70,6 +81,12 @@ export interface ReportAdapterDescriptor {
|
|
|
70
81
|
queryExecution: ReportQueryExecutionDescriptor;
|
|
71
82
|
dataTable: ReportDataTableDescriptor;
|
|
72
83
|
drilldown: ReportDrilldownDescriptor;
|
|
84
|
+
/**
|
|
85
|
+
* Present when the report joins other models: every join and the report's
|
|
86
|
+
* `where`/`having`, canonically rendered, so a changed join changes the
|
|
87
|
+
* definition fingerprint even when no visible column reads it. Opaque.
|
|
88
|
+
*/
|
|
89
|
+
semantics?: string;
|
|
73
90
|
refresh: ReportRefreshDescriptor;
|
|
74
91
|
}
|
|
75
92
|
/**
|
package/dist/adapter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,UAAU,EAChB,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EAEV,wBAAwB,EACxB,eAAe,EAGf,gBAAgB,EAChB,eAAe,EAEf,eAAe,EAChB,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EAEL,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAC7B,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { getRuntimeReportOptions } from "../decorators.js";
|
|
2
|
+
import { ObjectRegistry } from "@happyvertical/smrt-core";
|
|
3
|
+
import { toSnakeCase } from "@happyvertical/smrt-core/utils";
|
|
4
|
+
//#region src/registry-columns.ts
|
|
5
|
+
async function fieldColumn(className, requested) {
|
|
6
|
+
const fields = await ObjectRegistry.getAllFields(className);
|
|
7
|
+
const requestedColumn = toSnakeCase(requested);
|
|
8
|
+
for (const [fieldName] of fields.entries()) if (fieldName === requested || toSnakeCase(fieldName) === requestedColumn) return requestedColumn;
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
async function tenantColumn(className) {
|
|
12
|
+
const configuredField = ObjectRegistry.getClass(className)?.tenantScopedConfig?.field;
|
|
13
|
+
if (configuredField) return toSnakeCase(configuredField);
|
|
14
|
+
const fields = await ObjectRegistry.getAllFields(className);
|
|
15
|
+
for (const [fieldName, field] of fields.entries()) if (field?._meta?.__tenancy?.isTenantIdField) return toSnakeCase(fieldName);
|
|
16
|
+
return await fieldColumn(className, "tenantId");
|
|
17
|
+
}
|
|
18
|
+
function fieldPolicy(field) {
|
|
19
|
+
const meta = field?._meta ?? {};
|
|
20
|
+
const sensitivity = [field?.sensitivity, meta.sensitivity].find((value) => typeof value === "string");
|
|
21
|
+
return {
|
|
22
|
+
sensitive: field?.sensitive === true || meta.sensitive === true || sensitivity === "sensitive" || sensitivity === "secret",
|
|
23
|
+
readPermission: typeof field?.readPermission === "string" ? field.readPermission : typeof meta.readPermission === "string" ? meta.readPermission : void 0
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/compiler.ts
|
|
28
|
+
var REPORT_SOURCE_ALIAS = "src";
|
|
29
|
+
var JOIN_ALIAS = /^[a-z][a-z0-9_]*$/;
|
|
30
|
+
var REPORT_BASE_FIELDS = /* @__PURE__ */ new Set([
|
|
31
|
+
"id",
|
|
32
|
+
"slug",
|
|
33
|
+
"context",
|
|
34
|
+
"created_at",
|
|
35
|
+
"updated_at",
|
|
36
|
+
"createdAt",
|
|
37
|
+
"updatedAt",
|
|
38
|
+
"refreshedAt"
|
|
39
|
+
]);
|
|
40
|
+
function sourceClassName(source) {
|
|
41
|
+
if (typeof source === "string") return source;
|
|
42
|
+
return source.name;
|
|
43
|
+
}
|
|
44
|
+
function reportOptionsFromRegistry(reportCtor) {
|
|
45
|
+
return (ObjectRegistry.getClassByConstructor(reportCtor) ?? ObjectRegistry.getClass(reportCtor.name))?.config?.report;
|
|
46
|
+
}
|
|
47
|
+
function getReportOptions(reportCtor) {
|
|
48
|
+
const options = getRuntimeReportOptions(reportCtor) ?? reportOptionsFromRegistry(reportCtor);
|
|
49
|
+
if (!options?.source) throw new Error(`${reportCtor.name} is missing @report({ source }) metadata. Decorate the report class with @report({ source: SourceClass }).`);
|
|
50
|
+
return options;
|
|
51
|
+
}
|
|
52
|
+
function getRegisteredClassName(ctor) {
|
|
53
|
+
const registered = ObjectRegistry.getClassByConstructor(ctor) ?? ObjectRegistry.getClass(ctor.name);
|
|
54
|
+
return registered?.qualifiedName ?? registered?.name ?? ctor.name;
|
|
55
|
+
}
|
|
56
|
+
function resolveTableName(className) {
|
|
57
|
+
const tableName = ObjectRegistry.getTableName(className);
|
|
58
|
+
if (!tableName) throw new Error(`No SMRT table registered for ${className}. Ensure the class is decorated with @smrt() and its manifest is loaded.`);
|
|
59
|
+
return tableName;
|
|
60
|
+
}
|
|
61
|
+
function parseWhereKey(key) {
|
|
62
|
+
const trimmed = key.trim();
|
|
63
|
+
const operators = [
|
|
64
|
+
" is not null",
|
|
65
|
+
" is null",
|
|
66
|
+
" not in",
|
|
67
|
+
" like",
|
|
68
|
+
" in",
|
|
69
|
+
" !=",
|
|
70
|
+
" >=",
|
|
71
|
+
" <=",
|
|
72
|
+
" >",
|
|
73
|
+
" <",
|
|
74
|
+
" ="
|
|
75
|
+
];
|
|
76
|
+
const lower = trimmed.toLowerCase();
|
|
77
|
+
for (const operator of operators) if (lower.endsWith(operator)) return {
|
|
78
|
+
field: trimmed.slice(0, -operator.length).trim(),
|
|
79
|
+
suffix: operator
|
|
80
|
+
};
|
|
81
|
+
return {
|
|
82
|
+
field: trimmed,
|
|
83
|
+
suffix: ""
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function normalizeConditionKey(key, mapField) {
|
|
87
|
+
const { field, suffix } = parseWhereKey(key);
|
|
88
|
+
const mapped = mapField(field);
|
|
89
|
+
return suffix ? `${mapped}${suffix}` : mapped;
|
|
90
|
+
}
|
|
91
|
+
function normalizeWhere(where, mapField) {
|
|
92
|
+
if (!where) return void 0;
|
|
93
|
+
if (Array.isArray(where)) return where.map((andGroup) => andGroup.map((condition) => Object.fromEntries(Object.entries(condition).map(([key, value]) => [normalizeConditionKey(key, mapField), value]))));
|
|
94
|
+
return Object.fromEntries(Object.entries(where).map(([key, value]) => [normalizeConditionKey(key, mapField), value]));
|
|
95
|
+
}
|
|
96
|
+
function readReportMetadata(field) {
|
|
97
|
+
return field?._meta?.__report ?? field?.__report;
|
|
98
|
+
}
|
|
99
|
+
function isReportSystemField(fieldName, field) {
|
|
100
|
+
return REPORT_BASE_FIELDS.has(fieldName) || field?._meta?.__smrtSystemField || field?._meta?.__tenancy?.isTenantIdField;
|
|
101
|
+
}
|
|
102
|
+
async function resolveJoins(reportName, sourceName, joins) {
|
|
103
|
+
const resolved = [];
|
|
104
|
+
if (!joins) return resolved;
|
|
105
|
+
const sourceTenant = await tenantColumn(sourceName);
|
|
106
|
+
for (const [alias, join] of Object.entries(joins)) {
|
|
107
|
+
if (!JOIN_ALIAS.test(alias) || alias === "src" || alias.length > 32) throw new Error(`${reportName} join alias '${alias}' must be lower-case letters, digits and underscores, start with a letter, and not be 'src'`);
|
|
108
|
+
if (!join?.source || typeof join.on !== "string") throw new Error(`${reportName} join '${alias}' needs a source and an on field`);
|
|
109
|
+
if (join.type !== void 0 && join.type !== "inner" && join.type !== "left") throw new Error(`${reportName} join '${alias}' type must be 'inner' or 'left'`);
|
|
110
|
+
const className = sourceClassName(join.source);
|
|
111
|
+
const table = resolveTableName(className);
|
|
112
|
+
const onColumn = await fieldColumn(sourceName, join.on);
|
|
113
|
+
if (!onColumn) throw new Error(`${reportName} join '${alias}': ${sourceName} has no field '${join.on}'`);
|
|
114
|
+
const to = join.to ?? "id";
|
|
115
|
+
const targetColumn = to === "id" ? "id" : await fieldColumn(className, to);
|
|
116
|
+
if (!targetColumn) throw new Error(`${reportName} join '${alias}': ${className} has no field '${to}'`);
|
|
117
|
+
const joinedTenant = await tenantColumn(className);
|
|
118
|
+
if (joinedTenant && !sourceTenant) throw new Error(`${reportName} join '${alias}': ${className} is tenant-scoped but ${sourceName} has no tenant field to match it with`);
|
|
119
|
+
const softDelete = await fieldColumn(className, "deletedAt");
|
|
120
|
+
resolved.push({
|
|
121
|
+
alias,
|
|
122
|
+
className,
|
|
123
|
+
table,
|
|
124
|
+
type: join.type ?? "left",
|
|
125
|
+
sourceColumn: onColumn,
|
|
126
|
+
targetColumn,
|
|
127
|
+
...joinedTenant && sourceTenant ? { tenantColumns: {
|
|
128
|
+
source: sourceTenant,
|
|
129
|
+
joined: joinedTenant
|
|
130
|
+
} } : {},
|
|
131
|
+
...softDelete ? { softDeleteColumn: softDelete } : {}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return resolved;
|
|
135
|
+
}
|
|
136
|
+
function operandColumns(node, found = []) {
|
|
137
|
+
if (!node || typeof node !== "object") return found;
|
|
138
|
+
const record = node;
|
|
139
|
+
if (typeof record.col === "string") found.push(record.col);
|
|
140
|
+
for (const key of [
|
|
141
|
+
"left",
|
|
142
|
+
"right",
|
|
143
|
+
"later",
|
|
144
|
+
"earlier",
|
|
145
|
+
"operand"
|
|
146
|
+
]) operandColumns(record[key], found);
|
|
147
|
+
if (Array.isArray(record.operands)) for (const operand of record.operands) operandColumns(operand, found);
|
|
148
|
+
return found;
|
|
149
|
+
}
|
|
150
|
+
function referencedColumns(fieldName, report) {
|
|
151
|
+
if (report.kind === "group") return report.expr ? operandColumns(report.expr) : [report.sourceColumn ?? fieldName];
|
|
152
|
+
if (report.kind === "bucket") return [report.sourceColumn];
|
|
153
|
+
return [
|
|
154
|
+
...report.column ? [report.column] : [],
|
|
155
|
+
...operandColumns(report.expr),
|
|
156
|
+
...operandColumns(report.filter)
|
|
157
|
+
];
|
|
158
|
+
}
|
|
159
|
+
async function relatedPolicies(reportName, fieldName, report, joins) {
|
|
160
|
+
const policies = [];
|
|
161
|
+
for (const ref of referencedColumns(fieldName, report)) {
|
|
162
|
+
if (!ref.includes(".")) continue;
|
|
163
|
+
const parts = ref.split(".");
|
|
164
|
+
const join = parts.length === 2 ? joins.find((j) => j.alias === parts[0]) : void 0;
|
|
165
|
+
if (!join) throw new Error(`${reportName}.${fieldName} reads '${ref}', which is not a column of a declared join`);
|
|
166
|
+
const related = parts[1];
|
|
167
|
+
const registryFields = await ObjectRegistry.getAllFields(join.className);
|
|
168
|
+
const key = toSnakeCase(related);
|
|
169
|
+
const found = [...registryFields.entries()].find(([name]) => name === related || toSnakeCase(name) === key);
|
|
170
|
+
if (!found && key !== "id") throw new Error(`${reportName}.${fieldName} reads '${ref}', but ${join.className} has no field '${related}'`);
|
|
171
|
+
const policy = fieldPolicy(found?.[1]);
|
|
172
|
+
policies.push({
|
|
173
|
+
alias: join.alias,
|
|
174
|
+
fieldName: related,
|
|
175
|
+
sensitive: policy.sensitive,
|
|
176
|
+
...policy.readPermission ? { readPermission: policy.readPermission } : {}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return policies;
|
|
180
|
+
}
|
|
181
|
+
async function buildReportDefinition(reportCtor) {
|
|
182
|
+
const options = getReportOptions(reportCtor);
|
|
183
|
+
const reportClassName = getRegisteredClassName(reportCtor);
|
|
184
|
+
const sourceName = sourceClassName(options.source);
|
|
185
|
+
const sourceTable = resolveTableName(sourceName);
|
|
186
|
+
const fields = await ObjectRegistry.getAllFields(reportClassName);
|
|
187
|
+
const joins = await resolveJoins(reportCtor.name, sourceName, options.joins);
|
|
188
|
+
if (joins.length > 0 && options.refresh?.mode === "incremental") throw new Error(`${reportCtor.name} joins ${joins.map((join) => join.className).join(", ")} and cannot refresh incrementally: a change to a joined row does not move the source watermark. Use mode: 'rebuild'.`);
|
|
189
|
+
const reportFields = [];
|
|
190
|
+
for (const [fieldName, field] of fields.entries()) {
|
|
191
|
+
const report = readReportMetadata(field);
|
|
192
|
+
if (!report) {
|
|
193
|
+
if (!isReportSystemField(fieldName, field) && !field?.transient) throw new Error(`Report field ${reportCtor.name}.${fieldName} is neither a grouping key nor an aggregate. Add @groupBy(), a time-bucket decorator, or an aggregate decorator.`);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const related = await relatedPolicies(reportCtor.name, fieldName, report, joins);
|
|
197
|
+
reportFields.push({
|
|
198
|
+
fieldName,
|
|
199
|
+
columnName: toSnakeCase(fieldName),
|
|
200
|
+
type: field.type,
|
|
201
|
+
report,
|
|
202
|
+
...related.length > 0 ? { related } : {}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
reportClassName,
|
|
207
|
+
sourceClassName: sourceName,
|
|
208
|
+
sourceTable,
|
|
209
|
+
...joins.length > 0 ? { joins } : {},
|
|
210
|
+
fields: reportFields,
|
|
211
|
+
where: options.where,
|
|
212
|
+
having: options.having,
|
|
213
|
+
refresh: options.refresh
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function columnFor(ref, definition) {
|
|
217
|
+
const joins = definition.joins ?? [];
|
|
218
|
+
if (!ref.includes(".")) {
|
|
219
|
+
const column = toSnakeCase(ref);
|
|
220
|
+
return joins.length > 0 ? `src.${column}` : column;
|
|
221
|
+
}
|
|
222
|
+
const parts = ref.split(".");
|
|
223
|
+
if (parts.length !== 2 || !joins.some((join) => join.alias === parts[0])) throw new Error(`Report column '${ref}' is not a column of a declared join`);
|
|
224
|
+
return `${parts[0]}.${toSnakeCase(parts[1])}`;
|
|
225
|
+
}
|
|
226
|
+
function mapOperand(node, mapColumn) {
|
|
227
|
+
if (!node || typeof node !== "object") return node;
|
|
228
|
+
const record = node;
|
|
229
|
+
if (typeof record.col === "string") return { col: mapColumn(record.col) };
|
|
230
|
+
const out = { ...record };
|
|
231
|
+
for (const key of [
|
|
232
|
+
"left",
|
|
233
|
+
"right",
|
|
234
|
+
"later",
|
|
235
|
+
"earlier",
|
|
236
|
+
"operand"
|
|
237
|
+
]) if (record[key] !== void 0) out[key] = mapOperand(record[key], mapColumn);
|
|
238
|
+
if (Array.isArray(record.operands)) out.operands = record.operands.map((operand) => mapOperand(operand, mapColumn));
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
function sourceColumn(field, definition) {
|
|
242
|
+
const report = field.report;
|
|
243
|
+
if (!report) throw new Error(`Report field ${field.fieldName} is missing metadata`);
|
|
244
|
+
if (report.kind === "group") return columnFor(report.sourceColumn ?? field.fieldName, definition);
|
|
245
|
+
if (report.kind === "bucket") return columnFor(report.sourceColumn, definition);
|
|
246
|
+
if (report.kind === "aggregate") {
|
|
247
|
+
if (!report.column) return "";
|
|
248
|
+
return columnFor(report.column, definition);
|
|
249
|
+
}
|
|
250
|
+
return "";
|
|
251
|
+
}
|
|
252
|
+
function getReportGroupingColumns(definition) {
|
|
253
|
+
const columns = definition.fields.filter((field) => field.report?.kind === "group" || field.report?.kind === "bucket").map((field) => field.columnName ?? toSnakeCase(field.fieldName));
|
|
254
|
+
return columns.length > 0 ? columns : ["id"];
|
|
255
|
+
}
|
|
256
|
+
function joinCondition(join) {
|
|
257
|
+
const conditions = [{
|
|
258
|
+
op: "eq",
|
|
259
|
+
left: { col: `src.${join.sourceColumn}` },
|
|
260
|
+
right: { col: `${join.alias}.${join.targetColumn}` }
|
|
261
|
+
}];
|
|
262
|
+
if (join.tenantColumns) conditions.push({
|
|
263
|
+
op: "eq",
|
|
264
|
+
left: { col: `src.${join.tenantColumns.source}` },
|
|
265
|
+
right: { col: `${join.alias}.${join.tenantColumns.joined}` }
|
|
266
|
+
});
|
|
267
|
+
if (join.softDeleteColumn) conditions.push({
|
|
268
|
+
op: "isNull",
|
|
269
|
+
operand: { col: `${join.alias}.${join.softDeleteColumn}` }
|
|
270
|
+
});
|
|
271
|
+
return conditions.length === 1 ? conditions[0] : {
|
|
272
|
+
op: "and",
|
|
273
|
+
operands: conditions
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function compileReportDefinition(definition) {
|
|
277
|
+
const select = [];
|
|
278
|
+
const groupBy = [];
|
|
279
|
+
const reportFieldToColumn = /* @__PURE__ */ new Map();
|
|
280
|
+
const mapColumn = (ref) => columnFor(ref, definition);
|
|
281
|
+
for (const field of definition.fields) {
|
|
282
|
+
const outputColumn = field.columnName ?? toSnakeCase(field.fieldName);
|
|
283
|
+
reportFieldToColumn.set(field.fieldName, outputColumn);
|
|
284
|
+
reportFieldToColumn.set(outputColumn, outputColumn);
|
|
285
|
+
const report = field.report;
|
|
286
|
+
if (!report) continue;
|
|
287
|
+
if (report.kind === "group") {
|
|
288
|
+
select.push(report.expr ? {
|
|
289
|
+
expr: mapOperand(report.expr, mapColumn),
|
|
290
|
+
as: outputColumn
|
|
291
|
+
} : {
|
|
292
|
+
column: sourceColumn(field, definition),
|
|
293
|
+
as: outputColumn
|
|
294
|
+
});
|
|
295
|
+
groupBy.push(outputColumn);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (report.kind === "bucket") {
|
|
299
|
+
select.push({
|
|
300
|
+
bucket: report.unit,
|
|
301
|
+
column: sourceColumn(field, definition),
|
|
302
|
+
as: outputColumn
|
|
303
|
+
});
|
|
304
|
+
groupBy.push(outputColumn);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
select.push({
|
|
308
|
+
fn: report.fn,
|
|
309
|
+
column: report.column ? sourceColumn(field, definition) : void 0,
|
|
310
|
+
...report.expr ? { expr: mapOperand(report.expr, mapColumn) } : {},
|
|
311
|
+
...report.filter ? { filter: mapOperand(report.filter, mapColumn) } : {},
|
|
312
|
+
as: outputColumn,
|
|
313
|
+
distinct: report.distinct
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
if (select.length === 0) throw new Error(`${definition.reportClassName} has no report fields. Add @groupBy(), a time-bucket decorator, or an aggregate decorator.`);
|
|
317
|
+
const joins = definition.joins ?? [];
|
|
318
|
+
return {
|
|
319
|
+
from: definition.sourceTable,
|
|
320
|
+
...joins.length > 0 ? {
|
|
321
|
+
fromAlias: "src",
|
|
322
|
+
joins: joins.map((join) => ({
|
|
323
|
+
table: join.table,
|
|
324
|
+
alias: join.alias,
|
|
325
|
+
type: join.type,
|
|
326
|
+
on: joinCondition(join)
|
|
327
|
+
}))
|
|
328
|
+
} : {},
|
|
329
|
+
select,
|
|
330
|
+
groupBy,
|
|
331
|
+
where: normalizeWhere(definition.where, mapColumn),
|
|
332
|
+
having: normalizeWhere(definition.having, (field) => reportFieldToColumn.get(field) ?? toSnakeCase(field))
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
async function compileReportSpec(reportCtor) {
|
|
336
|
+
return compileReportDefinition(await buildReportDefinition(reportCtor));
|
|
337
|
+
}
|
|
338
|
+
//#endregion
|
|
339
|
+
export { getReportGroupingColumns as a, tenantColumn as c, compileReportSpec as i, buildReportDefinition as n, fieldColumn as o, compileReportDefinition as r, fieldPolicy as s, REPORT_SOURCE_ALIAS as t };
|
|
340
|
+
|
|
341
|
+
//# sourceMappingURL=compiler-IUB2YNgM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compiler-IUB2YNgM.js","names":[],"sources":["../../src/registry-columns.ts","../../src/compiler.ts"],"sourcesContent":["import { ObjectRegistry } from '@happyvertical/smrt-core';\nimport { toSnakeCase } from '@happyvertical/smrt-core/utils';\n\nexport async function fieldColumn(\n className: string,\n requested: string,\n): Promise<string | null> {\n const fields = await ObjectRegistry.getAllFields(className);\n const requestedColumn = toSnakeCase(requested);\n\n for (const [fieldName] of fields.entries()) {\n if (fieldName === requested || toSnakeCase(fieldName) === requestedColumn) {\n return requestedColumn;\n }\n }\n\n return null;\n}\n\nexport async function tenantColumn(className: string): Promise<string | null> {\n const registered = ObjectRegistry.getClass(className);\n const configuredField = registered?.tenantScopedConfig?.field;\n if (configuredField) {\n return toSnakeCase(configuredField);\n }\n\n const fields = await ObjectRegistry.getAllFields(className);\n for (const [fieldName, field] of fields.entries()) {\n if (field?._meta?.__tenancy?.isTenantIdField) {\n return toSnakeCase(fieldName);\n }\n }\n\n const tenantIdField = await fieldColumn(className, 'tenantId');\n return tenantIdField;\n}\n\n/** The registry field properties that decide who may read a column. */\nexport interface PolicyField {\n sensitive?: boolean;\n readPermission?: string;\n sensitivity?: unknown;\n _meta?: Record<string, unknown>;\n}\n\nexport function fieldPolicy(field: PolicyField | undefined): {\n sensitive: boolean;\n readPermission?: string;\n} {\n const meta = field?._meta ?? {};\n const sensitivity = [field?.sensitivity, meta.sensitivity].find(\n (value): value is string => typeof value === 'string',\n );\n return {\n sensitive:\n field?.sensitive === true ||\n meta.sensitive === true ||\n sensitivity === 'sensitive' ||\n sensitivity === 'secret',\n readPermission:\n typeof field?.readPermission === 'string'\n ? field.readPermission\n : typeof meta.readPermission === 'string'\n ? meta.readPermission\n : undefined,\n };\n}\n","import { ObjectRegistry, type SmrtObject } from '@happyvertical/smrt-core';\nimport { toSnakeCase } from '@happyvertical/smrt-core/utils';\nimport type { AggregateExpression, WhereClause } from '@happyvertical/sql';\nimport { getRuntimeReportOptions } from './decorators.js';\nimport { fieldColumn, fieldPolicy, tenantColumn } from './registry-columns.js';\nimport type {\n AggregateSelectExpr,\n AggregateSpec,\n ReportDefinition,\n ReportFieldDefinition,\n ReportFieldMetadata,\n ReportJoinDefinition,\n ReportOperand,\n ReportOptions,\n ReportRelatedFieldPolicy,\n ReportSource,\n} from './types.js';\n\n/** The report source's alias in a query that joins other tables. */\nexport const REPORT_SOURCE_ALIAS = 'src';\n\n/** Lower-case, so snake-casing a column reference leaves the alias alone. */\nconst JOIN_ALIAS = /^[a-z][a-z0-9_]*$/;\n\nconst REPORT_BASE_FIELDS = new Set([\n 'id',\n 'slug',\n 'context',\n 'created_at',\n 'updated_at',\n 'createdAt',\n 'updatedAt',\n 'refreshedAt',\n]);\n\nfunction isConstructor(\n value: ReportSource,\n): value is new (\n ...args: any[]\n) => SmrtObject {\n return typeof value === 'function';\n}\n\nfunction sourceClassName(source: ReportSource): string {\n if (typeof source === 'string') return source;\n return source.name;\n}\n\nfunction reportOptionsFromRegistry(\n reportCtor: new (...args: any[]) => SmrtObject,\n): ReportOptions | undefined {\n const registered =\n ObjectRegistry.getClassByConstructor(reportCtor) ??\n ObjectRegistry.getClass(reportCtor.name);\n return (registered?.config as { report?: ReportOptions } | undefined)?.report;\n}\n\nfunction getReportOptions(\n reportCtor: new (...args: any[]) => SmrtObject,\n): ReportOptions {\n const options =\n getRuntimeReportOptions(reportCtor) ??\n reportOptionsFromRegistry(reportCtor);\n if (!options?.source) {\n throw new Error(\n `${reportCtor.name} is missing @report({ source }) metadata. ` +\n `Decorate the report class with @report({ source: SourceClass }).`,\n );\n }\n return options;\n}\n\nfunction getRegisteredClassName(\n ctor: new (...args: any[]) => SmrtObject,\n): string {\n const registered =\n ObjectRegistry.getClassByConstructor(ctor) ??\n ObjectRegistry.getClass(ctor.name);\n return registered?.qualifiedName ?? registered?.name ?? ctor.name;\n}\n\nfunction resolveTableName(className: string): string {\n const tableName = ObjectRegistry.getTableName(className);\n if (!tableName) {\n throw new Error(\n `No SMRT table registered for ${className}. ` +\n `Ensure the class is decorated with @smrt() and its manifest is loaded.`,\n );\n }\n return tableName;\n}\n\nfunction parseWhereKey(key: string): { field: string; suffix: string } {\n const trimmed = key.trim();\n const operators = [\n ' is not null',\n ' is null',\n ' not in',\n ' like',\n ' in',\n ' !=',\n ' >=',\n ' <=',\n ' >',\n ' <',\n ' =',\n ];\n const lower = trimmed.toLowerCase();\n for (const operator of operators) {\n if (lower.endsWith(operator)) {\n return {\n field: trimmed.slice(0, -operator.length).trim(),\n suffix: operator,\n };\n }\n }\n return { field: trimmed, suffix: '' };\n}\n\nfunction normalizeConditionKey(\n key: string,\n mapField: (field: string) => string,\n): string {\n const { field, suffix } = parseWhereKey(key);\n const mapped = mapField(field);\n return suffix ? `${mapped}${suffix}` : mapped;\n}\n\nfunction normalizeWhere(\n where: WhereClause | undefined,\n mapField: (field: string) => string,\n): WhereClause | undefined {\n if (!where) return undefined;\n\n if (Array.isArray(where)) {\n return where.map((andGroup) =>\n andGroup.map((condition: Record<string, any>) =>\n Object.fromEntries(\n Object.entries(condition).map(([key, value]) => [\n normalizeConditionKey(key, mapField),\n value,\n ]),\n ),\n ),\n );\n }\n\n return Object.fromEntries(\n Object.entries(where).map(([key, value]) => [\n normalizeConditionKey(key, mapField),\n value,\n ]),\n );\n}\n\nfunction readReportMetadata(field: any): ReportFieldMetadata | undefined {\n return field?._meta?.__report ?? field?.__report;\n}\n\nfunction isReportSystemField(fieldName: string, field: any): boolean {\n return (\n REPORT_BASE_FIELDS.has(fieldName) ||\n field?._meta?.__smrtSystemField ||\n field?._meta?.__tenancy?.isTenantIdField\n );\n}\n\nasync function resolveJoins(\n reportName: string,\n sourceName: string,\n joins: ReportOptions['joins'],\n): Promise<ReportJoinDefinition[]> {\n const resolved: ReportJoinDefinition[] = [];\n if (!joins) return resolved;\n const sourceTenant = await tenantColumn(sourceName);\n\n for (const [alias, join] of Object.entries(joins)) {\n if (\n !JOIN_ALIAS.test(alias) ||\n alias === REPORT_SOURCE_ALIAS ||\n alias.length > 32\n ) {\n throw new Error(\n `${reportName} join alias '${alias}' must be lower-case letters, digits and underscores, start with a letter, and not be '${REPORT_SOURCE_ALIAS}'`,\n );\n }\n if (!join?.source || typeof join.on !== 'string') {\n throw new Error(\n `${reportName} join '${alias}' needs a source and an on field`,\n );\n }\n if (\n join.type !== undefined &&\n join.type !== 'inner' &&\n join.type !== 'left'\n ) {\n throw new Error(\n `${reportName} join '${alias}' type must be 'inner' or 'left'`,\n );\n }\n const className = sourceClassName(join.source);\n const table = resolveTableName(className);\n const onColumn = await fieldColumn(sourceName, join.on);\n if (!onColumn) {\n throw new Error(\n `${reportName} join '${alias}': ${sourceName} has no field '${join.on}'`,\n );\n }\n const to = join.to ?? 'id';\n const targetColumn = to === 'id' ? 'id' : await fieldColumn(className, to);\n if (!targetColumn) {\n throw new Error(\n `${reportName} join '${alias}': ${className} has no field '${to}'`,\n );\n }\n\n // A tenant-scoped related model is matched within the source row's tenant,\n // so ids that collide across tenants never join. A source with no tenant\n // cannot say which tenant's related rows to match.\n const joinedTenant = await tenantColumn(className);\n if (joinedTenant && !sourceTenant) {\n throw new Error(\n `${reportName} join '${alias}': ${className} is tenant-scoped but ${sourceName} has no tenant field to match it with`,\n );\n }\n const softDelete = await fieldColumn(className, 'deletedAt');\n resolved.push({\n alias,\n className,\n table,\n type: join.type ?? 'left',\n sourceColumn: onColumn,\n targetColumn,\n ...(joinedTenant && sourceTenant\n ? { tenantColumns: { source: sourceTenant, joined: joinedTenant } }\n : {}),\n ...(softDelete ? { softDeleteColumn: softDelete } : {}),\n });\n }\n return resolved;\n}\n\nfunction operandColumns(node: unknown, found: string[] = []): string[] {\n if (!node || typeof node !== 'object') return found;\n const record = node as Record<string, unknown>;\n if (typeof record.col === 'string') found.push(record.col);\n for (const key of ['left', 'right', 'later', 'earlier', 'operand']) {\n operandColumns(record[key], found);\n }\n if (Array.isArray(record.operands)) {\n for (const operand of record.operands) operandColumns(operand, found);\n }\n return found;\n}\n\n/** Every model field a report field reads, as written (`status`, `wo.title`). */\nfunction referencedColumns(\n fieldName: string,\n report: ReportFieldMetadata,\n): string[] {\n if (report.kind === 'group') {\n return report.expr\n ? operandColumns(report.expr)\n : [report.sourceColumn ?? fieldName];\n }\n if (report.kind === 'bucket') return [report.sourceColumn];\n return [\n ...(report.column ? [report.column] : []),\n ...operandColumns(report.expr),\n ...operandColumns(report.filter),\n ];\n}\n\nasync function relatedPolicies(\n reportName: string,\n fieldName: string,\n report: ReportFieldMetadata,\n joins: ReportJoinDefinition[],\n): Promise<ReportRelatedFieldPolicy[]> {\n const policies: ReportRelatedFieldPolicy[] = [];\n for (const ref of referencedColumns(fieldName, report)) {\n if (!ref.includes('.')) continue;\n const parts = ref.split('.');\n const join =\n parts.length === 2 ? joins.find((j) => j.alias === parts[0]) : undefined;\n if (!join) {\n throw new Error(\n `${reportName}.${fieldName} reads '${ref}', which is not a column of a declared join`,\n );\n }\n const related = parts[1];\n const registryFields = await ObjectRegistry.getAllFields(join.className);\n const key = toSnakeCase(related);\n const found = [...registryFields.entries()].find(\n ([name]) => name === related || toSnakeCase(name) === key,\n );\n if (!found && key !== 'id') {\n throw new Error(\n `${reportName}.${fieldName} reads '${ref}', but ${join.className} has no field '${related}'`,\n );\n }\n const policy = fieldPolicy(found?.[1] as Parameters<typeof fieldPolicy>[0]);\n policies.push({\n alias: join.alias,\n fieldName: related,\n sensitive: policy.sensitive,\n ...(policy.readPermission\n ? { readPermission: policy.readPermission }\n : {}),\n });\n }\n return policies;\n}\n\nexport async function buildReportDefinition(\n reportCtor: new (...args: any[]) => SmrtObject,\n): Promise<ReportDefinition> {\n const options = getReportOptions(reportCtor);\n const reportClassName = getRegisteredClassName(reportCtor);\n const sourceName = sourceClassName(options.source);\n const sourceTable = resolveTableName(sourceName);\n const fields = await ObjectRegistry.getAllFields(reportClassName);\n const joins = await resolveJoins(reportCtor.name, sourceName, options.joins);\n if (joins.length > 0 && options.refresh?.mode === 'incremental') {\n throw new Error(\n `${reportCtor.name} joins ${joins.map((join) => join.className).join(', ')} and cannot refresh incrementally: ` +\n `a change to a joined row does not move the source watermark. Use mode: 'rebuild'.`,\n );\n }\n\n const reportFields: ReportFieldDefinition[] = [];\n for (const [fieldName, field] of fields.entries()) {\n const report = readReportMetadata(field);\n if (!report) {\n if (!isReportSystemField(fieldName, field) && !field?.transient) {\n throw new Error(\n `Report field ${reportCtor.name}.${fieldName} is neither a grouping key nor an aggregate. ` +\n `Add @groupBy(), a time-bucket decorator, or an aggregate decorator.`,\n );\n }\n continue;\n }\n\n const related = await relatedPolicies(\n reportCtor.name,\n fieldName,\n report,\n joins,\n );\n reportFields.push({\n fieldName,\n columnName: toSnakeCase(fieldName),\n type: field.type,\n report,\n ...(related.length > 0 ? { related } : {}),\n });\n }\n\n return {\n reportClassName,\n sourceClassName: sourceName,\n sourceTable,\n ...(joins.length > 0 ? { joins } : {}),\n fields: reportFields,\n where: options.where,\n having: options.having,\n refresh: options.refresh,\n };\n}\n\n/**\n * A model field as a query column. Without joins it is the bare column, as\n * before. With joins every column is qualified: a bare field by the source\n * alias, `alias.field` by its join, so a column the source and a joined table\n * share (`tenant_id`, `deleted_at`) is never ambiguous.\n */\nfunction columnFor(\n ref: string,\n definition: Pick<ReportDefinition, 'joins'>,\n): string {\n const joins = definition.joins ?? [];\n if (!ref.includes('.')) {\n const column = toSnakeCase(ref);\n return joins.length > 0 ? `${REPORT_SOURCE_ALIAS}.${column}` : column;\n }\n const parts = ref.split('.');\n if (parts.length !== 2 || !joins.some((join) => join.alias === parts[0])) {\n throw new Error(\n `Report column '${ref}' is not a column of a declared join`,\n );\n }\n return `${parts[0]}.${toSnakeCase(parts[1])}`;\n}\n\nfunction mapOperand(\n node: ReportOperand,\n mapColumn: (ref: string) => string,\n): ReportOperand {\n if (!node || typeof node !== 'object') return node;\n const record = node as Record<string, unknown>;\n if (typeof record.col === 'string') return { col: mapColumn(record.col) };\n const out: Record<string, unknown> = { ...record };\n for (const key of ['left', 'right', 'later', 'earlier', 'operand']) {\n if (record[key] !== undefined) {\n out[key] = mapOperand(record[key] as ReportOperand, mapColumn);\n }\n }\n if (Array.isArray(record.operands)) {\n out.operands = record.operands.map((operand) =>\n mapOperand(operand as ReportOperand, mapColumn),\n );\n }\n return out as ReportOperand;\n}\n\nfunction sourceColumn(\n field: ReportFieldDefinition,\n definition: Pick<ReportDefinition, 'joins'>,\n): string {\n const report = field.report;\n if (!report) {\n throw new Error(`Report field ${field.fieldName} is missing metadata`);\n }\n if (report.kind === 'group') {\n return columnFor(report.sourceColumn ?? field.fieldName, definition);\n }\n if (report.kind === 'bucket') {\n return columnFor(report.sourceColumn, definition);\n }\n if (report.kind === 'aggregate') {\n if (!report.column) return '';\n return columnFor(report.column, definition);\n }\n return '';\n}\n\nexport function getReportGroupingColumns(\n definition: Pick<ReportDefinition, 'fields'>,\n): string[] {\n const columns = definition.fields\n .filter(\n (field) =>\n field.report?.kind === 'group' || field.report?.kind === 'bucket',\n )\n .map((field) => field.columnName ?? toSnakeCase(field.fieldName));\n return columns.length > 0 ? columns : ['id'];\n}\n\n/** The join condition: the key, the tenant match, and no soft-deleted related row. */\nfunction joinCondition(join: ReportJoinDefinition): AggregateExpression {\n const conditions: AggregateExpression[] = [\n {\n op: 'eq',\n left: { col: `${REPORT_SOURCE_ALIAS}.${join.sourceColumn}` },\n right: { col: `${join.alias}.${join.targetColumn}` },\n },\n ];\n if (join.tenantColumns) {\n conditions.push({\n op: 'eq',\n left: { col: `${REPORT_SOURCE_ALIAS}.${join.tenantColumns.source}` },\n right: { col: `${join.alias}.${join.tenantColumns.joined}` },\n });\n }\n if (join.softDeleteColumn) {\n conditions.push({\n op: 'isNull',\n operand: { col: `${join.alias}.${join.softDeleteColumn}` },\n });\n }\n return conditions.length === 1\n ? conditions[0]\n : { op: 'and', operands: conditions };\n}\n\nexport function compileReportDefinition(\n definition: ReportDefinition,\n): AggregateSpec {\n const select: AggregateSelectExpr[] = [];\n const groupBy: string[] = [];\n const reportFieldToColumn = new Map<string, string>();\n const mapColumn = (ref: string) => columnFor(ref, definition);\n\n for (const field of definition.fields) {\n const outputColumn = field.columnName ?? toSnakeCase(field.fieldName);\n reportFieldToColumn.set(field.fieldName, outputColumn);\n reportFieldToColumn.set(outputColumn, outputColumn);\n\n const report = field.report;\n if (!report) continue;\n\n if (report.kind === 'group') {\n select.push(\n report.expr\n ? { expr: mapOperand(report.expr, mapColumn), as: outputColumn }\n : { column: sourceColumn(field, definition), as: outputColumn },\n );\n groupBy.push(outputColumn);\n continue;\n }\n\n if (report.kind === 'bucket') {\n select.push({\n bucket: report.unit,\n column: sourceColumn(field, definition),\n as: outputColumn,\n });\n groupBy.push(outputColumn);\n continue;\n }\n\n select.push({\n fn: report.fn,\n column: report.column ? sourceColumn(field, definition) : undefined,\n ...(report.expr ? { expr: mapOperand(report.expr, mapColumn) } : {}),\n ...(report.filter\n ? { filter: mapOperand(report.filter, mapColumn) }\n : {}),\n as: outputColumn,\n distinct: report.distinct,\n });\n }\n\n if (select.length === 0) {\n throw new Error(\n `${definition.reportClassName} has no report fields. ` +\n `Add @groupBy(), a time-bucket decorator, or an aggregate decorator.`,\n );\n }\n\n const joins = definition.joins ?? [];\n return {\n from: definition.sourceTable,\n ...(joins.length > 0\n ? {\n fromAlias: REPORT_SOURCE_ALIAS,\n joins: joins.map((join) => ({\n table: join.table,\n alias: join.alias,\n type: join.type,\n on: joinCondition(join),\n })),\n }\n : {}),\n select,\n groupBy,\n where: normalizeWhere(definition.where, mapColumn),\n having: normalizeWhere(\n definition.having,\n (field) => reportFieldToColumn.get(field) ?? toSnakeCase(field),\n ),\n };\n}\n\nexport async function compileReportSpec(\n reportCtor: new (...args: any[]) => SmrtObject,\n): Promise<AggregateSpec> {\n return compileReportDefinition(await buildReportDefinition(reportCtor));\n}\n"],"mappings":";;;;AAGA,eAAsB,YACpB,WACA,WACwB;CACxB,MAAM,SAAS,MAAM,eAAe,aAAa,SAAS;CAC1D,MAAM,kBAAkB,YAAY,SAAS;CAE7C,KAAA,MAAW,CAAC,cAAc,OAAO,QAAQ,GACvC,IAAI,cAAc,aAAa,YAAY,SAAS,MAAM,iBACxD,OAAO;CAIX,OAAO;AACT;AAEA,eAAsB,aAAa,WAA2C;CAE5E,MAAM,kBADa,eAAe,SAAS,SACnB,CAAA,EAAY,oBAAoB;CACxD,IAAI,iBACF,OAAO,YAAY,eAAe;CAGpC,MAAM,SAAS,MAAM,eAAe,aAAa,SAAS;CAC1D,KAAA,MAAW,CAAC,WAAW,UAAU,OAAO,QAAQ,GAC9C,IAAI,OAAO,OAAO,WAAW,iBAC3B,OAAO,YAAY,SAAS;CAKhC,OAAO,MADqB,YAAY,WAAW,UAAU;AAE/D;AAUO,SAAS,YAAY,OAG1B;CACA,MAAM,OAAO,OAAO,SAAS,CAAC;CAC9B,MAAM,cAAc,CAAC,OAAO,aAAa,KAAK,WAAW,CAAA,CAAE,MACxD,UAA2B,OAAO,UAAU,QAC/C;CACA,OAAO;EACL,WACE,OAAO,cAAc,QACrB,KAAK,cAAc,QACnB,gBAAgB,eAChB,gBAAgB;EAClB,gBACE,OAAO,OAAO,mBAAmB,WAC7B,MAAM,iBACN,OAAO,KAAK,mBAAmB,WAC7B,KAAK,iBACL,KAAA;CACV;AACF;;;AC/CO,IAAM,sBAAsB;AAGnC,IAAM,aAAa;AAEnB,IAAM,qCAAqB,IAAI,IAAI;CACjC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAUD,SAAS,gBAAgB,QAA8B;CACrD,IAAI,OAAO,WAAW,UAAU,OAAO;CACvC,OAAO,OAAO;AAChB;AAEA,SAAS,0BACP,YAC2B;CAI3B,QAFE,eAAe,sBAAsB,UAAU,KAC/C,eAAe,SAAS,WAAW,IAAI,EAAA,EACrB,QAAmD;AACzE;AAEA,SAAS,iBACP,YACe;CACf,MAAM,UACJ,wBAAwB,UAAU,KAClC,0BAA0B,UAAU;CACtC,IAAI,CAAC,SAAS,QACZ,MAAM,IAAI,MACR,GAAG,WAAW,KAAI,2GAEpB;CAEF,OAAO;AACT;AAEA,SAAS,uBACP,MACQ;CACR,MAAM,aACJ,eAAe,sBAAsB,IAAI,KACzC,eAAe,SAAS,KAAK,IAAI;CACnC,OAAO,YAAY,iBAAiB,YAAY,QAAQ,KAAK;AAC/D;AAEA,SAAS,iBAAiB,WAA2B;CACnD,MAAM,YAAY,eAAe,aAAa,SAAS;CACvD,IAAI,CAAC,WACH,MAAM,IAAI,MACR,gCAAgC,UAAS,yEAE3C;CAEF,OAAO;AACT;AAEA,SAAS,cAAc,KAAgD;CACrE,MAAM,UAAU,IAAI,KAAK;CACzB,MAAM,YAAY;EAChB;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;CACA,MAAM,QAAQ,QAAQ,YAAY;CAClC,KAAA,MAAW,YAAY,WACrB,IAAI,MAAM,SAAS,QAAQ,GACzB,OAAO;EACL,OAAO,QAAQ,MAAM,GAAG,CAAC,SAAS,MAAM,CAAA,CAAE,KAAK;EAC/C,QAAQ;CACV;CAGJ,OAAO;EAAE,OAAO;EAAS,QAAQ;CAAG;AACtC;AAEA,SAAS,sBACP,KACA,UACQ;CACR,MAAM,EAAE,OAAO,WAAW,cAAc,GAAG;CAC3C,MAAM,SAAS,SAAS,KAAK;CAC7B,OAAO,SAAS,GAAG,SAAS,WAAW;AACzC;AAEA,SAAS,eACP,OACA,UACyB;CACzB,IAAI,CAAC,OAAO,OAAO,KAAA;CAEnB,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,KAAK,aAChB,SAAS,KAAK,cACZ,OAAO,YACL,OAAO,QAAQ,SAAS,CAAA,CAAE,KAAK,CAAC,KAAK,WAAW,CAC9C,sBAAsB,KAAK,QAAQ,GACnC,KACF,CAAC,CACH,CACF,CACF;CAGF,OAAO,OAAO,YACZ,OAAO,QAAQ,KAAK,CAAA,CAAE,KAAK,CAAC,KAAK,WAAW,CAC1C,sBAAsB,KAAK,QAAQ,GACnC,KACF,CAAC,CACH;AACF;AAEA,SAAS,mBAAmB,OAA6C;CACvE,OAAO,OAAO,OAAO,YAAY,OAAO;AAC1C;AAEA,SAAS,oBAAoB,WAAmB,OAAqB;CACnE,OACE,mBAAmB,IAAI,SAAS,KAChC,OAAO,OAAO,qBACd,OAAO,OAAO,WAAW;AAE7B;AAEA,eAAe,aACb,YACA,YACA,OACiC;CACjC,MAAM,WAAmC,CAAC;CAC1C,IAAI,CAAC,OAAO,OAAO;CACnB,MAAM,eAAe,MAAM,aAAa,UAAU;CAElD,KAAA,MAAW,CAAC,OAAO,SAAS,OAAO,QAAQ,KAAK,GAAG;EACjD,IACE,CAAC,WAAW,KAAK,KAAK,KACtB,UAAA,SACA,MAAM,SAAS,IAEf,MAAM,IAAI,MACR,GAAG,WAAU,eAAgB,MAAK,4FACpC;EAEF,IAAI,CAAC,MAAM,UAAU,OAAO,KAAK,OAAO,UACtC,MAAM,IAAI,MACR,GAAG,WAAU,SAAU,MAAK,iCAC9B;EAEF,IACE,KAAK,SAAS,KAAA,KACd,KAAK,SAAS,WACd,KAAK,SAAS,QAEd,MAAM,IAAI,MACR,GAAG,WAAU,SAAU,MAAK,iCAC9B;EAEF,MAAM,YAAY,gBAAgB,KAAK,MAAM;EAC7C,MAAM,QAAQ,iBAAiB,SAAS;EACxC,MAAM,WAAW,MAAM,YAAY,YAAY,KAAK,EAAE;EACtD,IAAI,CAAC,UACH,MAAM,IAAI,MACR,GAAG,WAAU,SAAU,MAAK,KAAM,WAAU,iBAAkB,KAAK,GAAE,EACvE;EAEF,MAAM,KAAK,KAAK,MAAM;EACtB,MAAM,eAAe,OAAO,OAAO,OAAO,MAAM,YAAY,WAAW,EAAE;EACzE,IAAI,CAAC,cACH,MAAM,IAAI,MACR,GAAG,WAAU,SAAU,MAAK,KAAM,UAAS,iBAAkB,GAAE,EACjE;EAMF,MAAM,eAAe,MAAM,aAAa,SAAS;EACjD,IAAI,gBAAgB,CAAC,cACnB,MAAM,IAAI,MACR,GAAG,WAAU,SAAU,MAAK,KAAM,UAAS,wBAAyB,WAAU,sCAChF;EAEF,MAAM,aAAa,MAAM,YAAY,WAAW,WAAW;EAC3D,SAAS,KAAK;GACZ;GACA;GACA;GACA,MAAM,KAAK,QAAQ;GACnB,cAAc;GACd;GACA,GAAI,gBAAgB,eAChB,EAAE,eAAe;IAAE,QAAQ;IAAc,QAAQ;GAAa,EAAE,IAChE,CAAC;GACL,GAAI,aAAa,EAAE,kBAAkB,WAAW,IAAI,CAAC;EACvD,CAAC;CACH;CACA,OAAO;AACT;AAEA,SAAS,eAAe,MAAe,QAAkB,CAAC,GAAa;CACrE,IAAI,CAAC,QAAQ,OAAO,SAAS,UAAU,OAAO;CAC9C,MAAM,SAAS;CACf,IAAI,OAAO,OAAO,QAAQ,UAAU,MAAM,KAAK,OAAO,GAAG;CACzD,KAAA,MAAW,OAAO;EAAC;EAAQ;EAAS;EAAS;EAAW;CAAS,GAC/D,eAAe,OAAO,MAAM,KAAK;CAEnC,IAAI,MAAM,QAAQ,OAAO,QAAQ,GAC/B,KAAA,MAAW,WAAW,OAAO,UAAU,eAAe,SAAS,KAAK;CAEtE,OAAO;AACT;AAGA,SAAS,kBACP,WACA,QACU;CACV,IAAI,OAAO,SAAS,SAClB,OAAO,OAAO,OACV,eAAe,OAAO,IAAI,IAC1B,CAAC,OAAO,gBAAgB,SAAS;CAEvC,IAAI,OAAO,SAAS,UAAU,OAAO,CAAC,OAAO,YAAY;CACzD,OAAO;EACL,GAAI,OAAO,SAAS,CAAC,OAAO,MAAM,IAAI,CAAC;EACvC,GAAG,eAAe,OAAO,IAAI;EAC7B,GAAG,eAAe,OAAO,MAAM;CACjC;AACF;AAEA,eAAe,gBACb,YACA,WACA,QACA,OACqC;CACrC,MAAM,WAAuC,CAAC;CAC9C,KAAA,MAAW,OAAO,kBAAkB,WAAW,MAAM,GAAG;EACtD,IAAI,CAAC,IAAI,SAAS,GAAG,GAAG;EACxB,MAAM,QAAQ,IAAI,MAAM,GAAG;EAC3B,MAAM,OACJ,MAAM,WAAW,IAAI,MAAM,MAAM,MAAM,EAAE,UAAU,MAAM,EAAE,IAAI,KAAA;EACjE,IAAI,CAAC,MACH,MAAM,IAAI,MACR,GAAG,WAAU,GAAI,UAAS,UAAW,IAAG,4CAC1C;EAEF,MAAM,UAAU,MAAM;EACtB,MAAM,iBAAiB,MAAM,eAAe,aAAa,KAAK,SAAS;EACvE,MAAM,MAAM,YAAY,OAAO;EAC/B,MAAM,QAAQ,CAAC,GAAG,eAAe,QAAQ,CAAC,CAAA,CAAE,MACzC,CAAC,UAAU,SAAS,WAAW,YAAY,IAAI,MAAM,GACxD;EACA,IAAI,CAAC,SAAS,QAAQ,MACpB,MAAM,IAAI,MACR,GAAG,WAAU,GAAI,UAAS,UAAW,IAAG,SAAU,KAAK,UAAS,iBAAkB,QAAO,EAC3F;EAEF,MAAM,SAAS,YAAY,QAAQ,EAAuC;EAC1E,SAAS,KAAK;GACZ,OAAO,KAAK;GACZ,WAAW;GACX,WAAW,OAAO;GAClB,GAAI,OAAO,iBACP,EAAE,gBAAgB,OAAO,eAAe,IACxC,CAAC;EACP,CAAC;CACH;CACA,OAAO;AACT;AAEA,eAAsB,sBACpB,YAC2B;CAC3B,MAAM,UAAU,iBAAiB,UAAU;CAC3C,MAAM,kBAAkB,uBAAuB,UAAU;CACzD,MAAM,aAAa,gBAAgB,QAAQ,MAAM;CACjD,MAAM,cAAc,iBAAiB,UAAU;CAC/C,MAAM,SAAS,MAAM,eAAe,aAAa,eAAe;CAChE,MAAM,QAAQ,MAAM,aAAa,WAAW,MAAM,YAAY,QAAQ,KAAK;CAC3E,IAAI,MAAM,SAAS,KAAK,QAAQ,SAAS,SAAS,eAChD,MAAM,IAAI,MACR,GAAG,WAAW,KAAI,SAAU,MAAM,KAAK,SAAS,KAAK,SAAS,CAAA,CAAE,KAAK,IAAI,EAAC,qHAE5E;CAGF,MAAM,eAAwC,CAAC;CAC/C,KAAA,MAAW,CAAC,WAAW,UAAU,OAAO,QAAQ,GAAG;EACjD,MAAM,SAAS,mBAAmB,KAAK;EACvC,IAAI,CAAC,QAAQ;GACX,IAAI,CAAC,oBAAoB,WAAW,KAAK,KAAK,CAAC,OAAO,WACpD,MAAM,IAAI,MACR,gBAAgB,WAAW,KAAI,GAAI,UAAS,iHAE9C;GAEF;EACF;EAEA,MAAM,UAAU,MAAM,gBACpB,WAAW,MACX,WACA,QACA,KACF;EACA,aAAa,KAAK;GAChB;GACA,YAAY,YAAY,SAAS;GACjC,MAAM,MAAM;GACZ;GACA,GAAI,QAAQ,SAAS,IAAI,EAAE,QAAQ,IAAI,CAAC;EAC1C,CAAC;CACH;CAEA,OAAO;EACL;EACA,iBAAiB;EACjB;EACA,GAAI,MAAM,SAAS,IAAI,EAAE,MAAM,IAAI,CAAC;EACpC,QAAQ;EACR,OAAO,QAAQ;EACf,QAAQ,QAAQ;EAChB,SAAS,QAAQ;CACnB;AACF;AAQA,SAAS,UACP,KACA,YACQ;CACR,MAAM,QAAQ,WAAW,SAAS,CAAC;CACnC,IAAI,CAAC,IAAI,SAAS,GAAG,GAAG;EACtB,MAAM,SAAS,YAAY,GAAG;EAC9B,OAAO,MAAM,SAAS,IAAI,OAA0B,WAAW;CACjE;CACA,MAAM,QAAQ,IAAI,MAAM,GAAG;CAC3B,IAAI,MAAM,WAAW,KAAK,CAAC,MAAM,MAAM,SAAS,KAAK,UAAU,MAAM,EAAE,GACrE,MAAM,IAAI,MACR,kBAAkB,IAAG,qCACvB;CAEF,OAAO,GAAG,MAAM,GAAE,GAAI,YAAY,MAAM,EAAE;AAC5C;AAEA,SAAS,WACP,MACA,WACe;CACf,IAAI,CAAC,QAAQ,OAAO,SAAS,UAAU,OAAO;CAC9C,MAAM,SAAS;CACf,IAAI,OAAO,OAAO,QAAQ,UAAU,OAAO,EAAE,KAAK,UAAU,OAAO,GAAG,EAAE;CACxE,MAAM,MAA+B,EAAE,GAAG,OAAO;CACjD,KAAA,MAAW,OAAO;EAAC;EAAQ;EAAS;EAAS;EAAW;CAAS,GAC/D,IAAI,OAAO,SAAS,KAAA,GAClB,IAAI,OAAO,WAAW,OAAO,MAAuB,SAAS;CAGjE,IAAI,MAAM,QAAQ,OAAO,QAAQ,GAC/B,IAAI,WAAW,OAAO,SAAS,KAAK,YAClC,WAAW,SAA0B,SAAS,CAChD;CAEF,OAAO;AACT;AAEA,SAAS,aACP,OACA,YACQ;CACR,MAAM,SAAS,MAAM;CACrB,IAAI,CAAC,QACH,MAAM,IAAI,MAAM,gBAAgB,MAAM,UAAS,qBAAsB;CAEvE,IAAI,OAAO,SAAS,SAClB,OAAO,UAAU,OAAO,gBAAgB,MAAM,WAAW,UAAU;CAErE,IAAI,OAAO,SAAS,UAClB,OAAO,UAAU,OAAO,cAAc,UAAU;CAElD,IAAI,OAAO,SAAS,aAAa;EAC/B,IAAI,CAAC,OAAO,QAAQ,OAAO;EAC3B,OAAO,UAAU,OAAO,QAAQ,UAAU;CAC5C;CACA,OAAO;AACT;AAEO,SAAS,yBACd,YACU;CACV,MAAM,UAAU,WAAW,OACxB,QACE,UACC,MAAM,QAAQ,SAAS,WAAW,MAAM,QAAQ,SAAS,QAC7D,CAAA,CACC,KAAK,UAAU,MAAM,cAAc,YAAY,MAAM,SAAS,CAAC;CAClE,OAAO,QAAQ,SAAS,IAAI,UAAU,CAAC,IAAI;AAC7C;AAGA,SAAS,cAAc,MAAiD;CACtE,MAAM,aAAoC,CACxC;EACE,IAAI;EACJ,MAAM,EAAE,KAAK,OAA0B,KAAK,eAAe;EAC3D,OAAO,EAAE,KAAK,GAAG,KAAK,MAAK,GAAI,KAAK,eAAe;CACrD,CACF;CACA,IAAI,KAAK,eACP,WAAW,KAAK;EACd,IAAI;EACJ,MAAM,EAAE,KAAK,OAA0B,KAAK,cAAc,SAAS;EACnE,OAAO,EAAE,KAAK,GAAG,KAAK,MAAK,GAAI,KAAK,cAAc,SAAS;CAC7D,CAAC;CAEH,IAAI,KAAK,kBACP,WAAW,KAAK;EACd,IAAI;EACJ,SAAS,EAAE,KAAK,GAAG,KAAK,MAAK,GAAI,KAAK,mBAAmB;CAC3D,CAAC;CAEH,OAAO,WAAW,WAAW,IACzB,WAAW,KACX;EAAE,IAAI;EAAO,UAAU;CAAW;AACxC;AAEO,SAAS,wBACd,YACe;CACf,MAAM,SAAgC,CAAC;CACvC,MAAM,UAAoB,CAAC;CAC3B,MAAM,sCAAsB,IAAI,IAAoB;CACpD,MAAM,aAAa,QAAgB,UAAU,KAAK,UAAU;CAE5D,KAAA,MAAW,SAAS,WAAW,QAAQ;EACrC,MAAM,eAAe,MAAM,cAAc,YAAY,MAAM,SAAS;EACpE,oBAAoB,IAAI,MAAM,WAAW,YAAY;EACrD,oBAAoB,IAAI,cAAc,YAAY;EAElD,MAAM,SAAS,MAAM;EACrB,IAAI,CAAC,QAAQ;EAEb,IAAI,OAAO,SAAS,SAAS;GAC3B,OAAO,KACL,OAAO,OACH;IAAE,MAAM,WAAW,OAAO,MAAM,SAAS;IAAG,IAAI;GAAa,IAC7D;IAAE,QAAQ,aAAa,OAAO,UAAU;IAAG,IAAI;GAAa,CAClE;GACA,QAAQ,KAAK,YAAY;GACzB;EACF;EAEA,IAAI,OAAO,SAAS,UAAU;GAC5B,OAAO,KAAK;IACV,QAAQ,OAAO;IACf,QAAQ,aAAa,OAAO,UAAU;IACtC,IAAI;GACN,CAAC;GACD,QAAQ,KAAK,YAAY;GACzB;EACF;EAEA,OAAO,KAAK;GACV,IAAI,OAAO;GACX,QAAQ,OAAO,SAAS,aAAa,OAAO,UAAU,IAAI,KAAA;GAC1D,GAAI,OAAO,OAAO,EAAE,MAAM,WAAW,OAAO,MAAM,SAAS,EAAE,IAAI,CAAC;GAClE,GAAI,OAAO,SACP,EAAE,QAAQ,WAAW,OAAO,QAAQ,SAAS,EAAE,IAC/C,CAAC;GACL,IAAI;GACJ,UAAU,OAAO;EACnB,CAAC;CACH;CAEA,IAAI,OAAO,WAAW,GACpB,MAAM,IAAI,MACR,GAAG,WAAW,gBAAe,2FAE/B;CAGF,MAAM,QAAQ,WAAW,SAAS,CAAC;CACnC,OAAO;EACL,MAAM,WAAW;EACjB,GAAI,MAAM,SAAS,IACf;GACE,WAAA;GACA,OAAO,MAAM,KAAK,UAAU;IAC1B,OAAO,KAAK;IACZ,OAAO,KAAK;IACZ,MAAM,KAAK;IACX,IAAI,cAAc,IAAI;GACxB,EAAE;EACJ,IACA,CAAC;EACL;EACA;EACA,OAAO,eAAe,WAAW,OAAO,SAAS;EACjD,QAAQ,eACN,WAAW,SACV,UAAU,oBAAoB,IAAI,KAAK,KAAK,YAAY,KAAK,CAChE;CACF;AACF;AAEA,eAAsB,kBACpB,YACwB;CACxB,OAAO,wBAAwB,MAAM,sBAAsB,UAAU,CAAC;AACxE"}
|
package/dist/compiler.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type SmrtObject } from '@happyvertical/smrt-core';
|
|
2
2
|
import type { AggregateSpec, ReportDefinition } from './types.js';
|
|
3
|
+
/** The report source's alias in a query that joins other tables. */
|
|
4
|
+
export declare const REPORT_SOURCE_ALIAS = "src";
|
|
3
5
|
export declare function buildReportDefinition(reportCtor: new (...args: any[]) => SmrtObject): Promise<ReportDefinition>;
|
|
4
6
|
export declare function getReportGroupingColumns(definition: Pick<ReportDefinition, 'fields'>): string[];
|
|
5
7
|
export declare function compileReportDefinition(definition: ReportDefinition): AggregateSpec;
|
package/dist/compiler.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compiler.d.ts","sourceRoot":"","sources":["../src/compiler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"compiler.d.ts","sourceRoot":"","sources":["../src/compiler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAK3E,OAAO,KAAK,EAEV,aAAa,EACb,gBAAgB,EAQjB,MAAM,YAAY,CAAC;AAEpB,oEAAoE;AACpE,eAAO,MAAM,mBAAmB,QAAQ,CAAC;AAuSzC,wBAAsB,qBAAqB,CACzC,UAAU,EAAE,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,UAAU,GAC7C,OAAO,CAAC,gBAAgB,CAAC,CAoD3B;AAoED,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,IAAI,CAAC,gBAAgB,EAAE,QAAQ,CAAC,GAC3C,MAAM,EAAE,CAQV;AA6BD,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,gBAAgB,GAC3B,aAAa,CA2Ef;AAED,wBAAsB,iBAAiB,CACrC,UAAU,EAAE,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,UAAU,GAC7C,OAAO,CAAC,aAAa,CAAC,CAExB"}
|