@happyvertical/smrt-reports 0.36.5

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.
@@ -0,0 +1,407 @@
1
+ import { AggregateBuildResult } from '@happyvertical/sql';
2
+ import { AggregateFunction } from '@happyvertical/sql';
3
+ import { AggregateSelectExpr } from '@happyvertical/sql';
4
+ import { AggregateSpec } from '@happyvertical/sql';
5
+ import { AggregateTimeBucketUnit } from '@happyvertical/sql';
6
+ import { bucketExpr } from '@happyvertical/sql';
7
+ import { buildAggregate } from '@happyvertical/sql';
8
+ import { DatabaseInterface } from '@happyvertical/sql';
9
+ import { EventEmitter } from 'node:events';
10
+ import { SmrtCollection } from '@happyvertical/smrt-core';
11
+ import { SmrtJob } from '@happyvertical/smrt-jobs';
12
+ import { SmrtObject } from '@happyvertical/smrt-core';
13
+ import { SmrtObjectOptions } from '@happyvertical/smrt-core';
14
+ import { SqlAdapterType } from '@happyvertical/sql';
15
+ import { WhereClause } from '@happyvertical/sql';
16
+
17
+ export declare function aggregate(options: AggregateDecoratorOptions): PropertyDecorator;
18
+
19
+ export { AggregateBuildResult }
20
+
21
+ declare interface AggregateDecoratorOptions {
22
+ fn: ReportAggregateFn;
23
+ column?: string;
24
+ distinct?: boolean;
25
+ }
26
+
27
+ export { AggregateSelectExpr }
28
+
29
+ export { AggregateSpec }
30
+
31
+ export declare function assertReportTablesReady(db: DatabaseInterface, tables?: readonly string[]): Promise<void>;
32
+
33
+ export declare const avg: (column?: string, options?: {
34
+ distinct?: boolean;
35
+ }) => PropertyDecorator;
36
+
37
+ export { bucketExpr }
38
+
39
+ export { buildAggregate }
40
+
41
+ export declare function buildReportDefinition(reportCtor: new (...args: any[]) => SmrtObject): Promise<ReportDefinition>;
42
+
43
+ export declare function compileReportDefinition(definition: ReportDefinition): AggregateSpec;
44
+
45
+ export declare function compileReportSpec(reportCtor: new (...args: any[]) => SmrtObject): Promise<AggregateSpec>;
46
+
47
+ export declare function count(): PropertyDecorator;
48
+
49
+ export declare function count(column: string, options?: {
50
+ distinct?: boolean;
51
+ }): PropertyDecorator;
52
+
53
+ export declare function count(options: {
54
+ distinct?: boolean;
55
+ }): PropertyDecorator;
56
+
57
+ export declare const day: (sourceColumn: string) => PropertyDecorator;
58
+
59
+ export declare function enqueueReportRefresh(options: EnqueueReportRefreshOptions): Promise<SmrtJob>;
60
+
61
+ export declare interface EnqueueReportRefreshOptions extends ReportRefreshJobArgs {
62
+ report?: ReportCtor_2;
63
+ reportClass: string;
64
+ db: DatabaseInterface;
65
+ queue?: string;
66
+ priority?: number;
67
+ timeout?: number;
68
+ maxAttempts?: number;
69
+ tenantJobCap?: number;
70
+ }
71
+
72
+ export declare function ensureReportRefreshSchedules(options: EnsureReportSchedulesOptions): Promise<void>;
73
+
74
+ export declare interface EnsureReportSchedulesOptions {
75
+ db: DatabaseInterface;
76
+ reports: ReportCtor_2[];
77
+ tenantIds?: string[];
78
+ queue?: string;
79
+ priority?: number;
80
+ timeout?: number;
81
+ }
82
+
83
+ export declare function getReportGroupingColumns(definition: Pick<ReportDefinition, 'fields'>): string[];
84
+
85
+ export declare function getRuntimeReportOptions(ctor: Function): ReportOptions | undefined;
86
+
87
+ export declare function groupBy(sourceColumn?: string): PropertyDecorator;
88
+
89
+ export declare const hour: (sourceColumn: string) => PropertyDecorator;
90
+
91
+ export declare const max: (column?: string, options?: {
92
+ distinct?: boolean;
93
+ }) => PropertyDecorator;
94
+
95
+ export declare const min: (column?: string, options?: {
96
+ distinct?: boolean;
97
+ }) => PropertyDecorator;
98
+
99
+ export declare const minute: (sourceColumn: string) => PropertyDecorator;
100
+
101
+ export declare const month: (sourceColumn: string) => PropertyDecorator;
102
+
103
+ export declare const quarter: (sourceColumn: string) => PropertyDecorator;
104
+
105
+ export declare function refreshReport(reportCtor: ReportCtor, options?: ReportRefreshOptions): Promise<ReportRefreshResult>;
106
+
107
+ export declare function registerReportRefreshInterceptor(options: ReportRefreshInterceptorOptions): () => boolean;
108
+
109
+ export declare function report(options: ReportOptions): <T extends abstract new (...args: any[]) => any>(ctor: T) => T;
110
+
111
+ export declare const REPORT_LOCKS_TABLE = "_smrt_report_locks";
112
+
113
+ export declare const REPORT_REFRESH_TASKS_TABLE = "_smrt_report_refresh_tasks";
114
+
115
+ export declare const REPORT_RUNS_TABLE = "_smrt_report_runs";
116
+
117
+ export declare const REPORT_RUNTIME_TABLES: readonly ["_smrt_report_runs", "_smrt_report_watermarks", "_smrt_report_locks"];
118
+
119
+ export declare const REPORT_SCHEDULER_TABLES: readonly ["_smrt_report_schedules", "_smrt_report_refresh_tasks"];
120
+
121
+ export declare const REPORT_SCHEDULES_TABLE = "_smrt_report_schedules";
122
+
123
+ export declare const REPORT_WATERMARKS_TABLE = "_smrt_report_watermarks";
124
+
125
+ export declare interface ReportAggregateFieldMetadata {
126
+ kind: 'aggregate';
127
+ fn: ReportAggregateFn;
128
+ column?: string;
129
+ distinct?: boolean;
130
+ }
131
+
132
+ export declare type ReportAggregateFn = AggregateFunction;
133
+
134
+ export declare interface ReportBucketFieldMetadata {
135
+ kind: 'bucket';
136
+ unit: ReportTimeBucketUnit;
137
+ sourceColumn: string;
138
+ }
139
+
140
+ declare type ReportCtor = new (...args: any[]) => SmrtObject;
141
+
142
+ declare type ReportCtor_2 = new (...args: any[]) => SmrtObject;
143
+
144
+ export declare interface ReportDefinition {
145
+ reportClassName: string;
146
+ sourceClassName: string;
147
+ sourceTable: string;
148
+ fields: ReportFieldDefinition[];
149
+ where?: WhereClause;
150
+ having?: WhereClause;
151
+ refresh?: ReportRefreshConfig;
152
+ }
153
+
154
+ export declare interface ReportFieldDefinition {
155
+ fieldName: string;
156
+ columnName?: string;
157
+ type?: string;
158
+ report?: ReportFieldMetadata;
159
+ }
160
+
161
+ export declare type ReportFieldMetadata = ReportGroupFieldMetadata | ReportBucketFieldMetadata | ReportAggregateFieldMetadata;
162
+
163
+ export declare interface ReportGroupFieldMetadata {
164
+ kind: 'group';
165
+ sourceColumn?: string;
166
+ }
167
+
168
+ export declare interface ReportOptions {
169
+ source: ReportSource;
170
+ where?: WhereClause;
171
+ having?: WhereClause;
172
+ refresh?: ReportRefreshConfig;
173
+ }
174
+
175
+ export declare interface ReportRefreshConfig {
176
+ mode?: ReportRefreshMode;
177
+ schedule?: string;
178
+ onChange?: ReportSource[];
179
+ /**
180
+ * Milliseconds before collection reads trigger a synchronous refresh.
181
+ *
182
+ * TTL refresh checks add a read-time MAX(refreshedAt) query, and stale reads
183
+ * perform the refresh before returning list/get results.
184
+ */
185
+ ttl?: number;
186
+ manual?: boolean;
187
+ watermarkColumn?: string;
188
+ softDeleteColumn?: string;
189
+ fullRebuildSchedule?: string;
190
+ tenantFanout?: boolean;
191
+ }
192
+
193
+ export declare interface ReportRefreshInterceptorOptions {
194
+ db: DatabaseInterface;
195
+ reports: ReportCtor_2[];
196
+ enqueue?: boolean;
197
+ queue?: string;
198
+ priority?: number;
199
+ timeout?: number;
200
+ tenantJobCap?: number;
201
+ name?: string;
202
+ }
203
+
204
+ export declare interface ReportRefreshJobArgs {
205
+ reportClass?: string;
206
+ mode?: ReportRefreshMode;
207
+ trigger?: ReportRefreshTrigger;
208
+ tenantId?: string | null;
209
+ tenantIds?: string[];
210
+ scheduleId?: string;
211
+ adapterType?: SqlAdapterType;
212
+ changedRows?: Record<string, unknown>[];
213
+ _scheduleId?: string;
214
+ }
215
+
216
+ export declare type ReportRefreshMode = 'rebuild' | 'incremental';
217
+
218
+ export declare interface ReportRefreshOptions {
219
+ db?: DatabaseInterface;
220
+ mode?: ReportRefreshMode;
221
+ adapterType?: SqlAdapterType;
222
+ trigger?: ReportRefreshTrigger;
223
+ tenantId?: string | null;
224
+ tenantIds?: string[];
225
+ lock?: boolean;
226
+ lockTtlMs?: number;
227
+ trackRuns?: boolean;
228
+ scheduleId?: string;
229
+ changedRows?: Record<string, unknown>[];
230
+ }
231
+
232
+ export declare interface ReportRefreshResult {
233
+ rowCount: number;
234
+ refreshedAt: Date;
235
+ mode: ReportRefreshMode;
236
+ tenantId?: string | null;
237
+ runId?: string;
238
+ changedGroupCount?: number;
239
+ skipped?: boolean;
240
+ tenantResults?: ReportRefreshResult[];
241
+ }
242
+
243
+ export declare type ReportRefreshTrigger = 'manual' | 'schedule' | 'change' | 'ttl' | 'job';
244
+
245
+ export declare function reportRowIdentity(row: Record<string, any>, definition: ReportDefinition): string;
246
+
247
+ declare type ReportRunStatus = 'running' | 'success' | 'failed' | 'skipped';
248
+
249
+ export declare interface ReportScheduleInfo {
250
+ id: string;
251
+ reportClass: string;
252
+ tenantId: string | null;
253
+ cron: string;
254
+ mode: ReportRefreshMode;
255
+ }
256
+
257
+ export declare class ReportScheduleRunner extends EventEmitter {
258
+ readonly id: string;
259
+ private readonly config;
260
+ private db;
261
+ private running;
262
+ private pollTimer;
263
+ constructor(config?: ReportScheduleRunnerConfig);
264
+ initialize(db: DatabaseInterface): Promise<void>;
265
+ start(): Promise<void>;
266
+ stop(): Promise<void>;
267
+ isRunning(): boolean;
268
+ handleJobCompletion(scheduleId: string, success: boolean, errorMessage?: string): Promise<void>;
269
+ private startPolling;
270
+ poll(): Promise<void>;
271
+ private triggerSchedule;
272
+ }
273
+
274
+ export declare interface ReportScheduleRunnerConfig {
275
+ id?: string;
276
+ pollInterval?: number;
277
+ batchSize?: number;
278
+ }
279
+
280
+ export declare interface ReportScheduleRunnerEvents {
281
+ 'schedule:triggered': (schedule: ReportScheduleInfo) => void;
282
+ 'schedule:error': (schedule: ReportScheduleInfo, error: Error) => void;
283
+ 'schedule:completed': (scheduleId: string) => void;
284
+ 'schedule:failed': (scheduleId: string, error: string) => void;
285
+ 'runner:started': () => void;
286
+ 'runner:stopped': () => void;
287
+ 'runner:error': (error: Error) => void;
288
+ }
289
+
290
+ declare type ReportScheduleStatus = 'active' | 'paused' | 'disabled' | 'error';
291
+
292
+ export declare type ReportSource = string | (new (...args: any[]) => SmrtObject) | (abstract new (...args: any[]) => SmrtObject);
293
+
294
+ export declare type ReportTimeBucketUnit = AggregateTimeBucketUnit;
295
+
296
+ export declare function scopeKeyForTenant(tenantId: string | null | undefined): string;
297
+
298
+ export declare class SmrtReport extends SmrtObject {
299
+ static readonly _isReportBase: true;
300
+ refreshedAt: Date | null;
301
+ constructor(options?: SmrtObjectOptions);
302
+ isStale(ttlMs?: number): boolean;
303
+ refresh(options?: Omit<ReportRefreshOptions, 'db'>): Promise<ReportRefreshResult>;
304
+ }
305
+
306
+ export declare class SmrtReportCollection<ModelType extends SmrtReport> extends SmrtCollection<ModelType> {
307
+ private refreshIfStale;
308
+ refresh(options?: Omit<ReportRefreshOptions, 'db'>): Promise<ReportRefreshResult>;
309
+ list(options?: Parameters<SmrtCollection<ModelType>['list']>[0]): Promise<ModelType[]>;
310
+ get(filter: Parameters<SmrtCollection<ModelType>['get']>[0], options?: Parameters<SmrtCollection<ModelType>['get']>[1]): Promise<ModelType | null>;
311
+ }
312
+
313
+ export declare class SmrtReportLock extends SmrtObject {
314
+ tenantId: string | null;
315
+ scopeKey: string;
316
+ reportClass: string;
317
+ ownerId: string | null;
318
+ acquiredAt: Date | null;
319
+ heartbeatAt: Date | null;
320
+ expiresAt: Date | null;
321
+ }
322
+
323
+ export declare class SmrtReportLockCollection extends SmrtCollection<SmrtReportLock> {
324
+ static readonly _itemClass: typeof SmrtReportLock;
325
+ }
326
+
327
+ export declare class SmrtReportRefreshTask extends SmrtObject {
328
+ tenantId: string | null;
329
+ reportClass: string;
330
+ mode: ReportRefreshMode;
331
+ trigger: ReportRefreshTrigger;
332
+ args: ReportRefreshJobArgs;
333
+ run(args?: ReportRefreshJobArgs): Promise<unknown>;
334
+ }
335
+
336
+ export declare class SmrtReportRun extends SmrtObject {
337
+ tenantId: string | null;
338
+ scopeKey: string;
339
+ reportClass: string;
340
+ sourceClass: string;
341
+ mode: ReportRefreshMode;
342
+ trigger: ReportRefreshTrigger;
343
+ status: ReportRunStatus;
344
+ startedAt: Date;
345
+ completedAt: Date | null;
346
+ rowCount: number;
347
+ changedGroupCount: number;
348
+ watermarkBefore: string | null;
349
+ watermarkAfter: string | null;
350
+ error: string | null;
351
+ metadata: Record<string, unknown>;
352
+ }
353
+
354
+ export declare class SmrtReportRunCollection extends SmrtCollection<SmrtReportRun> {
355
+ static readonly _itemClass: typeof SmrtReportRun;
356
+ }
357
+
358
+ export declare class SmrtReportSchedule extends SmrtObject {
359
+ tenantId: string | null;
360
+ scopeKey: string;
361
+ reportClass: string;
362
+ cron: string;
363
+ trigger: ReportRefreshTrigger;
364
+ mode: ReportRefreshMode;
365
+ enabled: boolean;
366
+ status: ReportScheduleStatus;
367
+ nextRun: Date | null;
368
+ lastRun: Date | null;
369
+ lastStatus: 'success' | 'failed' | null;
370
+ lastError: string | null;
371
+ runCount: number;
372
+ successCount: number;
373
+ failureCount: number;
374
+ runningCount: number;
375
+ maxConcurrent: number;
376
+ queue: string;
377
+ priority: number;
378
+ timeout: number;
379
+ }
380
+
381
+ export declare class SmrtReportScheduleCollection extends SmrtCollection<SmrtReportSchedule> {
382
+ static readonly _itemClass: typeof SmrtReportSchedule;
383
+ }
384
+
385
+ export declare class SmrtReportWatermark extends SmrtObject {
386
+ tenantId: string | null;
387
+ scopeKey: string;
388
+ reportClass: string;
389
+ sourceClass: string;
390
+ watermarkColumn: string;
391
+ watermarkValue: string | null;
392
+ lastRunId: string | null;
393
+ }
394
+
395
+ export declare class SmrtReportWatermarkCollection extends SmrtCollection<SmrtReportWatermark> {
396
+ static readonly _itemClass: typeof SmrtReportWatermark;
397
+ }
398
+
399
+ export declare const sum: (column?: string, options?: {
400
+ distinct?: boolean;
401
+ }) => PropertyDecorator;
402
+
403
+ export declare const week: (sourceColumn: string) => PropertyDecorator;
404
+
405
+ export declare const year: (sourceColumn: string) => PropertyDecorator;
406
+
407
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,162 @@
1
+ import { validateColumnName } from "@happyvertical/sql";
2
+ import { bucketExpr, buildAggregate } from "@happyvertical/sql";
3
+ import { buildReportDefinition } from "./compiler.js";
4
+ import { compileReportDefinition, compileReportSpec, getReportGroupingColumns } from "./compiler.js";
5
+ import { aggregate, avg, count, day, getRuntimeReportOptions, groupBy, hour, max, min, minute, month, quarter, report, sum, week, year } from "./decorators.js";
6
+ import { refreshReport } from "./refresh.js";
7
+ import { reportRowIdentity } from "./refresh.js";
8
+ import { SmrtObject, SmrtCollection, ObjectRegistry } from "@happyvertical/smrt-core";
9
+ import { toSnakeCase } from "@happyvertical/smrt-core/utils";
10
+ import { getTenantId } from "@happyvertical/smrt-tenancy";
11
+ import { ReportScheduleRunner, SmrtReportRefreshTask, enqueueReportRefresh, ensureReportRefreshSchedules, registerReportRefreshInterceptor } from "./scheduler.js";
12
+ import { REPORT_LOCKS_TABLE, REPORT_REFRESH_TASKS_TABLE, REPORT_RUNS_TABLE, REPORT_RUNTIME_TABLES, REPORT_SCHEDULER_TABLES, REPORT_SCHEDULES_TABLE, REPORT_WATERMARKS_TABLE, SmrtReportLock, SmrtReportLockCollection, SmrtReportRun, SmrtReportRunCollection, SmrtReportSchedule, SmrtReportScheduleCollection, SmrtReportWatermark, SmrtReportWatermarkCollection, assertReportTablesReady, scopeKeyForTenant } from "./state.js";
13
+ function registryColumnName(fieldName, field) {
14
+ return validateColumnName(
15
+ field?.columnName ?? field?._meta?.columnName ?? toSnakeCase(fieldName)
16
+ );
17
+ }
18
+ function findFieldColumn(fields, fieldName) {
19
+ const direct = fields.get(fieldName);
20
+ if (direct) return registryColumnName(fieldName, direct);
21
+ const requestedColumn = toSnakeCase(fieldName);
22
+ for (const [name, field] of fields.entries()) {
23
+ if (toSnakeCase(name) === requestedColumn) {
24
+ return registryColumnName(name, field);
25
+ }
26
+ }
27
+ return validateColumnName(requestedColumn);
28
+ }
29
+ function findTenantColumn(fields, configuredField) {
30
+ if (configuredField) {
31
+ return registryColumnName(configuredField, fields.get(configuredField));
32
+ }
33
+ for (const [fieldName, field] of fields.entries()) {
34
+ if (fieldName === "tenantId" || field?._meta?.__tenancy?.isTenantIdField) {
35
+ return registryColumnName(fieldName, field);
36
+ }
37
+ }
38
+ return null;
39
+ }
40
+ class SmrtReport extends SmrtObject {
41
+ static _isReportBase = true;
42
+ refreshedAt = null;
43
+ constructor(options = {}) {
44
+ super(options);
45
+ if (options.refreshedAt !== void 0) {
46
+ this.refreshedAt = options.refreshedAt instanceof Date ? options.refreshedAt : options.refreshedAt ? new Date(options.refreshedAt) : null;
47
+ }
48
+ }
49
+ isStale(ttlMs) {
50
+ if (!this.refreshedAt) return true;
51
+ if (ttlMs === void 0) return false;
52
+ return Date.now() - this.refreshedAt.getTime() > ttlMs;
53
+ }
54
+ async refresh(options = {}) {
55
+ const result = await refreshReport(this.constructor, {
56
+ ...options,
57
+ db: this.db
58
+ });
59
+ this.refreshedAt = result.refreshedAt;
60
+ return result;
61
+ }
62
+ }
63
+ class SmrtReportCollection extends SmrtCollection {
64
+ async refreshIfStale() {
65
+ const reportCtor = this.getItemClass();
66
+ const definition = await buildReportDefinition(reportCtor);
67
+ const refresh = definition.refresh;
68
+ if (!refresh?.ttl || refresh.manual) return;
69
+ const registered = ObjectRegistry.getClassByConstructor(reportCtor) ?? ObjectRegistry.getClass(reportCtor.name);
70
+ const reportClass = registered?.qualifiedName ?? registered?.name ?? reportCtor.name;
71
+ const tableName = ObjectRegistry.getTableName(reportClass);
72
+ if (!tableName) return;
73
+ const fields = await ObjectRegistry.getAllFields(reportClass);
74
+ const safeTableName = validateColumnName(tableName);
75
+ const refreshedAtColumn = findFieldColumn(fields, "refreshedAt");
76
+ const tenantColumn = findTenantColumn(
77
+ fields,
78
+ registered?.tenantScopedConfig?.field
79
+ );
80
+ const tenantId = getTenantId() ?? null;
81
+ const result = tenantColumn ? await this.db.query(
82
+ `SELECT MAX(${refreshedAtColumn}) AS refreshed_at FROM ${safeTableName} WHERE ${tenantColumn} ${tenantId ? "= ?" : "IS NULL"}`,
83
+ ...tenantId ? [tenantId] : []
84
+ ) : await this.db.query(
85
+ `SELECT MAX(${refreshedAtColumn}) AS refreshed_at FROM ${safeTableName}`
86
+ );
87
+ const refreshedAt = result.rows[0]?.refreshed_at ? new Date(result.rows[0].refreshed_at) : null;
88
+ const stale = !refreshedAt || Date.now() - refreshedAt.getTime() > refresh.ttl;
89
+ if (!stale) return;
90
+ await refreshReport(reportCtor, {
91
+ db: this.db,
92
+ mode: refresh.mode ?? "rebuild",
93
+ trigger: "ttl",
94
+ tenantId
95
+ });
96
+ }
97
+ async refresh(options = {}) {
98
+ return refreshReport(this.getItemClass(), {
99
+ ...options,
100
+ db: this.db
101
+ });
102
+ }
103
+ async list(options = {}) {
104
+ await this.refreshIfStale();
105
+ return super.list(options);
106
+ }
107
+ async get(filter, options = {}) {
108
+ await this.refreshIfStale();
109
+ return super.get(filter, options);
110
+ }
111
+ }
112
+ export {
113
+ REPORT_LOCKS_TABLE,
114
+ REPORT_REFRESH_TASKS_TABLE,
115
+ REPORT_RUNS_TABLE,
116
+ REPORT_RUNTIME_TABLES,
117
+ REPORT_SCHEDULER_TABLES,
118
+ REPORT_SCHEDULES_TABLE,
119
+ REPORT_WATERMARKS_TABLE,
120
+ ReportScheduleRunner,
121
+ SmrtReport,
122
+ SmrtReportCollection,
123
+ SmrtReportLock,
124
+ SmrtReportLockCollection,
125
+ SmrtReportRefreshTask,
126
+ SmrtReportRun,
127
+ SmrtReportRunCollection,
128
+ SmrtReportSchedule,
129
+ SmrtReportScheduleCollection,
130
+ SmrtReportWatermark,
131
+ SmrtReportWatermarkCollection,
132
+ aggregate,
133
+ assertReportTablesReady,
134
+ avg,
135
+ bucketExpr,
136
+ buildAggregate,
137
+ buildReportDefinition,
138
+ compileReportDefinition,
139
+ compileReportSpec,
140
+ count,
141
+ day,
142
+ enqueueReportRefresh,
143
+ ensureReportRefreshSchedules,
144
+ getReportGroupingColumns,
145
+ getRuntimeReportOptions,
146
+ groupBy,
147
+ hour,
148
+ max,
149
+ min,
150
+ minute,
151
+ month,
152
+ quarter,
153
+ refreshReport,
154
+ registerReportRefreshInterceptor,
155
+ report,
156
+ reportRowIdentity,
157
+ scopeKeyForTenant,
158
+ sum,
159
+ week,
160
+ year
161
+ };
162
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/report.ts"],"sourcesContent":["import {\n ObjectRegistry,\n SmrtCollection,\n SmrtObject,\n type SmrtObjectOptions,\n} from '@happyvertical/smrt-core';\nimport { toSnakeCase } from '@happyvertical/smrt-core/utils';\nimport { getTenantId } from '@happyvertical/smrt-tenancy';\nimport { validateColumnName } from '@happyvertical/sql';\nimport { buildReportDefinition } from './compiler.js';\nimport { refreshReport } from './refresh.js';\nimport type { ReportRefreshOptions, ReportRefreshResult } from './types.js';\n\ntype RegistryField = {\n columnName?: string;\n _meta?: {\n columnName?: string;\n __tenancy?: { isTenantIdField?: boolean };\n };\n};\n\nfunction registryColumnName(fieldName: string, field?: RegistryField): string {\n return validateColumnName(\n field?.columnName ?? field?._meta?.columnName ?? toSnakeCase(fieldName),\n );\n}\n\nfunction findFieldColumn(\n fields: Map<string, RegistryField>,\n fieldName: string,\n): string {\n const direct = fields.get(fieldName);\n if (direct) return registryColumnName(fieldName, direct);\n const requestedColumn = toSnakeCase(fieldName);\n for (const [name, field] of fields.entries()) {\n if (toSnakeCase(name) === requestedColumn) {\n return registryColumnName(name, field);\n }\n }\n return validateColumnName(requestedColumn);\n}\n\nfunction findTenantColumn(\n fields: Map<string, RegistryField>,\n configuredField?: string,\n): string | null {\n if (configuredField) {\n return registryColumnName(configuredField, fields.get(configuredField));\n }\n for (const [fieldName, field] of fields.entries()) {\n if (fieldName === 'tenantId' || field?._meta?.__tenancy?.isTenantIdField) {\n return registryColumnName(fieldName, field);\n }\n }\n return null;\n}\n\nexport class SmrtReport extends SmrtObject {\n static readonly _isReportBase = true as const;\n\n refreshedAt: Date | null = null;\n\n constructor(options: SmrtObjectOptions = {}) {\n super(options);\n if (options.refreshedAt !== undefined) {\n this.refreshedAt =\n options.refreshedAt instanceof Date\n ? options.refreshedAt\n : options.refreshedAt\n ? new Date(options.refreshedAt)\n : null;\n }\n }\n\n isStale(ttlMs?: number): boolean {\n if (!this.refreshedAt) return true;\n if (ttlMs === undefined) return false;\n return Date.now() - this.refreshedAt.getTime() > ttlMs;\n }\n\n async refresh(\n options: Omit<ReportRefreshOptions, 'db'> = {},\n ): Promise<ReportRefreshResult> {\n const result = await refreshReport(this.constructor as typeof SmrtReport, {\n ...options,\n db: this.db,\n });\n this.refreshedAt = result.refreshedAt;\n return result;\n }\n}\n\nexport class SmrtReportCollection<\n ModelType extends SmrtReport,\n> extends SmrtCollection<ModelType> {\n private async refreshIfStale(): Promise<void> {\n const reportCtor = this.getItemClass();\n const definition = await buildReportDefinition(reportCtor);\n const refresh = definition.refresh;\n if (!refresh?.ttl || refresh.manual) return;\n\n const registered =\n ObjectRegistry.getClassByConstructor(reportCtor) ??\n ObjectRegistry.getClass(reportCtor.name);\n const reportClass =\n registered?.qualifiedName ?? registered?.name ?? reportCtor.name;\n const tableName = ObjectRegistry.getTableName(reportClass);\n if (!tableName) return;\n\n const fields = (await ObjectRegistry.getAllFields(reportClass)) as Map<\n string,\n RegistryField\n >;\n const safeTableName = validateColumnName(tableName);\n const refreshedAtColumn = findFieldColumn(fields, 'refreshedAt');\n const tenantColumn = findTenantColumn(\n fields,\n registered?.tenantScopedConfig?.field,\n );\n const tenantId = getTenantId() ?? null;\n const result = tenantColumn\n ? await this.db.query(\n `SELECT MAX(${refreshedAtColumn}) AS refreshed_at FROM ${safeTableName} WHERE ${tenantColumn} ${tenantId ? '= ?' : 'IS NULL'}`,\n ...(tenantId ? [tenantId] : []),\n )\n : await this.db.query(\n `SELECT MAX(${refreshedAtColumn}) AS refreshed_at FROM ${safeTableName}`,\n );\n const refreshedAt = result.rows[0]?.refreshed_at\n ? new Date(result.rows[0].refreshed_at as string)\n : null;\n const stale =\n !refreshedAt || Date.now() - refreshedAt.getTime() > refresh.ttl;\n if (!stale) return;\n\n await refreshReport(reportCtor, {\n db: this.db,\n mode: refresh.mode ?? 'rebuild',\n trigger: 'ttl',\n tenantId,\n });\n }\n\n async refresh(\n options: Omit<ReportRefreshOptions, 'db'> = {},\n ): Promise<ReportRefreshResult> {\n return refreshReport(this.getItemClass(), {\n ...options,\n db: this.db,\n });\n }\n\n override async list(\n options: Parameters<SmrtCollection<ModelType>['list']>[0] = {},\n ): Promise<ModelType[]> {\n await this.refreshIfStale();\n return super.list(options);\n }\n\n override async get(\n filter: Parameters<SmrtCollection<ModelType>['get']>[0],\n options: Parameters<SmrtCollection<ModelType>['get']>[1] = {},\n ): Promise<ModelType | null> {\n await this.refreshIfStale();\n return super.get(filter, options);\n }\n}\n"],"names":[],"mappings":"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"}