@happyvertical/smrt-reports 0.49.2 → 0.49.4
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/README.md +11 -1
- package/dist/index.d.ts +60 -3
- package/dist/index.js +11 -5
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +438 -22
- package/dist/scheduler.d.ts +53 -1
- package/dist/scheduler.js +135 -21
- package/dist/scheduler.js.map +1 -1
- package/dist/smrt-knowledge.json +87 -5
- package/package.json +6 -6
package/dist/scheduler.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { DatabaseInterface } from '@happyvertical/sql';
|
|
2
|
+
import { DurableJobPayloadIntegrity } from '@happyvertical/smrt-jobs';
|
|
3
|
+
import { DurableJobPayloadSigner } from '@happyvertical/smrt-jobs';
|
|
2
4
|
import { EventEmitter } from 'node:events';
|
|
5
|
+
import { JobExecutionContext } from '@happyvertical/smrt-jobs';
|
|
3
6
|
import { SmrtJob } from '@happyvertical/smrt-jobs';
|
|
4
7
|
import { SmrtObject } from '@happyvertical/smrt-core';
|
|
5
8
|
import { SqlAdapterType } from '@happyvertical/sql';
|
|
@@ -15,6 +18,8 @@ export declare interface EnqueueReportRefreshOptions extends ReportRefreshJobArg
|
|
|
15
18
|
timeout?: number;
|
|
16
19
|
maxAttempts?: number;
|
|
17
20
|
tenantJobCap?: number;
|
|
21
|
+
/** Server-only signer; register the same key in every worker process. */
|
|
22
|
+
integritySigner?: DurableJobPayloadSigner;
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
export declare function ensureReportRefreshSchedules(options: EnsureReportSchedulesOptions): Promise<void>;
|
|
@@ -28,10 +33,48 @@ export declare interface EnsureReportSchedulesOptions {
|
|
|
28
33
|
timeout?: number;
|
|
29
34
|
}
|
|
30
35
|
|
|
36
|
+
export declare function registerReportRefreshExecutionAuthorityHost(hostId: string, host: ReportRefreshExecutionAuthorityHost): () => void;
|
|
37
|
+
|
|
31
38
|
export declare function registerReportRefreshInterceptor(options: ReportRefreshInterceptorOptions): () => boolean;
|
|
32
39
|
|
|
40
|
+
export declare function registerReportRefreshJobIntegritySigner(signer: DurableJobPayloadSigner): () => void;
|
|
41
|
+
|
|
33
42
|
declare type ReportCtor = new (...args: any[]) => SmrtObject;
|
|
34
43
|
|
|
44
|
+
export declare interface ReportExecutionPrincipalReference {
|
|
45
|
+
version: 1;
|
|
46
|
+
actorUserId: string;
|
|
47
|
+
tenantId: string | null;
|
|
48
|
+
onBehalfOfUserId?: string | null;
|
|
49
|
+
actsAsProfileId?: string | null;
|
|
50
|
+
agentClass?: string | null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export declare interface ReportRefreshExecutionAuditEvent extends ReportRefreshExecutionAuthorityContext {
|
|
54
|
+
outcome: 'allowed' | 'denied';
|
|
55
|
+
principal: ReportExecutionPrincipalReference;
|
|
56
|
+
reason?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export declare interface ReportRefreshExecutionAuthority {
|
|
60
|
+
version: 1;
|
|
61
|
+
hostId: string;
|
|
62
|
+
principal: ReportExecutionPrincipalReference;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export declare interface ReportRefreshExecutionAuthorityContext {
|
|
66
|
+
phase: 'execute';
|
|
67
|
+
reportClass: string;
|
|
68
|
+
mode: ReportRefreshMode;
|
|
69
|
+
trigger: ReportRefreshTrigger;
|
|
70
|
+
tenantId: string | null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export declare interface ReportRefreshExecutionAuthorityHost {
|
|
74
|
+
authorize(principal: Readonly<ReportExecutionPrincipalReference>, context: Readonly<ReportRefreshExecutionAuthorityContext>): Promise<void> | void;
|
|
75
|
+
audit(event: Readonly<ReportRefreshExecutionAuditEvent>): Promise<void> | void;
|
|
76
|
+
}
|
|
77
|
+
|
|
35
78
|
export declare interface ReportRefreshInterceptorOptions {
|
|
36
79
|
db: DatabaseInterface;
|
|
37
80
|
reports: ReportCtor[];
|
|
@@ -41,6 +84,7 @@ export declare interface ReportRefreshInterceptorOptions {
|
|
|
41
84
|
timeout?: number;
|
|
42
85
|
tenantJobCap?: number;
|
|
43
86
|
name?: string;
|
|
87
|
+
integritySigner?: DurableJobPayloadSigner;
|
|
44
88
|
}
|
|
45
89
|
|
|
46
90
|
export declare interface ReportRefreshJobArgs {
|
|
@@ -53,6 +97,8 @@ export declare interface ReportRefreshJobArgs {
|
|
|
53
97
|
adapterType?: SqlAdapterType;
|
|
54
98
|
changedRows?: Record<string, unknown>[];
|
|
55
99
|
_scheduleId?: string;
|
|
100
|
+
executionAuthority?: ReportRefreshExecutionAuthority;
|
|
101
|
+
integrity?: DurableJobPayloadIntegrity;
|
|
56
102
|
}
|
|
57
103
|
|
|
58
104
|
declare type ReportRefreshMode = 'rebuild' | 'incremental';
|
|
@@ -88,6 +134,7 @@ export declare interface ReportScheduleRunnerConfig {
|
|
|
88
134
|
id?: string;
|
|
89
135
|
pollInterval?: number;
|
|
90
136
|
batchSize?: number;
|
|
137
|
+
integritySigner?: DurableJobPayloadSigner;
|
|
91
138
|
}
|
|
92
139
|
|
|
93
140
|
export declare interface ReportScheduleRunnerEvents {
|
|
@@ -100,13 +147,18 @@ export declare interface ReportScheduleRunnerEvents {
|
|
|
100
147
|
'runner:error': (error: Error) => void;
|
|
101
148
|
}
|
|
102
149
|
|
|
150
|
+
/** Worker target whose authority requirement cannot be downgraded by job args. */
|
|
151
|
+
export declare class SmrtPrincipalReportRefreshTask extends SmrtReportRefreshTask {
|
|
152
|
+
run(args?: ReportRefreshJobArgs, context?: JobExecutionContext): Promise<unknown>;
|
|
153
|
+
}
|
|
154
|
+
|
|
103
155
|
export declare class SmrtReportRefreshTask extends SmrtObject {
|
|
104
156
|
tenantId: string | null;
|
|
105
157
|
reportClass: string;
|
|
106
158
|
mode: ReportRefreshMode;
|
|
107
159
|
trigger: ReportRefreshTrigger;
|
|
108
160
|
args: ReportRefreshJobArgs;
|
|
109
|
-
run(args?: ReportRefreshJobArgs): Promise<unknown>;
|
|
161
|
+
run(args?: ReportRefreshJobArgs, context?: JobExecutionContext): Promise<unknown>;
|
|
110
162
|
}
|
|
111
163
|
|
|
112
164
|
export { }
|
package/dist/scheduler.js
CHANGED
|
@@ -5,7 +5,7 @@ import { GlobalInterceptors, ObjectRegistry, SmrtObject, field, smrt } from "@ha
|
|
|
5
5
|
import { TenantScoped, getTenantId, tenantId } from "@happyvertical/smrt-tenancy";
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
7
7
|
import { EventEmitter } from "node:events";
|
|
8
|
-
import { SmrtJobCollection, backgroundEligible, getNextCronDate, validateCronExpression } from "@happyvertical/smrt-jobs";
|
|
8
|
+
import { SmrtJobCollection, backgroundEligible, getActiveJobExecutionContext, getNextCronDate, isRunnerExecutionContext, validateCronExpression } from "@happyvertical/smrt-jobs";
|
|
9
9
|
//#region src/scheduler.ts
|
|
10
10
|
var __defProp = Object.defineProperty;
|
|
11
11
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -24,6 +24,45 @@ var INTERNAL_SURFACE = {
|
|
|
24
24
|
},
|
|
25
25
|
mcp: false
|
|
26
26
|
};
|
|
27
|
+
var executionAuthorityHosts = /* @__PURE__ */ new Map();
|
|
28
|
+
var jobIntegritySigners = /* @__PURE__ */ new Map();
|
|
29
|
+
function registerReportRefreshJobIntegritySigner(signer) {
|
|
30
|
+
const existing = jobIntegritySigners.get(signer.keyId);
|
|
31
|
+
if (existing && existing !== signer) throw new Error(`Report refresh job integrity signer already registered: ${signer.keyId}`);
|
|
32
|
+
jobIntegritySigners.set(signer.keyId, signer);
|
|
33
|
+
return () => {
|
|
34
|
+
if (jobIntegritySigners.get(signer.keyId) === signer) jobIntegritySigners.delete(signer.keyId);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function resolveReportRefreshIntegritySigner(configured) {
|
|
38
|
+
if (configured) return configured;
|
|
39
|
+
if (jobIntegritySigners.size !== 1) return void 0;
|
|
40
|
+
return jobIntegritySigners.values().next().value;
|
|
41
|
+
}
|
|
42
|
+
function unsignedReportRefreshJobArgs(args) {
|
|
43
|
+
const { integrity: _integrity, _scheduleId: _internalScheduleId, ...unsigned } = args;
|
|
44
|
+
return unsigned;
|
|
45
|
+
}
|
|
46
|
+
function assertReportRefreshJobIntegrity(args) {
|
|
47
|
+
const integrity = args.integrity;
|
|
48
|
+
const signer = integrity ? jobIntegritySigners.get(integrity.keyId) : void 0;
|
|
49
|
+
if (!integrity || !signer?.verify(unsignedReportRefreshJobArgs(args), integrity)) throw new Error("Invalid durable report refresh job integrity binding");
|
|
50
|
+
}
|
|
51
|
+
function assertReportRefreshJobTarget(args, context) {
|
|
52
|
+
if (!context) return;
|
|
53
|
+
if (!isRunnerExecutionContext(context)) throw new Error("Invalid durable report refresh job context");
|
|
54
|
+
const expectedType = canonicalClassName((args.trigger ?? "job") === "manual" ? SmrtPrincipalReportRefreshTask : SmrtReportRefreshTask);
|
|
55
|
+
if (context.job.objectType !== expectedType || context.job.method !== "run") throw new Error("Invalid durable report refresh job target");
|
|
56
|
+
}
|
|
57
|
+
function registerReportRefreshExecutionAuthorityHost(hostId, host) {
|
|
58
|
+
if (!hostId || hostId.length > 256) throw new Error("Report refresh authority hostId must contain 1-256 characters");
|
|
59
|
+
const existing = executionAuthorityHosts.get(hostId);
|
|
60
|
+
if (existing && existing !== host) throw new Error(`Report refresh authority host already registered: ${hostId}`);
|
|
61
|
+
executionAuthorityHosts.set(hostId, host);
|
|
62
|
+
return () => {
|
|
63
|
+
if (executionAuthorityHosts.get(hostId) === host) executionAuthorityHosts.delete(hostId);
|
|
64
|
+
};
|
|
65
|
+
}
|
|
27
66
|
function stableUuid(values) {
|
|
28
67
|
const hash = createHash("sha256").update(JSON.stringify(values)).digest("hex");
|
|
29
68
|
const variant = (Number.parseInt(hash[16], 16) & 3 | 8).toString(16);
|
|
@@ -70,20 +109,68 @@ function changedRowSnapshot(instance) {
|
|
|
70
109
|
const serializable = instance.toJSON();
|
|
71
110
|
return serializable && typeof serializable === "object" ? serializable : {};
|
|
72
111
|
}
|
|
112
|
+
async function authorizeReportRefreshExecution(args, reportClass, jobTenantId) {
|
|
113
|
+
if ((args.tenantId ?? null) !== jobTenantId) throw new Error("Invalid report refresh execution tenant");
|
|
114
|
+
const authority = args.executionAuthority;
|
|
115
|
+
if ((args.trigger ?? "job") === "manual" && !authority) throw new Error("Manual report refresh execution authority is missing");
|
|
116
|
+
if (!authority) return;
|
|
117
|
+
const principal = authority.principal;
|
|
118
|
+
const tenantId2 = args.tenantId ?? null;
|
|
119
|
+
if (authority.version !== 1 || principal?.version !== 1 || !authority.hostId || !principal.actorUserId || principal.tenantId !== tenantId2 || principal.tenantId !== jobTenantId) throw new Error("Invalid report refresh execution authority");
|
|
120
|
+
const host = executionAuthorityHosts.get(authority.hostId);
|
|
121
|
+
if (!host) throw new Error(`No report refresh authority host registered for ${authority.hostId}`);
|
|
122
|
+
const context = {
|
|
123
|
+
phase: "execute",
|
|
124
|
+
reportClass,
|
|
125
|
+
mode: args.mode ?? "incremental",
|
|
126
|
+
trigger: args.trigger ?? "job",
|
|
127
|
+
tenantId: tenantId2
|
|
128
|
+
};
|
|
129
|
+
try {
|
|
130
|
+
await host.authorize(Object.freeze({ ...principal }), Object.freeze(context));
|
|
131
|
+
} catch {
|
|
132
|
+
await host.audit({
|
|
133
|
+
...context,
|
|
134
|
+
outcome: "denied",
|
|
135
|
+
principal: Object.freeze({ ...principal }),
|
|
136
|
+
reason: "current_authority_denied"
|
|
137
|
+
});
|
|
138
|
+
throw new Error("Report refresh execution authority denied");
|
|
139
|
+
}
|
|
140
|
+
await host.audit({
|
|
141
|
+
...context,
|
|
142
|
+
outcome: "allowed",
|
|
143
|
+
principal: Object.freeze({ ...principal })
|
|
144
|
+
});
|
|
145
|
+
}
|
|
73
146
|
var SmrtReportRefreshTask = class extends SmrtObject {
|
|
74
147
|
tenantId = null;
|
|
75
148
|
reportClass = "";
|
|
76
149
|
mode = "incremental";
|
|
77
150
|
trigger = "job";
|
|
78
151
|
args = {};
|
|
79
|
-
async run(args = {}) {
|
|
80
|
-
|
|
152
|
+
async run(args = {}, context) {
|
|
153
|
+
assertReportRefreshJobIntegrity(args);
|
|
154
|
+
const executionContext = getActiveJobExecutionContext() ?? context;
|
|
155
|
+
assertReportRefreshJobTarget(args, executionContext);
|
|
156
|
+
const reportClass = args.reportClass;
|
|
81
157
|
if (!reportClass) throw new Error("Report refresh job requires reportClass");
|
|
82
|
-
|
|
158
|
+
const mode = args.mode ?? "incremental";
|
|
159
|
+
const trigger = args.trigger ?? "job";
|
|
160
|
+
const reportCtor = resolveReportClass(reportClass);
|
|
161
|
+
let jobTenantId;
|
|
162
|
+
if (executionContext) {
|
|
163
|
+
const runnerTenantId = executionContext.job.tenantId;
|
|
164
|
+
if (runnerTenantId === null) jobTenantId = null;
|
|
165
|
+
else if (typeof runnerTenantId === "string" && runnerTenantId.length > 0) jobTenantId = runnerTenantId;
|
|
166
|
+
else throw new Error("Invalid report refresh execution tenant");
|
|
167
|
+
} else jobTenantId = tenantIdFromInstance(this);
|
|
168
|
+
await authorizeReportRefreshExecution(args, reportClass, jobTenantId);
|
|
169
|
+
return refreshReport(reportCtor, {
|
|
83
170
|
db: this.db,
|
|
84
|
-
mode
|
|
85
|
-
trigger
|
|
86
|
-
tenantId:
|
|
171
|
+
mode,
|
|
172
|
+
trigger,
|
|
173
|
+
tenantId: jobTenantId,
|
|
87
174
|
tenantIds: args.tenantIds,
|
|
88
175
|
adapterType: args.adapterType,
|
|
89
176
|
scheduleId: args.scheduleId ?? args._scheduleId,
|
|
@@ -110,11 +197,41 @@ SmrtReportRefreshTask = __decorateClass([TenantScoped({ mode: "optional" }), smr
|
|
|
110
197
|
tableName: "_smrt_report_refresh_tasks",
|
|
111
198
|
...INTERNAL_SURFACE
|
|
112
199
|
})], SmrtReportRefreshTask);
|
|
200
|
+
var SmrtPrincipalReportRefreshTask = class extends SmrtReportRefreshTask {
|
|
201
|
+
async run(args = {}, context) {
|
|
202
|
+
return super.run({
|
|
203
|
+
...args,
|
|
204
|
+
trigger: "manual"
|
|
205
|
+
}, context);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
SmrtPrincipalReportRefreshTask = __decorateClass([TenantScoped({ mode: "optional" }), smrt({
|
|
209
|
+
tableName: "_smrt_principal_report_refresh_tasks",
|
|
210
|
+
...INTERNAL_SURFACE
|
|
211
|
+
})], SmrtPrincipalReportRefreshTask);
|
|
113
212
|
async function enqueueReportRefresh(options) {
|
|
213
|
+
const integritySigner = resolveReportRefreshIntegritySigner(options.integritySigner);
|
|
214
|
+
if (!integritySigner) throw new Error("Report refresh queue requires a durable job integrity signer");
|
|
215
|
+
if (options.trigger === "manual" && !options.executionAuthority) throw new Error("Manual report refresh requires execution-time authority");
|
|
216
|
+
if (options.executionAuthority && (options.executionAuthority.version !== 1 || options.executionAuthority.principal?.version !== 1 || !options.executionAuthority.hostId || !options.executionAuthority.principal.actorUserId || options.executionAuthority.principal.tenantId !== (options.tenantId ?? null))) throw new Error("Invalid report refresh execution authority");
|
|
217
|
+
if (options.executionAuthority && (options.trigger !== "manual" || (options.tenantIds?.length ?? 0) > 0)) throw new Error("Principal-bound report refresh requires one manual tenant scope");
|
|
114
218
|
await ObjectRegistry.ensureManifestLoaded("SmrtJob");
|
|
115
219
|
const collection = await SmrtJobCollection.create({ db: options.db });
|
|
116
|
-
const taskType = canonicalClassName(SmrtReportRefreshTask);
|
|
220
|
+
const taskType = canonicalClassName(options.trigger === "manual" ? SmrtPrincipalReportRefreshTask : SmrtReportRefreshTask);
|
|
117
221
|
const scheduleId = options.scheduleId ?? options._scheduleId;
|
|
222
|
+
const unsignedArgs = {
|
|
223
|
+
reportClass: options.reportClass,
|
|
224
|
+
mode: options.mode ?? "incremental",
|
|
225
|
+
trigger: options.trigger ?? "job",
|
|
226
|
+
tenantId: options.tenantId,
|
|
227
|
+
tenantIds: options.tenantIds,
|
|
228
|
+
adapterType: options.adapterType,
|
|
229
|
+
changedRows: options.changedRows,
|
|
230
|
+
scheduleId,
|
|
231
|
+
executionAuthority: options.executionAuthority
|
|
232
|
+
};
|
|
233
|
+
const integrity = integritySigner.sign(unsignedArgs);
|
|
234
|
+
if (!integritySigner.verify(unsignedArgs, integrity)) throw new Error("Report refresh job integrity signer rejected its queued payload");
|
|
118
235
|
return collection.enqueueJob({
|
|
119
236
|
tenantId: options.tenantId ?? null,
|
|
120
237
|
queue: options.queue ?? "reports",
|
|
@@ -122,15 +239,9 @@ async function enqueueReportRefresh(options) {
|
|
|
122
239
|
objectId: null,
|
|
123
240
|
method: "run",
|
|
124
241
|
args: {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
tenantId: options.tenantId,
|
|
129
|
-
tenantIds: options.tenantIds,
|
|
130
|
-
adapterType: options.adapterType,
|
|
131
|
-
changedRows: options.changedRows,
|
|
132
|
-
scheduleId,
|
|
133
|
-
_scheduleId: scheduleId
|
|
242
|
+
...unsignedArgs,
|
|
243
|
+
_scheduleId: scheduleId,
|
|
244
|
+
integrity
|
|
134
245
|
},
|
|
135
246
|
priority: options.priority ?? 70,
|
|
136
247
|
timeout: options.timeout ?? 36e5,
|
|
@@ -214,7 +325,8 @@ var ReportScheduleRunner = class extends EventEmitter {
|
|
|
214
325
|
this.config = {
|
|
215
326
|
id: config.id || `reports_${stableUuid([Date.now()]).slice(0, 8)}`,
|
|
216
327
|
pollInterval: config.pollInterval ?? 6e4,
|
|
217
|
-
batchSize: config.batchSize ?? 50
|
|
328
|
+
batchSize: config.batchSize ?? 50,
|
|
329
|
+
integritySigner: config.integritySigner
|
|
218
330
|
};
|
|
219
331
|
this.id = this.config.id;
|
|
220
332
|
}
|
|
@@ -315,7 +427,8 @@ var ReportScheduleRunner = class extends EventEmitter {
|
|
|
315
427
|
scheduleId: schedule.id,
|
|
316
428
|
queue: String(row.queue || "reports"),
|
|
317
429
|
priority: Number(row.priority ?? 70),
|
|
318
|
-
timeout: Number(row.timeout ?? 36e5)
|
|
430
|
+
timeout: Number(row.timeout ?? 36e5),
|
|
431
|
+
integritySigner: this.config.integritySigner
|
|
319
432
|
});
|
|
320
433
|
await this.db.query(`UPDATE ${REPORT_SCHEDULES_TABLE}
|
|
321
434
|
SET running_count = COALESCE(running_count, 0) + 1,
|
|
@@ -374,11 +487,12 @@ async function triggerReportsForInstance(options, instance, context) {
|
|
|
374
487
|
priority: options.priority,
|
|
375
488
|
timeout: options.timeout,
|
|
376
489
|
tenantJobCap: options.tenantJobCap,
|
|
377
|
-
changedRows
|
|
490
|
+
changedRows,
|
|
491
|
+
integritySigner: options.integritySigner
|
|
378
492
|
});
|
|
379
493
|
}
|
|
380
494
|
}
|
|
381
495
|
//#endregion
|
|
382
|
-
export { ReportScheduleRunner, SmrtReportRefreshTask, enqueueReportRefresh, ensureReportRefreshSchedules, registerReportRefreshInterceptor };
|
|
496
|
+
export { ReportScheduleRunner, SmrtPrincipalReportRefreshTask, SmrtReportRefreshTask, enqueueReportRefresh, ensureReportRefreshSchedules, registerReportRefreshExecutionAuthorityHost, registerReportRefreshInterceptor, registerReportRefreshJobIntegritySigner };
|
|
383
497
|
|
|
384
498
|
//# sourceMappingURL=scheduler.js.map
|
package/dist/scheduler.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scheduler.js","names":["tenantId"],"sources":["../src/scheduler.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport { EventEmitter } from 'node:events';\nimport {\n field,\n GlobalInterceptors,\n type InterceptorContext,\n ObjectRegistry,\n SmrtObject,\n smrt,\n} from '@happyvertical/smrt-core';\nimport {\n backgroundEligible,\n getNextCronDate,\n type SmrtJob,\n SmrtJobCollection,\n validateCronExpression,\n} from '@happyvertical/smrt-jobs';\nimport {\n getTenantId,\n TenantScoped,\n tenantId,\n} from '@happyvertical/smrt-tenancy';\nimport type { DatabaseInterface, SqlAdapterType } from '@happyvertical/sql';\nimport { buildReportDefinition } from './compiler.js';\nimport { refreshReport } from './refresh.js';\nimport {\n assertReportTablesReady,\n REPORT_SCHEDULER_TABLES,\n REPORT_SCHEDULES_TABLE,\n scopeKeyForTenant,\n} from './state.js';\nimport type {\n ReportDefinition,\n ReportRefreshMode,\n ReportRefreshTrigger,\n ReportSource,\n} from './types.js';\n\ntype ReportCtor = new (...args: any[]) => SmrtObject;\n\nexport interface ReportRefreshJobArgs {\n reportClass?: string;\n mode?: ReportRefreshMode;\n trigger?: ReportRefreshTrigger;\n tenantId?: string | null;\n tenantIds?: string[];\n scheduleId?: string;\n adapterType?: SqlAdapterType;\n changedRows?: Record<string, unknown>[];\n _scheduleId?: string;\n}\n\nexport interface EnqueueReportRefreshOptions extends ReportRefreshJobArgs {\n report?: ReportCtor;\n reportClass: string;\n db: DatabaseInterface;\n queue?: string;\n priority?: number;\n timeout?: number;\n maxAttempts?: number;\n tenantJobCap?: number;\n}\n\nexport interface EnsureReportSchedulesOptions {\n db: DatabaseInterface;\n reports: ReportCtor[];\n tenantIds?: string[];\n queue?: string;\n priority?: number;\n timeout?: number;\n}\n\nexport interface ReportScheduleRunnerConfig {\n id?: string;\n pollInterval?: number;\n batchSize?: number;\n}\n\nexport interface ReportScheduleInfo {\n id: string;\n reportClass: string;\n tenantId: string | null;\n cron: string;\n mode: ReportRefreshMode;\n}\n\nexport interface ReportScheduleRunnerEvents {\n 'schedule:triggered': (schedule: ReportScheduleInfo) => void;\n 'schedule:error': (schedule: ReportScheduleInfo, error: Error) => void;\n 'schedule:completed': (scheduleId: string) => void;\n 'schedule:failed': (scheduleId: string, error: string) => void;\n 'runner:started': () => void;\n 'runner:stopped': () => void;\n 'runner:error': (error: Error) => void;\n}\n\nexport interface ReportRefreshInterceptorOptions {\n db: DatabaseInterface;\n reports: ReportCtor[];\n enqueue?: boolean;\n queue?: string;\n priority?: number;\n timeout?: number;\n tenantJobCap?: number;\n name?: string;\n}\n\nconst INTERNAL_SURFACE = {\n api: false,\n cli: {\n include: ['list', 'get'],\n skipApiCheck: true,\n http: false,\n },\n mcp: false,\n};\n\nfunction stableUuid(values: unknown[]): string {\n const hash = createHash('sha256')\n .update(JSON.stringify(values))\n .digest('hex');\n const variant = ((Number.parseInt(hash[16], 16) & 0x3) | 0x8).toString(16);\n return [\n hash.slice(0, 8),\n hash.slice(8, 12),\n `4${hash.slice(13, 16)}`,\n `${variant}${hash.slice(17, 20)}`,\n hash.slice(20, 32),\n ].join('-');\n}\n\nfunction canonicalClassName(reportCtor: ReportCtor): string {\n const registered =\n ObjectRegistry.getClassByConstructor(reportCtor) ??\n ObjectRegistry.getClass(reportCtor.name);\n return registered?.qualifiedName ?? registered?.name ?? reportCtor.name;\n}\n\nfunction resolveReportClass(name: string): ReportCtor {\n const registered =\n ObjectRegistry.getClassByQualifiedName(name) ??\n ObjectRegistry.getClass(name);\n if (!registered) {\n throw new Error(`Unknown report class: ${name}`);\n }\n return registered.constructor as unknown as ReportCtor;\n}\n\nfunction reportSourceName(source: ReportSource): string {\n if (typeof source === 'string') return source;\n return source.name;\n}\n\nfunction sourceMatches(\n definition: ReportDefinition,\n instance: SmrtObject,\n context: InterceptorContext,\n): boolean {\n const configured = definition.refresh?.onChange;\n if (!configured || configured.length === 0) return false;\n\n const eventNames = new Set<string>([\n context.className,\n instance.constructor.name,\n ]);\n const registered = ObjectRegistry.getClassByConstructor(\n instance.constructor as ReportCtor,\n );\n if (registered?.qualifiedName) eventNames.add(registered.qualifiedName);\n if (registered?.name) eventNames.add(registered.name);\n\n for (const source of configured) {\n const name = reportSourceName(source);\n const registeredSource =\n ObjectRegistry.getClassByQualifiedName(name) ??\n ObjectRegistry.getClass(name);\n if (\n eventNames.has(name) ||\n (registeredSource?.name && eventNames.has(registeredSource.name)) ||\n (registeredSource?.qualifiedName &&\n eventNames.has(registeredSource.qualifiedName))\n ) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction tenantIdFromInstance(instance: SmrtObject): string | null {\n const value = (instance as unknown as { tenantId?: unknown }).tenantId;\n return typeof value === 'string' && value.length > 0\n ? value\n : (getTenantId() ?? null);\n}\n\nfunction changedRowSnapshot(instance: SmrtObject): Record<string, unknown> {\n const serializable = instance.toJSON();\n return serializable && typeof serializable === 'object'\n ? (serializable as Record<string, unknown>)\n : {};\n}\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableName: '_smrt_report_refresh_tasks',\n ...INTERNAL_SURFACE,\n})\nexport class SmrtReportRefreshTask extends SmrtObject {\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n @field({ type: 'text', required: true })\n reportClass: string = '';\n\n @field({ type: 'text', required: true })\n mode: ReportRefreshMode = 'incremental';\n\n @field({ type: 'text', required: true })\n trigger: ReportRefreshTrigger = 'job';\n\n @field({ type: 'json' })\n args: ReportRefreshJobArgs = {};\n\n @backgroundEligible()\n async run(args: ReportRefreshJobArgs = {}): Promise<unknown> {\n const reportClass = args.reportClass || this.reportClass;\n if (!reportClass) {\n throw new Error('Report refresh job requires reportClass');\n }\n\n const reportCtor = resolveReportClass(reportClass);\n return refreshReport(reportCtor, {\n db: this.db,\n mode: args.mode ?? this.mode,\n trigger: args.trigger ?? this.trigger,\n tenantId: args.tenantId,\n tenantIds: args.tenantIds,\n adapterType: args.adapterType,\n scheduleId: args.scheduleId ?? args._scheduleId,\n changedRows: args.changedRows,\n });\n }\n}\n\nexport async function enqueueReportRefresh(\n options: EnqueueReportRefreshOptions,\n): Promise<SmrtJob> {\n await ObjectRegistry.ensureManifestLoaded('SmrtJob');\n const collection = await SmrtJobCollection.create({ db: options.db });\n const taskType = canonicalClassName(SmrtReportRefreshTask);\n const scheduleId = options.scheduleId ?? options._scheduleId;\n\n return collection.enqueueJob(\n {\n tenantId: options.tenantId ?? null,\n queue: options.queue ?? 'reports',\n objectType: taskType,\n objectId: null,\n method: 'run',\n args: {\n reportClass: options.reportClass,\n mode: options.mode,\n trigger: options.trigger ?? 'job',\n tenantId: options.tenantId,\n tenantIds: options.tenantIds,\n adapterType: options.adapterType,\n changedRows: options.changedRows,\n scheduleId,\n _scheduleId: scheduleId,\n },\n priority: options.priority ?? 70,\n timeout: options.timeout ?? 3600000,\n maxAttempts: options.maxAttempts ?? 3,\n },\n { tenantJobCap: options.tenantJobCap },\n );\n}\n\nexport async function ensureReportRefreshSchedules(\n options: EnsureReportSchedulesOptions,\n): Promise<void> {\n await assertReportTablesReady(options.db, REPORT_SCHEDULER_TABLES);\n\n for (const reportCtor of options.reports) {\n const definition = await buildReportDefinition(reportCtor);\n const refresh = definition.refresh;\n if (!refresh || refresh.manual) continue;\n\n const reportClass = canonicalClassName(reportCtor);\n const targetTenants = refresh.tenantFanout\n ? options.tenantIds\n : [null as string | null];\n if (\n refresh.tenantFanout &&\n (!targetTenants || targetTenants.length === 0)\n ) {\n throw new Error(\n `${definition.reportClassName} refresh.tenantFanout requires tenantIds when creating schedules.`,\n );\n }\n\n const schedules = [\n refresh.schedule\n ? {\n cron: refresh.schedule,\n mode: refresh.mode ?? 'incremental',\n trigger: 'schedule' as const,\n }\n : null,\n refresh.fullRebuildSchedule\n ? {\n cron: refresh.fullRebuildSchedule,\n mode: 'rebuild' as const,\n trigger: 'schedule' as const,\n }\n : null,\n ].filter(Boolean) as Array<{\n cron: string;\n mode: ReportRefreshMode;\n trigger: ReportRefreshTrigger;\n }>;\n\n for (const schedule of schedules) {\n validateCronExpression(schedule.cron);\n for (const tenantId of targetTenants ?? []) {\n const scopeKey = scopeKeyForTenant(tenantId);\n const id = stableUuid([\n 'schedule',\n reportClass,\n scopeKey,\n schedule.cron,\n schedule.mode,\n ]);\n const now = new Date().toISOString();\n await options.db.upsert(\n REPORT_SCHEDULES_TABLE,\n ['report_class', 'scope_key', 'cron', 'mode'],\n {\n id,\n slug: id,\n context: scopeKey,\n tenant_id: tenantId,\n scope_key: scopeKey,\n report_class: reportClass,\n cron: schedule.cron,\n trigger: schedule.trigger,\n mode: schedule.mode,\n enabled: true,\n status: 'active',\n next_run: getNextCronDate(schedule.cron).toISOString(),\n last_run: null,\n last_status: null,\n last_error: null,\n run_count: 0,\n success_count: 0,\n failure_count: 0,\n running_count: 0,\n max_concurrent: 1,\n queue: options.queue ?? 'reports',\n priority: options.priority ?? 70,\n timeout: options.timeout ?? 3600000,\n created_at: now,\n updated_at: now,\n },\n );\n }\n }\n }\n}\n\nexport class ReportScheduleRunner extends EventEmitter {\n readonly id: string;\n private readonly config: Required<ReportScheduleRunnerConfig>;\n private db: DatabaseInterface | null = null;\n private running = false;\n private pollTimer: NodeJS.Timeout | null = null;\n\n constructor(config: ReportScheduleRunnerConfig = {}) {\n super();\n this.config = {\n id: config.id || `reports_${stableUuid([Date.now()]).slice(0, 8)}`,\n pollInterval: config.pollInterval ?? 60000,\n batchSize: config.batchSize ?? 50,\n };\n this.id = this.config.id;\n }\n\n async initialize(db: DatabaseInterface): Promise<void> {\n this.db = db;\n await assertReportTablesReady(db, REPORT_SCHEDULER_TABLES);\n }\n\n async start(): Promise<void> {\n if (this.running) return;\n if (!this.db) {\n throw new Error(\n 'ReportScheduleRunner not initialized. Call initialize() first.',\n );\n }\n this.running = true;\n this.startPolling();\n this.emit('runner:started');\n }\n\n async stop(): Promise<void> {\n if (!this.running) return;\n this.running = false;\n if (this.pollTimer) {\n clearTimeout(this.pollTimer);\n this.pollTimer = null;\n }\n this.emit('runner:stopped');\n }\n\n isRunning(): boolean {\n return this.running;\n }\n\n async handleJobCompletion(\n scheduleId: string,\n success: boolean,\n errorMessage?: string,\n ): Promise<void> {\n if (!this.db) return;\n const now = new Date().toISOString();\n if (success) {\n await this.db.query(\n `UPDATE ${REPORT_SCHEDULES_TABLE}\n SET running_count = CASE WHEN COALESCE(running_count, 0) > 0 THEN running_count - 1 ELSE 0 END,\n last_run = ?,\n last_status = 'success',\n last_error = NULL,\n run_count = COALESCE(run_count, 0) + 1,\n success_count = COALESCE(success_count, 0) + 1,\n updated_at = ?\n WHERE id = ?`,\n now,\n now,\n scheduleId,\n );\n this.emit('schedule:completed', scheduleId);\n return;\n }\n\n const safeError = errorMessage ?? 'Unknown error';\n await this.db.query(\n `UPDATE ${REPORT_SCHEDULES_TABLE}\n SET running_count = CASE WHEN COALESCE(running_count, 0) > 0 THEN running_count - 1 ELSE 0 END,\n last_run = ?,\n last_status = 'failed',\n last_error = ?,\n run_count = COALESCE(run_count, 0) + 1,\n failure_count = COALESCE(failure_count, 0) + 1,\n updated_at = ?\n WHERE id = ?`,\n now,\n safeError,\n now,\n scheduleId,\n );\n this.emit('schedule:failed', scheduleId, safeError);\n }\n\n private startPolling(): void {\n const poll = async () => {\n if (!this.running) return;\n try {\n await this.poll();\n } catch (error) {\n this.emit('runner:error', error as Error);\n }\n if (this.running) {\n this.pollTimer = setTimeout(poll, this.config.pollInterval);\n if (typeof this.pollTimer.unref === 'function') {\n this.pollTimer.unref();\n }\n }\n };\n poll();\n }\n\n async poll(): Promise<void> {\n if (!this.db) return;\n const result = await this.db.query(\n `SELECT * FROM ${REPORT_SCHEDULES_TABLE}\n WHERE enabled = true\n AND status = 'active'\n AND next_run <= ?\n AND COALESCE(running_count, 0) < COALESCE(max_concurrent, 1)\n ORDER BY next_run ASC\n LIMIT ?`,\n new Date().toISOString(),\n this.config.batchSize,\n );\n\n for (const row of result.rows) {\n await this.triggerSchedule(row as ReportScheduleRow);\n }\n }\n\n private async triggerSchedule(row: ReportScheduleRow): Promise<void> {\n if (!this.db) return;\n const schedule: ReportScheduleInfo = {\n id: String(row.id),\n reportClass: String(row.report_class),\n tenantId:\n typeof row.tenant_id === 'string' && row.tenant_id.length > 0\n ? row.tenant_id\n : null,\n cron: String(row.cron),\n mode: (row.mode as ReportRefreshMode) || 'incremental',\n };\n\n try {\n const nextRun = getNextCronDate(schedule.cron);\n await enqueueReportRefresh({\n db: this.db,\n reportClass: schedule.reportClass,\n mode: schedule.mode,\n trigger: (row.trigger as ReportRefreshTrigger) || 'schedule',\n tenantId: schedule.tenantId,\n scheduleId: schedule.id,\n queue: String(row.queue || 'reports'),\n priority: Number(row.priority ?? 70),\n timeout: Number(row.timeout ?? 3600000),\n });\n await this.db.query(\n `UPDATE ${REPORT_SCHEDULES_TABLE}\n SET running_count = COALESCE(running_count, 0) + 1,\n next_run = ?,\n updated_at = ?\n WHERE id = ?`,\n nextRun.toISOString(),\n new Date().toISOString(),\n schedule.id,\n );\n this.emit('schedule:triggered', schedule);\n } catch (error) {\n await this.db.query(\n `UPDATE ${REPORT_SCHEDULES_TABLE}\n SET last_error = ?,\n updated_at = ?\n WHERE id = ?`,\n error instanceof Error ? error.message : String(error),\n new Date().toISOString(),\n schedule.id,\n );\n this.emit('schedule:error', schedule, error as Error);\n }\n }\n}\n\ninterface ReportScheduleRow {\n id: unknown;\n tenant_id: unknown;\n report_class: unknown;\n cron: unknown;\n trigger: unknown;\n mode: unknown;\n queue: unknown;\n priority: unknown;\n timeout: unknown;\n}\n\nexport function registerReportRefreshInterceptor(\n options: ReportRefreshInterceptorOptions,\n): () => boolean {\n const name = options.name ?? 'smrt-reports-refresh';\n GlobalInterceptors.register({\n name,\n priority: -10,\n async afterSave(instance, context) {\n await triggerReportsForInstance(options, instance, context);\n },\n async afterDelete(instance, context) {\n await triggerReportsForInstance(options, instance, context);\n },\n });\n return () => GlobalInterceptors.unregister(name);\n}\n\nasync function triggerReportsForInstance(\n options: ReportRefreshInterceptorOptions,\n instance: SmrtObject,\n context: InterceptorContext,\n): Promise<void> {\n for (const reportCtor of options.reports) {\n const definition = await buildReportDefinition(reportCtor);\n if (definition.refresh?.manual) continue;\n if (!sourceMatches(definition, instance, context)) continue;\n\n const mode = definition.refresh?.mode ?? 'incremental';\n const tenantId = tenantIdFromInstance(instance);\n const changedRows = [changedRowSnapshot(instance)];\n if (options.enqueue === false) {\n await refreshReport(reportCtor, {\n db: options.db,\n mode,\n trigger: 'change',\n tenantId,\n changedRows,\n });\n continue;\n }\n\n await enqueueReportRefresh({\n db: options.db,\n reportClass: canonicalClassName(reportCtor),\n mode,\n trigger: 'change',\n tenantId,\n queue: options.queue,\n priority: options.priority,\n timeout: options.timeout,\n tenantJobCap: options.tenantJobCap,\n changedRows,\n });\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"scheduler.js","names":["tenantId"],"sources":["../src/scheduler.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport { EventEmitter } from 'node:events';\nimport {\n field,\n GlobalInterceptors,\n type InterceptorContext,\n ObjectRegistry,\n SmrtObject,\n smrt,\n} from '@happyvertical/smrt-core';\nimport {\n backgroundEligible,\n type DurableJobPayloadIntegrity,\n type DurableJobPayloadSigner,\n getActiveJobExecutionContext,\n getNextCronDate,\n isRunnerExecutionContext,\n type JobExecutionContext,\n type SmrtJob,\n SmrtJobCollection,\n validateCronExpression,\n} from '@happyvertical/smrt-jobs';\nimport {\n getTenantId,\n TenantScoped,\n tenantId,\n} from '@happyvertical/smrt-tenancy';\nimport type { DatabaseInterface, SqlAdapterType } from '@happyvertical/sql';\nimport { buildReportDefinition } from './compiler.js';\nimport { refreshReport } from './refresh.js';\nimport {\n assertReportTablesReady,\n REPORT_SCHEDULER_TABLES,\n REPORT_SCHEDULES_TABLE,\n scopeKeyForTenant,\n} from './state.js';\nimport type {\n ReportDefinition,\n ReportRefreshMode,\n ReportRefreshTrigger,\n ReportSource,\n} from './types.js';\n\ntype ReportCtor = new (...args: any[]) => SmrtObject;\n\nexport interface ReportRefreshJobArgs {\n reportClass?: string;\n mode?: ReportRefreshMode;\n trigger?: ReportRefreshTrigger;\n tenantId?: string | null;\n tenantIds?: string[];\n scheduleId?: string;\n adapterType?: SqlAdapterType;\n changedRows?: Record<string, unknown>[];\n _scheduleId?: string;\n executionAuthority?: ReportRefreshExecutionAuthority;\n integrity?: DurableJobPayloadIntegrity;\n}\n\nexport interface ReportExecutionPrincipalReference {\n version: 1;\n actorUserId: string;\n tenantId: string | null;\n onBehalfOfUserId?: string | null;\n actsAsProfileId?: string | null;\n agentClass?: string | null;\n}\n\nexport interface ReportRefreshExecutionAuthority {\n version: 1;\n hostId: string;\n principal: ReportExecutionPrincipalReference;\n}\n\nexport interface ReportRefreshExecutionAuthorityContext {\n phase: 'execute';\n reportClass: string;\n mode: ReportRefreshMode;\n trigger: ReportRefreshTrigger;\n tenantId: string | null;\n}\n\nexport interface ReportRefreshExecutionAuditEvent\n extends ReportRefreshExecutionAuthorityContext {\n outcome: 'allowed' | 'denied';\n principal: ReportExecutionPrincipalReference;\n reason?: string;\n}\n\nexport interface ReportRefreshExecutionAuthorityHost {\n authorize(\n principal: Readonly<ReportExecutionPrincipalReference>,\n context: Readonly<ReportRefreshExecutionAuthorityContext>,\n ): Promise<void> | void;\n audit(\n event: Readonly<ReportRefreshExecutionAuditEvent>,\n ): Promise<void> | void;\n}\n\nexport interface EnqueueReportRefreshOptions extends ReportRefreshJobArgs {\n report?: ReportCtor;\n reportClass: string;\n db: DatabaseInterface;\n queue?: string;\n priority?: number;\n timeout?: number;\n maxAttempts?: number;\n tenantJobCap?: number;\n /** Server-only signer; register the same key in every worker process. */\n integritySigner?: DurableJobPayloadSigner;\n}\n\nexport interface EnsureReportSchedulesOptions {\n db: DatabaseInterface;\n reports: ReportCtor[];\n tenantIds?: string[];\n queue?: string;\n priority?: number;\n timeout?: number;\n}\n\nexport interface ReportScheduleRunnerConfig {\n id?: string;\n pollInterval?: number;\n batchSize?: number;\n integritySigner?: DurableJobPayloadSigner;\n}\n\nexport interface ReportScheduleInfo {\n id: string;\n reportClass: string;\n tenantId: string | null;\n cron: string;\n mode: ReportRefreshMode;\n}\n\nexport interface ReportScheduleRunnerEvents {\n 'schedule:triggered': (schedule: ReportScheduleInfo) => void;\n 'schedule:error': (schedule: ReportScheduleInfo, error: Error) => void;\n 'schedule:completed': (scheduleId: string) => void;\n 'schedule:failed': (scheduleId: string, error: string) => void;\n 'runner:started': () => void;\n 'runner:stopped': () => void;\n 'runner:error': (error: Error) => void;\n}\n\nexport interface ReportRefreshInterceptorOptions {\n db: DatabaseInterface;\n reports: ReportCtor[];\n enqueue?: boolean;\n queue?: string;\n priority?: number;\n timeout?: number;\n tenantJobCap?: number;\n name?: string;\n integritySigner?: DurableJobPayloadSigner;\n}\n\nconst INTERNAL_SURFACE = {\n api: false,\n cli: {\n include: ['list', 'get'],\n skipApiCheck: true,\n http: false,\n },\n mcp: false,\n};\n\nconst executionAuthorityHosts = new Map<\n string,\n ReportRefreshExecutionAuthorityHost\n>();\nconst jobIntegritySigners = new Map<string, DurableJobPayloadSigner>();\n\nexport function registerReportRefreshJobIntegritySigner(\n signer: DurableJobPayloadSigner,\n): () => void {\n const existing = jobIntegritySigners.get(signer.keyId);\n if (existing && existing !== signer) {\n throw new Error(\n `Report refresh job integrity signer already registered: ${signer.keyId}`,\n );\n }\n jobIntegritySigners.set(signer.keyId, signer);\n return () => {\n if (jobIntegritySigners.get(signer.keyId) === signer) {\n jobIntegritySigners.delete(signer.keyId);\n }\n };\n}\n\n/**\n * Maintenance callers historically configured only their runner/interceptor;\n * they do not have a request host from which to obtain a signer. When one\n * worker-shared signer is registered, use it as the application default. More\n * than one key is ambiguous and remains fail-closed until the caller selects\n * one explicitly.\n */\nfunction resolveReportRefreshIntegritySigner(\n configured: DurableJobPayloadSigner | undefined,\n): DurableJobPayloadSigner | undefined {\n if (configured) return configured;\n if (jobIntegritySigners.size !== 1) return undefined;\n return jobIntegritySigners.values().next().value;\n}\n\nfunction unsignedReportRefreshJobArgs(\n args: ReportRefreshJobArgs,\n): Omit<ReportRefreshJobArgs, 'integrity'> {\n const {\n integrity: _integrity,\n _scheduleId: _internalScheduleId,\n ...unsigned\n } = args;\n return unsigned;\n}\n\nfunction assertReportRefreshJobIntegrity(args: ReportRefreshJobArgs): void {\n const integrity = args.integrity;\n const signer = integrity\n ? jobIntegritySigners.get(integrity.keyId)\n : undefined;\n if (\n !integrity ||\n !signer?.verify(unsignedReportRefreshJobArgs(args), integrity)\n ) {\n throw new Error('Invalid durable report refresh job integrity binding');\n }\n}\n\nfunction assertReportRefreshJobTarget(\n args: ReportRefreshJobArgs,\n context: JobExecutionContext | undefined,\n): void {\n if (!context) return;\n if (!isRunnerExecutionContext(context)) {\n throw new Error('Invalid durable report refresh job context');\n }\n const expectedType = canonicalClassName(\n (args.trigger ?? 'job') === 'manual'\n ? SmrtPrincipalReportRefreshTask\n : SmrtReportRefreshTask,\n );\n if (context.job.objectType !== expectedType || context.job.method !== 'run') {\n throw new Error('Invalid durable report refresh job target');\n }\n}\n\nexport function registerReportRefreshExecutionAuthorityHost(\n hostId: string,\n host: ReportRefreshExecutionAuthorityHost,\n): () => void {\n if (!hostId || hostId.length > 256) {\n throw new Error(\n 'Report refresh authority hostId must contain 1-256 characters',\n );\n }\n const existing = executionAuthorityHosts.get(hostId);\n if (existing && existing !== host) {\n throw new Error(\n `Report refresh authority host already registered: ${hostId}`,\n );\n }\n executionAuthorityHosts.set(hostId, host);\n return () => {\n if (executionAuthorityHosts.get(hostId) === host) {\n executionAuthorityHosts.delete(hostId);\n }\n };\n}\n\nfunction stableUuid(values: unknown[]): string {\n const hash = createHash('sha256')\n .update(JSON.stringify(values))\n .digest('hex');\n const variant = ((Number.parseInt(hash[16], 16) & 0x3) | 0x8).toString(16);\n return [\n hash.slice(0, 8),\n hash.slice(8, 12),\n `4${hash.slice(13, 16)}`,\n `${variant}${hash.slice(17, 20)}`,\n hash.slice(20, 32),\n ].join('-');\n}\n\nfunction canonicalClassName(reportCtor: ReportCtor): string {\n const registered =\n ObjectRegistry.getClassByConstructor(reportCtor) ??\n ObjectRegistry.getClass(reportCtor.name);\n return registered?.qualifiedName ?? registered?.name ?? reportCtor.name;\n}\n\nfunction resolveReportClass(name: string): ReportCtor {\n const registered =\n ObjectRegistry.getClassByQualifiedName(name) ??\n ObjectRegistry.getClass(name);\n if (!registered) {\n throw new Error(`Unknown report class: ${name}`);\n }\n return registered.constructor as unknown as ReportCtor;\n}\n\nfunction reportSourceName(source: ReportSource): string {\n if (typeof source === 'string') return source;\n return source.name;\n}\n\nfunction sourceMatches(\n definition: ReportDefinition,\n instance: SmrtObject,\n context: InterceptorContext,\n): boolean {\n const configured = definition.refresh?.onChange;\n if (!configured || configured.length === 0) return false;\n\n const eventNames = new Set<string>([\n context.className,\n instance.constructor.name,\n ]);\n const registered = ObjectRegistry.getClassByConstructor(\n instance.constructor as ReportCtor,\n );\n if (registered?.qualifiedName) eventNames.add(registered.qualifiedName);\n if (registered?.name) eventNames.add(registered.name);\n\n for (const source of configured) {\n const name = reportSourceName(source);\n const registeredSource =\n ObjectRegistry.getClassByQualifiedName(name) ??\n ObjectRegistry.getClass(name);\n if (\n eventNames.has(name) ||\n (registeredSource?.name && eventNames.has(registeredSource.name)) ||\n (registeredSource?.qualifiedName &&\n eventNames.has(registeredSource.qualifiedName))\n ) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction tenantIdFromInstance(instance: SmrtObject): string | null {\n const value = (instance as unknown as { tenantId?: unknown }).tenantId;\n return typeof value === 'string' && value.length > 0\n ? value\n : (getTenantId() ?? null);\n}\n\nfunction changedRowSnapshot(instance: SmrtObject): Record<string, unknown> {\n const serializable = instance.toJSON();\n return serializable && typeof serializable === 'object'\n ? (serializable as Record<string, unknown>)\n : {};\n}\n\nasync function authorizeReportRefreshExecution(\n args: ReportRefreshJobArgs,\n reportClass: string,\n jobTenantId: string | null,\n): Promise<void> {\n if ((args.tenantId ?? null) !== jobTenantId) {\n throw new Error('Invalid report refresh execution tenant');\n }\n const authority = args.executionAuthority;\n if ((args.trigger ?? 'job') === 'manual' && !authority) {\n throw new Error('Manual report refresh execution authority is missing');\n }\n // Scheduled and on-change maintenance jobs predate user-bound actions and\n // intentionally run under the worker's system authority.\n if (!authority) return;\n const principal = authority.principal;\n const tenantId = args.tenantId ?? null;\n if (\n authority.version !== 1 ||\n principal?.version !== 1 ||\n !authority.hostId ||\n !principal.actorUserId ||\n principal.tenantId !== tenantId ||\n principal.tenantId !== jobTenantId\n ) {\n throw new Error('Invalid report refresh execution authority');\n }\n const host = executionAuthorityHosts.get(authority.hostId);\n if (!host) {\n throw new Error(\n `No report refresh authority host registered for ${authority.hostId}`,\n );\n }\n const context: ReportRefreshExecutionAuthorityContext = {\n phase: 'execute',\n reportClass,\n mode: args.mode ?? 'incremental',\n trigger: args.trigger ?? 'job',\n tenantId,\n };\n try {\n await host.authorize(\n Object.freeze({ ...principal }),\n Object.freeze(context),\n );\n } catch {\n await host.audit({\n ...context,\n outcome: 'denied',\n principal: Object.freeze({ ...principal }),\n reason: 'current_authority_denied',\n });\n throw new Error('Report refresh execution authority denied');\n }\n await host.audit({\n ...context,\n outcome: 'allowed',\n principal: Object.freeze({ ...principal }),\n });\n}\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableName: '_smrt_report_refresh_tasks',\n ...INTERNAL_SURFACE,\n})\nexport class SmrtReportRefreshTask extends SmrtObject {\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n @field({ type: 'text', required: true })\n reportClass: string = '';\n\n @field({ type: 'text', required: true })\n mode: ReportRefreshMode = 'incremental';\n\n @field({ type: 'text', required: true })\n trigger: ReportRefreshTrigger = 'job';\n\n @field({ type: 'json' })\n args: ReportRefreshJobArgs = {};\n\n @backgroundEligible()\n async run(\n args: ReportRefreshJobArgs = {},\n context?: JobExecutionContext,\n ): Promise<unknown> {\n assertReportRefreshJobIntegrity(args);\n const executionContext = getActiveJobExecutionContext() ?? context;\n assertReportRefreshJobTarget(args, executionContext);\n const reportClass = args.reportClass;\n if (!reportClass) {\n throw new Error('Report refresh job requires reportClass');\n }\n const mode = args.mode ?? 'incremental';\n const trigger = args.trigger ?? 'job';\n\n const reportCtor = resolveReportClass(reportClass);\n // A runner context, including its explicit global `null` tenant, owns the\n // scope. Only direct callers without runner context may use instance scope.\n let jobTenantId: string | null;\n if (executionContext) {\n const runnerTenantId = executionContext.job.tenantId;\n if (runnerTenantId === null) {\n jobTenantId = null;\n } else if (\n typeof runnerTenantId === 'string' &&\n runnerTenantId.length > 0\n ) {\n jobTenantId = runnerTenantId;\n } else {\n throw new Error('Invalid report refresh execution tenant');\n }\n } else {\n jobTenantId = tenantIdFromInstance(this);\n }\n await authorizeReportRefreshExecution(args, reportClass, jobTenantId);\n return refreshReport(reportCtor, {\n db: this.db,\n mode,\n trigger,\n tenantId: jobTenantId,\n tenantIds: args.tenantIds,\n adapterType: args.adapterType,\n scheduleId: args.scheduleId ?? args._scheduleId,\n changedRows: args.changedRows,\n });\n }\n}\n\n/** Worker target whose authority requirement cannot be downgraded by job args. */\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableName: '_smrt_principal_report_refresh_tasks',\n ...INTERNAL_SURFACE,\n})\nexport class SmrtPrincipalReportRefreshTask extends SmrtReportRefreshTask {\n override async run(\n args: ReportRefreshJobArgs = {},\n context?: JobExecutionContext,\n ): Promise<unknown> {\n return super.run({ ...args, trigger: 'manual' }, context);\n }\n}\n\nexport async function enqueueReportRefresh(\n options: EnqueueReportRefreshOptions,\n): Promise<SmrtJob> {\n const integritySigner = resolveReportRefreshIntegritySigner(\n options.integritySigner,\n );\n if (!integritySigner) {\n throw new Error(\n 'Report refresh queue requires a durable job integrity signer',\n );\n }\n if (options.trigger === 'manual' && !options.executionAuthority) {\n throw new Error('Manual report refresh requires execution-time authority');\n }\n if (\n options.executionAuthority &&\n (options.executionAuthority.version !== 1 ||\n options.executionAuthority.principal?.version !== 1 ||\n !options.executionAuthority.hostId ||\n !options.executionAuthority.principal.actorUserId ||\n options.executionAuthority.principal.tenantId !==\n (options.tenantId ?? null))\n ) {\n throw new Error('Invalid report refresh execution authority');\n }\n if (\n options.executionAuthority &&\n (options.trigger !== 'manual' || (options.tenantIds?.length ?? 0) > 0)\n ) {\n throw new Error(\n 'Principal-bound report refresh requires one manual tenant scope',\n );\n }\n await ObjectRegistry.ensureManifestLoaded('SmrtJob');\n const collection = await SmrtJobCollection.create({ db: options.db });\n const taskType = canonicalClassName(\n options.trigger === 'manual'\n ? SmrtPrincipalReportRefreshTask\n : SmrtReportRefreshTask,\n );\n const scheduleId = options.scheduleId ?? options._scheduleId;\n\n const unsignedArgs: Omit<ReportRefreshJobArgs, 'integrity'> = {\n reportClass: options.reportClass,\n mode: options.mode ?? 'incremental',\n trigger: options.trigger ?? 'job',\n tenantId: options.tenantId,\n tenantIds: options.tenantIds,\n adapterType: options.adapterType,\n changedRows: options.changedRows,\n scheduleId,\n executionAuthority: options.executionAuthority,\n };\n const integrity = integritySigner.sign(unsignedArgs);\n if (!integritySigner.verify(unsignedArgs, integrity)) {\n throw new Error(\n 'Report refresh job integrity signer rejected its queued payload',\n );\n }\n return collection.enqueueJob(\n {\n tenantId: options.tenantId ?? null,\n queue: options.queue ?? 'reports',\n objectType: taskType,\n objectId: null,\n method: 'run',\n args: { ...unsignedArgs, _scheduleId: scheduleId, integrity },\n priority: options.priority ?? 70,\n timeout: options.timeout ?? 3600000,\n maxAttempts: options.maxAttempts ?? 3,\n },\n { tenantJobCap: options.tenantJobCap },\n );\n}\n\nexport async function ensureReportRefreshSchedules(\n options: EnsureReportSchedulesOptions,\n): Promise<void> {\n await assertReportTablesReady(options.db, REPORT_SCHEDULER_TABLES);\n\n for (const reportCtor of options.reports) {\n const definition = await buildReportDefinition(reportCtor);\n const refresh = definition.refresh;\n if (!refresh || refresh.manual) continue;\n\n const reportClass = canonicalClassName(reportCtor);\n const targetTenants = refresh.tenantFanout\n ? options.tenantIds\n : [null as string | null];\n if (\n refresh.tenantFanout &&\n (!targetTenants || targetTenants.length === 0)\n ) {\n throw new Error(\n `${definition.reportClassName} refresh.tenantFanout requires tenantIds when creating schedules.`,\n );\n }\n\n const schedules = [\n refresh.schedule\n ? {\n cron: refresh.schedule,\n mode: refresh.mode ?? 'incremental',\n trigger: 'schedule' as const,\n }\n : null,\n refresh.fullRebuildSchedule\n ? {\n cron: refresh.fullRebuildSchedule,\n mode: 'rebuild' as const,\n trigger: 'schedule' as const,\n }\n : null,\n ].filter(Boolean) as Array<{\n cron: string;\n mode: ReportRefreshMode;\n trigger: ReportRefreshTrigger;\n }>;\n\n for (const schedule of schedules) {\n validateCronExpression(schedule.cron);\n for (const tenantId of targetTenants ?? []) {\n const scopeKey = scopeKeyForTenant(tenantId);\n const id = stableUuid([\n 'schedule',\n reportClass,\n scopeKey,\n schedule.cron,\n schedule.mode,\n ]);\n const now = new Date().toISOString();\n await options.db.upsert(\n REPORT_SCHEDULES_TABLE,\n ['report_class', 'scope_key', 'cron', 'mode'],\n {\n id,\n slug: id,\n context: scopeKey,\n tenant_id: tenantId,\n scope_key: scopeKey,\n report_class: reportClass,\n cron: schedule.cron,\n trigger: schedule.trigger,\n mode: schedule.mode,\n enabled: true,\n status: 'active',\n next_run: getNextCronDate(schedule.cron).toISOString(),\n last_run: null,\n last_status: null,\n last_error: null,\n run_count: 0,\n success_count: 0,\n failure_count: 0,\n running_count: 0,\n max_concurrent: 1,\n queue: options.queue ?? 'reports',\n priority: options.priority ?? 70,\n timeout: options.timeout ?? 3600000,\n created_at: now,\n updated_at: now,\n },\n );\n }\n }\n }\n}\n\nexport class ReportScheduleRunner extends EventEmitter {\n readonly id: string;\n private readonly config: Required<\n Omit<ReportScheduleRunnerConfig, 'integritySigner'>\n > &\n Pick<ReportScheduleRunnerConfig, 'integritySigner'>;\n private db: DatabaseInterface | null = null;\n private running = false;\n private pollTimer: NodeJS.Timeout | null = null;\n\n constructor(config: ReportScheduleRunnerConfig = {}) {\n super();\n this.config = {\n id: config.id || `reports_${stableUuid([Date.now()]).slice(0, 8)}`,\n pollInterval: config.pollInterval ?? 60000,\n batchSize: config.batchSize ?? 50,\n integritySigner: config.integritySigner,\n };\n this.id = this.config.id;\n }\n\n async initialize(db: DatabaseInterface): Promise<void> {\n this.db = db;\n await assertReportTablesReady(db, REPORT_SCHEDULER_TABLES);\n }\n\n async start(): Promise<void> {\n if (this.running) return;\n if (!this.db) {\n throw new Error(\n 'ReportScheduleRunner not initialized. Call initialize() first.',\n );\n }\n this.running = true;\n this.startPolling();\n this.emit('runner:started');\n }\n\n async stop(): Promise<void> {\n if (!this.running) return;\n this.running = false;\n if (this.pollTimer) {\n clearTimeout(this.pollTimer);\n this.pollTimer = null;\n }\n this.emit('runner:stopped');\n }\n\n isRunning(): boolean {\n return this.running;\n }\n\n async handleJobCompletion(\n scheduleId: string,\n success: boolean,\n errorMessage?: string,\n ): Promise<void> {\n if (!this.db) return;\n const now = new Date().toISOString();\n if (success) {\n await this.db.query(\n `UPDATE ${REPORT_SCHEDULES_TABLE}\n SET running_count = CASE WHEN COALESCE(running_count, 0) > 0 THEN running_count - 1 ELSE 0 END,\n last_run = ?,\n last_status = 'success',\n last_error = NULL,\n run_count = COALESCE(run_count, 0) + 1,\n success_count = COALESCE(success_count, 0) + 1,\n updated_at = ?\n WHERE id = ?`,\n now,\n now,\n scheduleId,\n );\n this.emit('schedule:completed', scheduleId);\n return;\n }\n\n const safeError = errorMessage ?? 'Unknown error';\n await this.db.query(\n `UPDATE ${REPORT_SCHEDULES_TABLE}\n SET running_count = CASE WHEN COALESCE(running_count, 0) > 0 THEN running_count - 1 ELSE 0 END,\n last_run = ?,\n last_status = 'failed',\n last_error = ?,\n run_count = COALESCE(run_count, 0) + 1,\n failure_count = COALESCE(failure_count, 0) + 1,\n updated_at = ?\n WHERE id = ?`,\n now,\n safeError,\n now,\n scheduleId,\n );\n this.emit('schedule:failed', scheduleId, safeError);\n }\n\n private startPolling(): void {\n const poll = async () => {\n if (!this.running) return;\n try {\n await this.poll();\n } catch (error) {\n this.emit('runner:error', error as Error);\n }\n if (this.running) {\n this.pollTimer = setTimeout(poll, this.config.pollInterval);\n if (typeof this.pollTimer.unref === 'function') {\n this.pollTimer.unref();\n }\n }\n };\n poll();\n }\n\n async poll(): Promise<void> {\n if (!this.db) return;\n const result = await this.db.query(\n `SELECT * FROM ${REPORT_SCHEDULES_TABLE}\n WHERE enabled = true\n AND status = 'active'\n AND next_run <= ?\n AND COALESCE(running_count, 0) < COALESCE(max_concurrent, 1)\n ORDER BY next_run ASC\n LIMIT ?`,\n new Date().toISOString(),\n this.config.batchSize,\n );\n\n for (const row of result.rows) {\n await this.triggerSchedule(row as ReportScheduleRow);\n }\n }\n\n private async triggerSchedule(row: ReportScheduleRow): Promise<void> {\n if (!this.db) return;\n const schedule: ReportScheduleInfo = {\n id: String(row.id),\n reportClass: String(row.report_class),\n tenantId:\n typeof row.tenant_id === 'string' && row.tenant_id.length > 0\n ? row.tenant_id\n : null,\n cron: String(row.cron),\n mode: (row.mode as ReportRefreshMode) || 'incremental',\n };\n\n try {\n const nextRun = getNextCronDate(schedule.cron);\n await enqueueReportRefresh({\n db: this.db,\n reportClass: schedule.reportClass,\n mode: schedule.mode,\n trigger: (row.trigger as ReportRefreshTrigger) || 'schedule',\n tenantId: schedule.tenantId,\n scheduleId: schedule.id,\n queue: String(row.queue || 'reports'),\n priority: Number(row.priority ?? 70),\n timeout: Number(row.timeout ?? 3600000),\n integritySigner: this.config.integritySigner,\n });\n await this.db.query(\n `UPDATE ${REPORT_SCHEDULES_TABLE}\n SET running_count = COALESCE(running_count, 0) + 1,\n next_run = ?,\n updated_at = ?\n WHERE id = ?`,\n nextRun.toISOString(),\n new Date().toISOString(),\n schedule.id,\n );\n this.emit('schedule:triggered', schedule);\n } catch (error) {\n await this.db.query(\n `UPDATE ${REPORT_SCHEDULES_TABLE}\n SET last_error = ?,\n updated_at = ?\n WHERE id = ?`,\n error instanceof Error ? error.message : String(error),\n new Date().toISOString(),\n schedule.id,\n );\n this.emit('schedule:error', schedule, error as Error);\n }\n }\n}\n\ninterface ReportScheduleRow {\n id: unknown;\n tenant_id: unknown;\n report_class: unknown;\n cron: unknown;\n trigger: unknown;\n mode: unknown;\n queue: unknown;\n priority: unknown;\n timeout: unknown;\n}\n\nexport function registerReportRefreshInterceptor(\n options: ReportRefreshInterceptorOptions,\n): () => boolean {\n const name = options.name ?? 'smrt-reports-refresh';\n GlobalInterceptors.register({\n name,\n priority: -10,\n async afterSave(instance, context) {\n await triggerReportsForInstance(options, instance, context);\n },\n async afterDelete(instance, context) {\n await triggerReportsForInstance(options, instance, context);\n },\n });\n return () => GlobalInterceptors.unregister(name);\n}\n\nasync function triggerReportsForInstance(\n options: ReportRefreshInterceptorOptions,\n instance: SmrtObject,\n context: InterceptorContext,\n): Promise<void> {\n for (const reportCtor of options.reports) {\n const definition = await buildReportDefinition(reportCtor);\n if (definition.refresh?.manual) continue;\n if (!sourceMatches(definition, instance, context)) continue;\n\n const mode = definition.refresh?.mode ?? 'incremental';\n const tenantId = tenantIdFromInstance(instance);\n const changedRows = [changedRowSnapshot(instance)];\n if (options.enqueue === false) {\n await refreshReport(reportCtor, {\n db: options.db,\n mode,\n trigger: 'change',\n tenantId,\n changedRows,\n });\n continue;\n }\n\n await enqueueReportRefresh({\n db: options.db,\n reportClass: canonicalClassName(reportCtor),\n mode,\n trigger: 'change',\n tenantId,\n queue: options.queue,\n priority: options.priority,\n timeout: options.timeout,\n tenantJobCap: options.tenantJobCap,\n changedRows,\n integritySigner: options.integritySigner,\n });\n }\n}\n"],"mappings":"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"}
|
package/dist/smrt-knowledge.json
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
"sensitiveFieldsExcluded": true,
|
|
4
4
|
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
5
5
|
"packageName": "@happyvertical/smrt-reports",
|
|
6
|
-
"packageVersion": "0.49.
|
|
6
|
+
"packageVersion": "0.49.4",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
10
|
+
"manifest": "dbdbbf364cc13c70e36e709c73828630d926d32c18bf6be5736104620fa9491c",
|
|
11
|
+
"packageJson": "1f26f803797709740289cc00e8d77461e36c4da780b07d789393f34bd7ab69dd",
|
|
12
12
|
"agents": "1328f1da3d58fba2369d696cb9fa42c9d324255257978fa3ae9d1086e9de5af6"
|
|
13
13
|
},
|
|
14
14
|
"exports": [
|
|
@@ -180,7 +180,8 @@
|
|
|
180
180
|
"name": "run",
|
|
181
181
|
"async": true,
|
|
182
182
|
"params": [
|
|
183
|
-
"args?: ReportRefreshJobArgs"
|
|
183
|
+
"args?: ReportRefreshJobArgs",
|
|
184
|
+
"context?: JobExecutionContext"
|
|
184
185
|
],
|
|
185
186
|
"returns": "Promise<unknown>"
|
|
186
187
|
}
|
|
@@ -224,6 +225,75 @@
|
|
|
224
225
|
"tags": [],
|
|
225
226
|
"risks": []
|
|
226
227
|
},
|
|
228
|
+
{
|
|
229
|
+
"name": "SmrtPrincipalReportRefreshTask",
|
|
230
|
+
"qualifiedName": "@happyvertical/smrt-reports:SmrtPrincipalReportRefreshTask",
|
|
231
|
+
"collection": "smrtprincipalreportrefreshtasks",
|
|
232
|
+
"tableName": "_smrt_principal_report_refresh_tasks",
|
|
233
|
+
"packageName": "@happyvertical/smrt-reports",
|
|
234
|
+
"extends": "SmrtReportRefreshTask",
|
|
235
|
+
"fields": [
|
|
236
|
+
{
|
|
237
|
+
"name": "tenantId",
|
|
238
|
+
"type": "text",
|
|
239
|
+
"required": false,
|
|
240
|
+
"columnType": "UUID"
|
|
241
|
+
}
|
|
242
|
+
],
|
|
243
|
+
"relationships": [],
|
|
244
|
+
"methods": [
|
|
245
|
+
"run"
|
|
246
|
+
],
|
|
247
|
+
"methodSignatures": [
|
|
248
|
+
{
|
|
249
|
+
"name": "run",
|
|
250
|
+
"async": true,
|
|
251
|
+
"params": [
|
|
252
|
+
"args?: ReportRefreshJobArgs",
|
|
253
|
+
"context?: JobExecutionContext"
|
|
254
|
+
],
|
|
255
|
+
"returns": "Promise<unknown>"
|
|
256
|
+
}
|
|
257
|
+
],
|
|
258
|
+
"tenant": {
|
|
259
|
+
"scoped": true,
|
|
260
|
+
"mode": "optional",
|
|
261
|
+
"field": "tenantId"
|
|
262
|
+
},
|
|
263
|
+
"conflictColumns": [
|
|
264
|
+
"tenant_id",
|
|
265
|
+
"slug",
|
|
266
|
+
"context"
|
|
267
|
+
],
|
|
268
|
+
"surfaces": [
|
|
269
|
+
{
|
|
270
|
+
"kind": "cli",
|
|
271
|
+
"name": "smrtprincipalreportrefreshtask_list",
|
|
272
|
+
"operation": "list",
|
|
273
|
+
"objectName": "@happyvertical/smrt-reports:SmrtPrincipalReportRefreshTask"
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
"kind": "cli",
|
|
277
|
+
"name": "smrtprincipalreportrefreshtask_get",
|
|
278
|
+
"operation": "get",
|
|
279
|
+
"objectName": "@happyvertical/smrt-reports:SmrtPrincipalReportRefreshTask"
|
|
280
|
+
}
|
|
281
|
+
],
|
|
282
|
+
"withheldSurfaces": [
|
|
283
|
+
{
|
|
284
|
+
"kind": "api",
|
|
285
|
+
"operation": "run",
|
|
286
|
+
"code": "api-disabled",
|
|
287
|
+
"reason": "api is disabled",
|
|
288
|
+
"objectName": "@happyvertical/smrt-reports:SmrtPrincipalReportRefreshTask"
|
|
289
|
+
}
|
|
290
|
+
],
|
|
291
|
+
"relationshipFeatures": [
|
|
292
|
+
"uuidColumns"
|
|
293
|
+
],
|
|
294
|
+
"tags": [],
|
|
295
|
+
"risks": []
|
|
296
|
+
},
|
|
227
297
|
{
|
|
228
298
|
"name": "SmrtReportRun",
|
|
229
299
|
"qualifiedName": "@happyvertical/smrt-reports:SmrtReportRun",
|
|
@@ -765,6 +835,18 @@
|
|
|
765
835
|
"operation": "get",
|
|
766
836
|
"objectName": "@happyvertical/smrt-reports:SmrtReportRefreshTask"
|
|
767
837
|
},
|
|
838
|
+
{
|
|
839
|
+
"kind": "cli",
|
|
840
|
+
"name": "smrtprincipalreportrefreshtask_list",
|
|
841
|
+
"operation": "list",
|
|
842
|
+
"objectName": "@happyvertical/smrt-reports:SmrtPrincipalReportRefreshTask"
|
|
843
|
+
},
|
|
844
|
+
{
|
|
845
|
+
"kind": "cli",
|
|
846
|
+
"name": "smrtprincipalreportrefreshtask_get",
|
|
847
|
+
"operation": "get",
|
|
848
|
+
"objectName": "@happyvertical/smrt-reports:SmrtPrincipalReportRefreshTask"
|
|
849
|
+
},
|
|
768
850
|
{
|
|
769
851
|
"kind": "cli",
|
|
770
852
|
"name": "smrtreportrun_list",
|
|
@@ -821,7 +903,7 @@
|
|
|
821
903
|
"junctionCollections": 0,
|
|
822
904
|
"hierarchicalObjects": 0,
|
|
823
905
|
"polymorphicAssociations": 0,
|
|
824
|
-
"uuidColumns":
|
|
906
|
+
"uuidColumns": 20
|
|
825
907
|
},
|
|
826
908
|
"agentDoc": "# @happyvertical/smrt-reports\n\nMaterialized aggregate report models for SMRT.\n\n## Key Pieces\n\n| Module | Purpose |\n| --- | --- |\n| `SmrtReport` | Abstract report row base with `refreshedAt`, `isStale()`, and manual `refresh()` |\n| decorators | `@report`, grouping decorators, time buckets, and aggregate measure decorators |\n| compiler | Pure `ReportDefinition -> AggregateSpec` compiler and ObjectRegistry adapter |\n| aggregate | Compatibility re-export of the SDK aggregate query builder |\n| refresh | Rebuild and incremental refresh engine with run tracking, watermarks, locks, and tenant scoping |\n| state | Internal `_smrt_report_*` system models for runs, watermarks, locks, schedules, and refresh tasks |\n| scheduler | Cron schedule runner, durable refresh job enqueueing, and `onChange` interceptor registration |\n| adapter | Transport-neutral report descriptor, canonical materialized-row reads, and stable `id` row identity |\n| lifecycle | Tenant-safe freshness, run, lock, failure, and manual refresh preview/apply surfaces |\n| views | Policy-revalidated saved views, snapshot-bound exports, bounded job handoff, and artifact metadata validation |\n\n## Adapter contract\n\n- `buildReportAdapterDescriptor()` returns deterministic, serializable metadata\n for a report surface: a stable resource id, typed persisted report columns,\n the canonical `DataQuerySchema`, and UI-neutral DataTable hints. It must not\n import `smrt-ui` or expose a report-domain class to the consumer.\n- `queryReportMaterializedRows()` owns only the bounded read slice for rows that\n are already materialized. It supports projection, offset/limit paging,\n validated filters, deterministic multi-sort with an `id` tie-breaker, exact\n totals, and dimension facets. At source-query compilation,\n dimension and bucket filters compile to `WHERE`, aggregate-measure filters\n compile to `HAVING`, and mixed `OR`/`NOT` filter scopes fail closed.\n- `id` is the only row identity. It must be a non-empty persisted string and is\n never replaced by a display index or page position.\n- The descriptor is an exposure boundary. Sensitive/secret fields, fields with\n `readPermission`, and transient, system, or non-column fields fail closed and\n do not become public columns when no principal is available.\n- `tenantScoped`/`tenantField` reflect actual registered tenant metadata. A\n `tenantScope` option only contributes to the stable resource id; it is not\n authorization. Pass the same `adapter` options to\n `queryReportMaterializedRows()` that were used to build the selected\n descriptor, so results and background tasks retain that stable resource id.\n The default query path resolves the registered collection via `ObjectRegistry`,\n so normal collection tenancy interceptors apply. An injected collection is\n application-owned and must preserve the same boundary.\n- `refresh` is a declaration, not execution. It describes configured mode,\n triggers, positive-TTL stale-read behavior, and a permissioned/audited action\n with preview/apply phases. The adapter remains read-only, while\n `getReportLifecycle()` provides an explicit, tenant-safe lifecycle snapshot\n and `previewReportRefresh()` / `applyReportRefresh()` delegate authorization,\n audit, and queueing through an application action host. Only a registered\n `SmrtReportCollection` may synchronously refresh stale reads, when its TTL is\n positive and the report is not manual.\n- `views` accepts no authority. Persisted views must be normalized again through\n the current descriptor before restoring them, so changed column policy or a\n changed report definition fails closed. Treat unversioned persisted layouts as\n legacy v0 and migrate them to v1 before normalization; unknown versions must\n fail rather than being inferred. Export snapshots retain the canonical\n query fingerprint, projection, sort, as-of/freshness state, exact row count,\n and the fixed principal/tenant/report-definition/field-policy inheritance\n contract. A snapshot needs an application-host-issued opaque binding and an\n explicit offset-page read plan: workers advance it from offset zero instead\n of reusing a visible page. Preview, apply, worker, and artifact-serving\n boundaries must call the validators, including the host's immutable-snapshot\n assertion; application hosts own authorization, audit records, durable\n storage, download tokens, and queue execution.\n\n## Conventions\n\n- Report cache tables are normal `@smrt()` tables. Runtime refresh must not create schema.\n- Store report metadata under field `_meta.__report`; scanner and runtime decorators must stay aligned.\n- Keep SQL generation portable. Use the SDK `buildAggregate()`/`bucketExpr()` helpers for time buckets and `$N` placeholders.\n- Do not add a local aggregate SQL builder here; the implementation lives in `@happyvertical/sql`.\n- 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.\n- Incremental refresh requires a source watermark column (default `updatedAt`) and soft-delete column (default `deletedAt`); it recomputes affected groups and deletes empty report groups instead of applying aggregate deltas.\n- Raw aggregate refreshes must explicitly filter `tenant_id`; the tenancy interceptor only protects normal collection reads.\n- Scheduled/on-change refreshes enqueue `SmrtReportRefreshTask.run()` through `@happyvertical/smrt-jobs`; do not add a separate report queue.\n"
|
|
827
909
|
}
|