@happyvertical/smrt-reports 0.37.2 → 0.37.3
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/dist/aggregate.js +1 -5
- package/dist/compiler.js +138 -185
- package/dist/compiler.js.map +1 -1
- package/dist/decorators.js +53 -68
- package/dist/decorators.js.map +1 -1
- package/dist/index.js +80 -152
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +2 -2
- package/dist/refresh.js +423 -667
- package/dist/refresh.js.map +1 -1
- package/dist/scheduler.js +330 -395
- package/dist/scheduler.js.map +1 -1
- package/dist/smrt-knowledge.json +7 -7
- package/dist/state.js +305 -305
- package/dist/state.js.map +1 -1
- package/dist/types.js +0 -2
- package/package.json +9 -9
- package/dist/aggregate.js.map +0 -1
- package/dist/types.js.map +0 -1
package/dist/scheduler.js
CHANGED
|
@@ -1,284 +1,251 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { EventEmitter } from "node:events";
|
|
3
|
-
import { field, smrt, SmrtObject, ObjectRegistry, GlobalInterceptors } from "@happyvertical/smrt-core";
|
|
4
|
-
import { backgroundEligible, getNextCronDate, SmrtJobCollection, validateCronExpression } from "@happyvertical/smrt-jobs";
|
|
5
|
-
import { tenantId, TenantScoped, getTenantId } from "@happyvertical/smrt-tenancy";
|
|
6
1
|
import { buildReportDefinition } from "./compiler.js";
|
|
2
|
+
import { REPORT_SCHEDULER_TABLES, REPORT_SCHEDULES_TABLE, assertReportTablesReady, scopeKeyForTenant } from "./state.js";
|
|
7
3
|
import { refreshReport } from "./refresh.js";
|
|
8
|
-
import {
|
|
4
|
+
import { GlobalInterceptors, ObjectRegistry, SmrtObject, field, smrt } from "@happyvertical/smrt-core";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { TenantScoped, getTenantId, tenantId } from "@happyvertical/smrt-tenancy";
|
|
7
|
+
import { EventEmitter } from "node:events";
|
|
8
|
+
import { SmrtJobCollection, backgroundEligible, getNextCronDate, validateCronExpression } from "@happyvertical/smrt-jobs";
|
|
9
|
+
//#region src/scheduler.ts
|
|
9
10
|
var __defProp = Object.defineProperty;
|
|
10
11
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
11
12
|
var __decorateClass = (decorators, target, key, kind) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (kind && result) __defProp(target, key, result);
|
|
17
|
-
return result;
|
|
13
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
14
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
15
|
+
if (kind && result) __defProp(target, key, result);
|
|
16
|
+
return result;
|
|
18
17
|
};
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
18
|
+
var INTERNAL_SURFACE = {
|
|
19
|
+
api: false,
|
|
20
|
+
cli: {
|
|
21
|
+
include: ["list", "get"],
|
|
22
|
+
skipApiCheck: true,
|
|
23
|
+
http: false
|
|
24
|
+
},
|
|
25
|
+
mcp: false
|
|
27
26
|
};
|
|
28
27
|
function stableUuid(values) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
28
|
+
const hash = createHash("sha256").update(JSON.stringify(values)).digest("hex");
|
|
29
|
+
const variant = (Number.parseInt(hash[16], 16) & 3 | 8).toString(16);
|
|
30
|
+
return [
|
|
31
|
+
hash.slice(0, 8),
|
|
32
|
+
hash.slice(8, 12),
|
|
33
|
+
`4${hash.slice(13, 16)}`,
|
|
34
|
+
`${variant}${hash.slice(17, 20)}`,
|
|
35
|
+
hash.slice(20, 32)
|
|
36
|
+
].join("-");
|
|
38
37
|
}
|
|
39
38
|
function canonicalClassName(reportCtor) {
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
const registered = ObjectRegistry.getClassByConstructor(reportCtor) ?? ObjectRegistry.getClass(reportCtor.name);
|
|
40
|
+
return registered?.qualifiedName ?? registered?.name ?? reportCtor.name;
|
|
42
41
|
}
|
|
43
42
|
function resolveReportClass(name) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
return registered.constructor;
|
|
43
|
+
const registered = ObjectRegistry.getClassByQualifiedName(name) ?? ObjectRegistry.getClass(name);
|
|
44
|
+
if (!registered) throw new Error(`Unknown report class: ${name}`);
|
|
45
|
+
return registered.constructor;
|
|
49
46
|
}
|
|
50
47
|
function reportSourceName(source) {
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
if (typeof source === "string") return source;
|
|
49
|
+
return source.name;
|
|
53
50
|
}
|
|
54
51
|
function sourceMatches(definition, instance, context) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const name = reportSourceName(source);
|
|
68
|
-
const registeredSource = ObjectRegistry.getClassByQualifiedName(name) ?? ObjectRegistry.getClass(name);
|
|
69
|
-
if (eventNames.has(name) || registeredSource?.name && eventNames.has(registeredSource.name) || registeredSource?.qualifiedName && eventNames.has(registeredSource.qualifiedName)) {
|
|
70
|
-
return true;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return false;
|
|
52
|
+
const configured = definition.refresh?.onChange;
|
|
53
|
+
if (!configured || configured.length === 0) return false;
|
|
54
|
+
const eventNames = /* @__PURE__ */ new Set([context.className, instance.constructor.name]);
|
|
55
|
+
const registered = ObjectRegistry.getClassByConstructor(instance.constructor);
|
|
56
|
+
if (registered?.qualifiedName) eventNames.add(registered.qualifiedName);
|
|
57
|
+
if (registered?.name) eventNames.add(registered.name);
|
|
58
|
+
for (const source of configured) {
|
|
59
|
+
const name = reportSourceName(source);
|
|
60
|
+
const registeredSource = ObjectRegistry.getClassByQualifiedName(name) ?? ObjectRegistry.getClass(name);
|
|
61
|
+
if (eventNames.has(name) || registeredSource?.name && eventNames.has(registeredSource.name) || registeredSource?.qualifiedName && eventNames.has(registeredSource.qualifiedName)) return true;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
74
64
|
}
|
|
75
65
|
function tenantIdFromInstance(instance) {
|
|
76
|
-
|
|
77
|
-
|
|
66
|
+
const value = instance.tenantId;
|
|
67
|
+
return typeof value === "string" && value.length > 0 ? value : getTenantId() ?? null;
|
|
78
68
|
}
|
|
79
69
|
function changedRowSnapshot(instance) {
|
|
80
|
-
|
|
81
|
-
|
|
70
|
+
const serializable = instance.toJSON();
|
|
71
|
+
return serializable && typeof serializable === "object" ? serializable : {};
|
|
82
72
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
changedRows: args.changedRows
|
|
104
|
-
});
|
|
105
|
-
}
|
|
73
|
+
var SmrtReportRefreshTask = class extends SmrtObject {
|
|
74
|
+
tenantId = null;
|
|
75
|
+
reportClass = "";
|
|
76
|
+
mode = "incremental";
|
|
77
|
+
trigger = "job";
|
|
78
|
+
args = {};
|
|
79
|
+
async run(args = {}) {
|
|
80
|
+
const reportClass = args.reportClass || this.reportClass;
|
|
81
|
+
if (!reportClass) throw new Error("Report refresh job requires reportClass");
|
|
82
|
+
return refreshReport(resolveReportClass(reportClass), {
|
|
83
|
+
db: this.db,
|
|
84
|
+
mode: args.mode ?? this.mode,
|
|
85
|
+
trigger: args.trigger ?? this.trigger,
|
|
86
|
+
tenantId: args.tenantId,
|
|
87
|
+
tenantIds: args.tenantIds,
|
|
88
|
+
adapterType: args.adapterType,
|
|
89
|
+
scheduleId: args.scheduleId ?? args._scheduleId,
|
|
90
|
+
changedRows: args.changedRows
|
|
91
|
+
});
|
|
92
|
+
}
|
|
106
93
|
};
|
|
107
|
-
__decorateClass([
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
], SmrtReportRefreshTask.prototype, "mode", 2);
|
|
116
|
-
__decorateClass([
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
], SmrtReportRefreshTask.prototype, "
|
|
122
|
-
__decorateClass([
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
SmrtReportRefreshTask
|
|
126
|
-
TenantScoped({ mode: "optional" }),
|
|
127
|
-
smrt({
|
|
128
|
-
tableName: "_smrt_report_refresh_tasks",
|
|
129
|
-
...INTERNAL_SURFACE
|
|
130
|
-
})
|
|
131
|
-
], SmrtReportRefreshTask);
|
|
94
|
+
__decorateClass([tenantId({ nullable: true })], SmrtReportRefreshTask.prototype, "tenantId", 2);
|
|
95
|
+
__decorateClass([field({
|
|
96
|
+
type: "text",
|
|
97
|
+
required: true
|
|
98
|
+
})], SmrtReportRefreshTask.prototype, "reportClass", 2);
|
|
99
|
+
__decorateClass([field({
|
|
100
|
+
type: "text",
|
|
101
|
+
required: true
|
|
102
|
+
})], SmrtReportRefreshTask.prototype, "mode", 2);
|
|
103
|
+
__decorateClass([field({
|
|
104
|
+
type: "text",
|
|
105
|
+
required: true
|
|
106
|
+
})], SmrtReportRefreshTask.prototype, "trigger", 2);
|
|
107
|
+
__decorateClass([field({ type: "json" })], SmrtReportRefreshTask.prototype, "args", 2);
|
|
108
|
+
__decorateClass([backgroundEligible()], SmrtReportRefreshTask.prototype, "run", 1);
|
|
109
|
+
SmrtReportRefreshTask = __decorateClass([TenantScoped({ mode: "optional" }), smrt({
|
|
110
|
+
tableName: "_smrt_report_refresh_tasks",
|
|
111
|
+
...INTERNAL_SURFACE
|
|
112
|
+
})], SmrtReportRefreshTask);
|
|
132
113
|
async function enqueueReportRefresh(options) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
},
|
|
159
|
-
{ tenantJobCap: options.tenantJobCap }
|
|
160
|
-
);
|
|
114
|
+
await ObjectRegistry.ensureManifestLoaded("SmrtJob");
|
|
115
|
+
const collection = await SmrtJobCollection.create({ db: options.db });
|
|
116
|
+
const taskType = canonicalClassName(SmrtReportRefreshTask);
|
|
117
|
+
const scheduleId = options.scheduleId ?? options._scheduleId;
|
|
118
|
+
return collection.enqueueJob({
|
|
119
|
+
tenantId: options.tenantId ?? null,
|
|
120
|
+
queue: options.queue ?? "reports",
|
|
121
|
+
objectType: taskType,
|
|
122
|
+
objectId: null,
|
|
123
|
+
method: "run",
|
|
124
|
+
args: {
|
|
125
|
+
reportClass: options.reportClass,
|
|
126
|
+
mode: options.mode,
|
|
127
|
+
trigger: options.trigger ?? "job",
|
|
128
|
+
tenantId: options.tenantId,
|
|
129
|
+
tenantIds: options.tenantIds,
|
|
130
|
+
adapterType: options.adapterType,
|
|
131
|
+
changedRows: options.changedRows,
|
|
132
|
+
scheduleId,
|
|
133
|
+
_scheduleId: scheduleId
|
|
134
|
+
},
|
|
135
|
+
priority: options.priority ?? 70,
|
|
136
|
+
timeout: options.timeout ?? 36e5,
|
|
137
|
+
maxAttempts: options.maxAttempts ?? 3
|
|
138
|
+
}, { tenantJobCap: options.tenantJobCap });
|
|
161
139
|
}
|
|
162
140
|
async function ensureReportRefreshSchedules(options) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
updated_at: now
|
|
228
|
-
}
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
141
|
+
await assertReportTablesReady(options.db, REPORT_SCHEDULER_TABLES);
|
|
142
|
+
for (const reportCtor of options.reports) {
|
|
143
|
+
const definition = await buildReportDefinition(reportCtor);
|
|
144
|
+
const refresh = definition.refresh;
|
|
145
|
+
if (!refresh || refresh.manual) continue;
|
|
146
|
+
const reportClass = canonicalClassName(reportCtor);
|
|
147
|
+
const targetTenants = refresh.tenantFanout ? options.tenantIds : [null];
|
|
148
|
+
if (refresh.tenantFanout && (!targetTenants || targetTenants.length === 0)) throw new Error(`${definition.reportClassName} refresh.tenantFanout requires tenantIds when creating schedules.`);
|
|
149
|
+
const schedules = [refresh.schedule ? {
|
|
150
|
+
cron: refresh.schedule,
|
|
151
|
+
mode: refresh.mode ?? "incremental",
|
|
152
|
+
trigger: "schedule"
|
|
153
|
+
} : null, refresh.fullRebuildSchedule ? {
|
|
154
|
+
cron: refresh.fullRebuildSchedule,
|
|
155
|
+
mode: "rebuild",
|
|
156
|
+
trigger: "schedule"
|
|
157
|
+
} : null].filter(Boolean);
|
|
158
|
+
for (const schedule of schedules) {
|
|
159
|
+
validateCronExpression(schedule.cron);
|
|
160
|
+
for (const tenantId2 of targetTenants ?? []) {
|
|
161
|
+
const scopeKey = scopeKeyForTenant(tenantId2);
|
|
162
|
+
const id = stableUuid([
|
|
163
|
+
"schedule",
|
|
164
|
+
reportClass,
|
|
165
|
+
scopeKey,
|
|
166
|
+
schedule.cron,
|
|
167
|
+
schedule.mode
|
|
168
|
+
]);
|
|
169
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
170
|
+
await options.db.upsert(REPORT_SCHEDULES_TABLE, [
|
|
171
|
+
"report_class",
|
|
172
|
+
"scope_key",
|
|
173
|
+
"cron",
|
|
174
|
+
"mode"
|
|
175
|
+
], {
|
|
176
|
+
id,
|
|
177
|
+
slug: id,
|
|
178
|
+
context: scopeKey,
|
|
179
|
+
tenant_id: tenantId2,
|
|
180
|
+
scope_key: scopeKey,
|
|
181
|
+
report_class: reportClass,
|
|
182
|
+
cron: schedule.cron,
|
|
183
|
+
trigger: schedule.trigger,
|
|
184
|
+
mode: schedule.mode,
|
|
185
|
+
enabled: true,
|
|
186
|
+
status: "active",
|
|
187
|
+
next_run: getNextCronDate(schedule.cron).toISOString(),
|
|
188
|
+
last_run: null,
|
|
189
|
+
last_status: null,
|
|
190
|
+
last_error: null,
|
|
191
|
+
run_count: 0,
|
|
192
|
+
success_count: 0,
|
|
193
|
+
failure_count: 0,
|
|
194
|
+
running_count: 0,
|
|
195
|
+
max_concurrent: 1,
|
|
196
|
+
queue: options.queue ?? "reports",
|
|
197
|
+
priority: options.priority ?? 70,
|
|
198
|
+
timeout: options.timeout ?? 36e5,
|
|
199
|
+
created_at: now,
|
|
200
|
+
updated_at: now
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
233
205
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if (!this.db) return;
|
|
278
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
279
|
-
if (success) {
|
|
280
|
-
await this.db.query(
|
|
281
|
-
`UPDATE ${REPORT_SCHEDULES_TABLE}
|
|
206
|
+
var ReportScheduleRunner = class extends EventEmitter {
|
|
207
|
+
id;
|
|
208
|
+
config;
|
|
209
|
+
db = null;
|
|
210
|
+
running = false;
|
|
211
|
+
pollTimer = null;
|
|
212
|
+
constructor(config = {}) {
|
|
213
|
+
super();
|
|
214
|
+
this.config = {
|
|
215
|
+
id: config.id || `reports_${stableUuid([Date.now()]).slice(0, 8)}`,
|
|
216
|
+
pollInterval: config.pollInterval ?? 6e4,
|
|
217
|
+
batchSize: config.batchSize ?? 50
|
|
218
|
+
};
|
|
219
|
+
this.id = this.config.id;
|
|
220
|
+
}
|
|
221
|
+
async initialize(db) {
|
|
222
|
+
this.db = db;
|
|
223
|
+
await assertReportTablesReady(db, REPORT_SCHEDULER_TABLES);
|
|
224
|
+
}
|
|
225
|
+
async start() {
|
|
226
|
+
if (this.running) return;
|
|
227
|
+
if (!this.db) throw new Error("ReportScheduleRunner not initialized. Call initialize() first.");
|
|
228
|
+
this.running = true;
|
|
229
|
+
this.startPolling();
|
|
230
|
+
this.emit("runner:started");
|
|
231
|
+
}
|
|
232
|
+
async stop() {
|
|
233
|
+
if (!this.running) return;
|
|
234
|
+
this.running = false;
|
|
235
|
+
if (this.pollTimer) {
|
|
236
|
+
clearTimeout(this.pollTimer);
|
|
237
|
+
this.pollTimer = null;
|
|
238
|
+
}
|
|
239
|
+
this.emit("runner:stopped");
|
|
240
|
+
}
|
|
241
|
+
isRunning() {
|
|
242
|
+
return this.running;
|
|
243
|
+
}
|
|
244
|
+
async handleJobCompletion(scheduleId, success, errorMessage) {
|
|
245
|
+
if (!this.db) return;
|
|
246
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
247
|
+
if (success) {
|
|
248
|
+
await this.db.query(`UPDATE ${REPORT_SCHEDULES_TABLE}
|
|
282
249
|
SET running_count = CASE WHEN COALESCE(running_count, 0) > 0 THEN running_count - 1 ELSE 0 END,
|
|
283
250
|
last_run = ?,
|
|
284
251
|
last_status = 'success',
|
|
@@ -286,17 +253,12 @@ class ReportScheduleRunner extends EventEmitter {
|
|
|
286
253
|
run_count = COALESCE(run_count, 0) + 1,
|
|
287
254
|
success_count = COALESCE(success_count, 0) + 1,
|
|
288
255
|
updated_at = ?
|
|
289
|
-
WHERE id = ?`,
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
const safeError = errorMessage ?? "Unknown error";
|
|
298
|
-
await this.db.query(
|
|
299
|
-
`UPDATE ${REPORT_SCHEDULES_TABLE}
|
|
256
|
+
WHERE id = ?`, now, now, scheduleId);
|
|
257
|
+
this.emit("schedule:completed", scheduleId);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const safeError = errorMessage ?? "Unknown error";
|
|
261
|
+
await this.db.query(`UPDATE ${REPORT_SCHEDULES_TABLE}
|
|
300
262
|
SET running_count = CASE WHEN COALESCE(running_count, 0) > 0 THEN running_count - 1 ELSE 0 END,
|
|
301
263
|
last_run = ?,
|
|
302
264
|
last_status = 'failed',
|
|
@@ -304,146 +266,119 @@ class ReportScheduleRunner extends EventEmitter {
|
|
|
304
266
|
run_count = COALESCE(run_count, 0) + 1,
|
|
305
267
|
failure_count = COALESCE(failure_count, 0) + 1,
|
|
306
268
|
updated_at = ?
|
|
307
|
-
WHERE id = ?`,
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
};
|
|
330
|
-
poll();
|
|
331
|
-
}
|
|
332
|
-
async poll() {
|
|
333
|
-
if (!this.db) return;
|
|
334
|
-
const result = await this.db.query(
|
|
335
|
-
`SELECT * FROM ${REPORT_SCHEDULES_TABLE}
|
|
269
|
+
WHERE id = ?`, now, safeError, now, scheduleId);
|
|
270
|
+
this.emit("schedule:failed", scheduleId, safeError);
|
|
271
|
+
}
|
|
272
|
+
startPolling() {
|
|
273
|
+
const poll = async () => {
|
|
274
|
+
if (!this.running) return;
|
|
275
|
+
try {
|
|
276
|
+
await this.poll();
|
|
277
|
+
} catch (error) {
|
|
278
|
+
this.emit("runner:error", error);
|
|
279
|
+
}
|
|
280
|
+
if (this.running) {
|
|
281
|
+
this.pollTimer = setTimeout(poll, this.config.pollInterval);
|
|
282
|
+
if (typeof this.pollTimer.unref === "function") this.pollTimer.unref();
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
poll();
|
|
286
|
+
}
|
|
287
|
+
async poll() {
|
|
288
|
+
if (!this.db) return;
|
|
289
|
+
const result = await this.db.query(`SELECT * FROM ${REPORT_SCHEDULES_TABLE}
|
|
336
290
|
WHERE enabled = true
|
|
337
291
|
AND status = 'active'
|
|
338
292
|
AND next_run <= ?
|
|
339
293
|
AND COALESCE(running_count, 0) < COALESCE(max_concurrent, 1)
|
|
340
294
|
ORDER BY next_run ASC
|
|
341
|
-
LIMIT ?`,
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
queue: String(row.queue || "reports"),
|
|
368
|
-
priority: Number(row.priority ?? 70),
|
|
369
|
-
timeout: Number(row.timeout ?? 36e5)
|
|
370
|
-
});
|
|
371
|
-
await this.db.query(
|
|
372
|
-
`UPDATE ${REPORT_SCHEDULES_TABLE}
|
|
295
|
+
LIMIT ?`, (/* @__PURE__ */ new Date()).toISOString(), this.config.batchSize);
|
|
296
|
+
for (const row of result.rows) await this.triggerSchedule(row);
|
|
297
|
+
}
|
|
298
|
+
async triggerSchedule(row) {
|
|
299
|
+
if (!this.db) return;
|
|
300
|
+
const schedule = {
|
|
301
|
+
id: String(row.id),
|
|
302
|
+
reportClass: String(row.report_class),
|
|
303
|
+
tenantId: typeof row.tenant_id === "string" && row.tenant_id.length > 0 ? row.tenant_id : null,
|
|
304
|
+
cron: String(row.cron),
|
|
305
|
+
mode: row.mode || "incremental"
|
|
306
|
+
};
|
|
307
|
+
try {
|
|
308
|
+
const nextRun = getNextCronDate(schedule.cron);
|
|
309
|
+
await enqueueReportRefresh({
|
|
310
|
+
db: this.db,
|
|
311
|
+
reportClass: schedule.reportClass,
|
|
312
|
+
mode: schedule.mode,
|
|
313
|
+
trigger: row.trigger || "schedule",
|
|
314
|
+
tenantId: schedule.tenantId,
|
|
315
|
+
scheduleId: schedule.id,
|
|
316
|
+
queue: String(row.queue || "reports"),
|
|
317
|
+
priority: Number(row.priority ?? 70),
|
|
318
|
+
timeout: Number(row.timeout ?? 36e5)
|
|
319
|
+
});
|
|
320
|
+
await this.db.query(`UPDATE ${REPORT_SCHEDULES_TABLE}
|
|
373
321
|
SET running_count = COALESCE(running_count, 0) + 1,
|
|
374
322
|
next_run = ?,
|
|
375
323
|
updated_at = ?
|
|
376
|
-
WHERE id = ?`,
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
);
|
|
381
|
-
this.emit("schedule:triggered", schedule);
|
|
382
|
-
} catch (error) {
|
|
383
|
-
await this.db.query(
|
|
384
|
-
`UPDATE ${REPORT_SCHEDULES_TABLE}
|
|
324
|
+
WHERE id = ?`, nextRun.toISOString(), (/* @__PURE__ */ new Date()).toISOString(), schedule.id);
|
|
325
|
+
this.emit("schedule:triggered", schedule);
|
|
326
|
+
} catch (error) {
|
|
327
|
+
await this.db.query(`UPDATE ${REPORT_SCHEDULES_TABLE}
|
|
385
328
|
SET last_error = ?,
|
|
386
329
|
updated_at = ?
|
|
387
|
-
WHERE id = ?`,
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
this.emit("schedule:error", schedule, error);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
}
|
|
330
|
+
WHERE id = ?`, error instanceof Error ? error.message : String(error), (/* @__PURE__ */ new Date()).toISOString(), schedule.id);
|
|
331
|
+
this.emit("schedule:error", schedule, error);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
};
|
|
396
335
|
function registerReportRefreshInterceptor(options) {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
336
|
+
const name = options.name ?? "smrt-reports-refresh";
|
|
337
|
+
GlobalInterceptors.register({
|
|
338
|
+
name,
|
|
339
|
+
priority: -10,
|
|
340
|
+
async afterSave(instance, context) {
|
|
341
|
+
await triggerReportsForInstance(options, instance, context);
|
|
342
|
+
},
|
|
343
|
+
async afterDelete(instance, context) {
|
|
344
|
+
await triggerReportsForInstance(options, instance, context);
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
return () => GlobalInterceptors.unregister(name);
|
|
409
348
|
}
|
|
410
349
|
async function triggerReportsForInstance(options, instance, context) {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
350
|
+
for (const reportCtor of options.reports) {
|
|
351
|
+
const definition = await buildReportDefinition(reportCtor);
|
|
352
|
+
if (definition.refresh?.manual) continue;
|
|
353
|
+
if (!sourceMatches(definition, instance, context)) continue;
|
|
354
|
+
const mode = definition.refresh?.mode ?? "incremental";
|
|
355
|
+
const tenantId2 = tenantIdFromInstance(instance);
|
|
356
|
+
const changedRows = [changedRowSnapshot(instance)];
|
|
357
|
+
if (options.enqueue === false) {
|
|
358
|
+
await refreshReport(reportCtor, {
|
|
359
|
+
db: options.db,
|
|
360
|
+
mode,
|
|
361
|
+
trigger: "change",
|
|
362
|
+
tenantId: tenantId2,
|
|
363
|
+
changedRows
|
|
364
|
+
});
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
await enqueueReportRefresh({
|
|
368
|
+
db: options.db,
|
|
369
|
+
reportClass: canonicalClassName(reportCtor),
|
|
370
|
+
mode,
|
|
371
|
+
trigger: "change",
|
|
372
|
+
tenantId: tenantId2,
|
|
373
|
+
queue: options.queue,
|
|
374
|
+
priority: options.priority,
|
|
375
|
+
timeout: options.timeout,
|
|
376
|
+
tenantJobCap: options.tenantJobCap,
|
|
377
|
+
changedRows
|
|
378
|
+
});
|
|
379
|
+
}
|
|
441
380
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
ensureReportRefreshSchedules,
|
|
447
|
-
registerReportRefreshInterceptor
|
|
448
|
-
};
|
|
449
|
-
//# sourceMappingURL=scheduler.js.map
|
|
381
|
+
//#endregion
|
|
382
|
+
export { ReportScheduleRunner, SmrtReportRefreshTask, enqueueReportRefresh, ensureReportRefreshSchedules, registerReportRefreshInterceptor };
|
|
383
|
+
|
|
384
|
+
//# sourceMappingURL=scheduler.js.map
|