@objectstack/plugin-reports 17.0.0-rc.0 → 17.0.0-rc.1
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/CHANGELOG.md +256 -0
- package/dist/index.js +6 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -5
- package/.turbo/turbo-build.log +0 -22
- package/src/index.ts +0 -34
- package/src/report-export-axis.test.ts +0 -220
- package/src/report-service.test.ts +0 -475
- package/src/report-service.ts +0 -707
- package/src/reports-plugin.ts +0 -161
- package/tsconfig.json +0 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,261 @@
|
|
|
1
1
|
# @objectstack/plugin-reports
|
|
2
2
|
|
|
3
|
+
## 17.0.0-rc.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 2e836de: chore(packaging): CHANGELOG.md ships in every npm tarball (#4261)
|
|
8
|
+
|
|
9
|
+
The AGENTS.md post-task checklist requires breaking changesets to carry their
|
|
10
|
+
FROM → TO migration because "this text ships to consumers as `CHANGELOG.md`
|
|
11
|
+
inside the npm package and is what an upgrading agent greps after the tombstone
|
|
12
|
+
error." That delivery path was severed for 68 of the 69 publishable packages:
|
|
13
|
+
npm packs `package.json` / `README*` / `LICENSE*` unconditionally but — unlike
|
|
14
|
+
older npm versions — not `CHANGELOG.md`, and the canonical
|
|
15
|
+
`"files": ["dist", "README.md"]` whitelist never named it. Measured on npm
|
|
16
|
+
10.9.7: `npm pack --dry-run` on `@objectstack/types` shipped 3 files while its
|
|
17
|
+
70KB `CHANGELOG.md` stayed behind. Only `@objectstack/spec` listed it
|
|
18
|
+
explicitly.
|
|
19
|
+
|
|
20
|
+
The tombstone-error scenario is precisely the one where the repo is out of
|
|
21
|
+
reach — the upgrading agent has `node_modules` and nothing else — so the
|
|
22
|
+
migration text has to ride in the tarball. Every publishable package now
|
|
23
|
+
declares `CHANGELOG.md` in `files`, and the canonical whitelist is
|
|
24
|
+
`["dist", "README.md", "CHANGELOG.md"]`.
|
|
25
|
+
|
|
26
|
+
The other half is the gate: `check:published-files` gains a fifth invariant,
|
|
27
|
+
COMPLETE — a whitelist that fails to cover `CHANGELOG.md` fails the
|
|
28
|
+
always-required lint job, so the next package cannot silently sever the path
|
|
29
|
+
again. `@objectstack/spec`'s per-package EXTRA_ENTRIES exemption dissolves
|
|
30
|
+
into the canonical set.
|
|
31
|
+
|
|
32
|
+
Consumer-visible change: one more file per install (the package's changelog,
|
|
33
|
+
e.g. 70.8KB for `@objectstack/types`), and `grep -r "removed key"
|
|
34
|
+
node_modules/@objectstack/*/CHANGELOG.md` now finds the migration it was
|
|
35
|
+
promised.
|
|
36
|
+
|
|
37
|
+
- b5f9397: fix(sharing,runtime): a `sort` passed straight to the engine never ordered anything; migrate every in-repo engine call to canonical QueryAST keys (#4346)
|
|
38
|
+
|
|
39
|
+
Two changes with different weights, from one sweep of every in-repo engine
|
|
40
|
+
call site that still speaks a deprecated alias.
|
|
41
|
+
|
|
42
|
+
**The bug — three dropped sorts.** #4346 made the engine fold `filter`→`where`
|
|
43
|
+
and `top`→`limit` on all six methods. The other four pairs in
|
|
44
|
+
`RPC_QUERY_ALIAS_SLOTS` (`select`, `sort`, `skip`, `populate`) are folded at
|
|
45
|
+
the RPC/wire layer only — their values need shape lowering that belongs to
|
|
46
|
+
those layers — and a **direct `engine.find()` never crosses that layer**. Three
|
|
47
|
+
call sites passed `sort` there, so it rode onto the AST untouched, every
|
|
48
|
+
driver's `Array.isArray(query.orderBy)` guard declined to emit an ORDER BY, and
|
|
49
|
+
the query returned an ordinary-looking, arbitrarily-ordered result:
|
|
50
|
+
|
|
51
|
+
| call site | asked for | actually got |
|
|
52
|
+
| ----------------------------------- | ------------------------------------------------- | --------------------------- |
|
|
53
|
+
| `share-link-routes.ts` | shared AI conversation messages, `created_at asc` | messages in arbitrary order |
|
|
54
|
+
| `runtime/domains/share-links.ts` | same route, runtime-domain copy | same |
|
|
55
|
+
| `share-link-service.ts` `listLinks` | the 200 most recent share links | an arbitrary 200 |
|
|
56
|
+
|
|
57
|
+
All three combine the dropped sort with a `limit` — the "latest N" shape whose
|
|
58
|
+
failure #4226 spelled out: an unapplied sort returns rows in arbitrary order,
|
|
59
|
+
which `limit` then slices into an arbitrary page. #4226 fixed that in the wire
|
|
60
|
+
normalizer; these calls sit one layer below it. `listLinks` had no test at all,
|
|
61
|
+
which is why it went unnoticed. Now pinned — on the option bag the engine
|
|
62
|
+
receives, not on row order, because the failure is that the key never becomes
|
|
63
|
+
`orderBy` and a fake engine honouring either spelling would pass either way.
|
|
64
|
+
|
|
65
|
+
**The cleanup — 27 no-op renames.** Every remaining in-repo engine call passing
|
|
66
|
+
`filter` now passes `where` (approvals 5, auth 2, reports 6, sharing 11,
|
|
67
|
+
webhooks 2, plus the one `filters` in a spec doc example). These are strict
|
|
68
|
+
no-ops since #4346 folds the alias — the point is that the framework stops
|
|
69
|
+
depending on a spelling it asks users to migrate off, which is a prerequisite
|
|
70
|
+
for ever retiring the aliases. Service-level `filter` PARAMETERS (each
|
|
71
|
+
service's own public API, e.g. `listRequests(filter)`) are deliberately
|
|
72
|
+
untouched — those are not engine option bags.
|
|
73
|
+
|
|
74
|
+
Two of the renamed calls were live victims of the #4346 bug rather than
|
|
75
|
+
cosmetic: `auth-manager`'s `stampIdentitySource` read the table's first row via
|
|
76
|
+
`findOne({filter})` and counted the whole table via `count({filter})`, so a
|
|
77
|
+
federated sign-in never stamped `source: 'idp_provisioned'`. #4346 already
|
|
78
|
+
corrected the behaviour; this makes the call say what it means.
|
|
79
|
+
|
|
80
|
+
- cc2de0e: chore(packaging): 20 packages stop publishing their sources, tests and build tooling (#4248)
|
|
81
|
+
|
|
82
|
+
These 20 packages declared no `files` field, so npm fell back to packing the
|
|
83
|
+
whole package directory. `npm pack --dry-run` on `@objectstack/plugin-webhooks`
|
|
84
|
+
listed **21 files** — 15 under `src/`, three of them unit tests
|
|
85
|
+
(`auto-enqueuer.test.ts`, `bootstrap-declared-webhooks.test.ts`, …), plus the
|
|
86
|
+
build-time `scripts/i18n-extract.config.ts`. `dist/` lands on top of that at
|
|
87
|
+
publish time rather than instead of it, so consumers were installing the
|
|
88
|
+
TypeScript sources and the test suite alongside the artifact they asked for.
|
|
89
|
+
|
|
90
|
+
Each now declares `"files": ["dist", "README.md"]`, matching the 29 packages
|
|
91
|
+
that already did. Nothing a consumer imports moves: every `main` / `types` /
|
|
92
|
+
`exports` target in all 20 already resolved inside `dist/`, which the new
|
|
93
|
+
`check:published-files` guard verifies rather than assumes. The visible change
|
|
94
|
+
is a smaller install and a smaller dependency-scanning surface — `npm pack` on
|
|
95
|
+
`@objectstack/plugin-webhooks` now yields 2 files plus `dist/`.
|
|
96
|
+
|
|
97
|
+
The other half of the fix is the gate. Half the packages declaring `files` and
|
|
98
|
+
half not was the #3786 shape — a hand-copied convention with nothing enforcing
|
|
99
|
+
it, where whoever forgets the line gets no signal at all. `check:published-files`
|
|
100
|
+
(new, wired into the always-required `lint` job) holds every non-private
|
|
101
|
+
workspace package to four invariants: `files` is **declared**; it is
|
|
102
|
+
**sufficient** (covers every entry point, so tightening a whitelist cannot ship
|
|
103
|
+
a package that fails to resolve); it is **minimal** (admits no test, test-harness
|
|
104
|
+
config or build script); and anything beyond `dist` + `README.md` is
|
|
105
|
+
**registered** with a reason, reconciled in both directions so a stale exemption
|
|
106
|
+
is an error rather than dead text. `@objectstack/spec` is the one package with
|
|
107
|
+
registered extras — its `.zod.ts` sources, JSON Schemas, liveness ledgers and
|
|
108
|
+
`CHANGELOG.md` are product, not build input.
|
|
109
|
+
|
|
110
|
+
This also closes an assumption #4206 was resting on. Excluding `<pkg>/scripts/**`
|
|
111
|
+
from the docs-drift implementation test is sound only while no package publishes
|
|
112
|
+
`scripts/` as runtime code; that held, but it held because someone read all three
|
|
113
|
+
offenders by hand. It is now checked on every PR.
|
|
114
|
+
|
|
115
|
+
- Updated dependencies [6a67d7a]
|
|
116
|
+
- Updated dependencies [0ecc656]
|
|
117
|
+
- Updated dependencies [06772eb]
|
|
118
|
+
- Updated dependencies [270650f]
|
|
119
|
+
- Updated dependencies [3aef718]
|
|
120
|
+
- Updated dependencies [1ea6bce]
|
|
121
|
+
- Updated dependencies [c1dcacd]
|
|
122
|
+
- Updated dependencies [ad303ed]
|
|
123
|
+
- Updated dependencies [32ccb23]
|
|
124
|
+
- Updated dependencies [f5a4ef0]
|
|
125
|
+
- Updated dependencies [2d3e255]
|
|
126
|
+
- Updated dependencies [7d7521f]
|
|
127
|
+
- Updated dependencies [5dc4d02]
|
|
128
|
+
- Updated dependencies [05154a1]
|
|
129
|
+
- Updated dependencies [9b6fe7c]
|
|
130
|
+
- Updated dependencies [8c711fb]
|
|
131
|
+
- Updated dependencies [09e4547]
|
|
132
|
+
- Updated dependencies [91f4c78]
|
|
133
|
+
- Updated dependencies [820eff9]
|
|
134
|
+
- Updated dependencies [8d895ff]
|
|
135
|
+
- Updated dependencies [f6472d7]
|
|
136
|
+
- Updated dependencies [78caf51]
|
|
137
|
+
- Updated dependencies [62a789b]
|
|
138
|
+
- Updated dependencies [789ad63]
|
|
139
|
+
- Updated dependencies [2af1988]
|
|
140
|
+
- Updated dependencies [0af50a3]
|
|
141
|
+
- Updated dependencies [2e836de]
|
|
142
|
+
- Updated dependencies [12a19a8]
|
|
143
|
+
- Updated dependencies [41dcda3]
|
|
144
|
+
- Updated dependencies [c8124e5]
|
|
145
|
+
- Updated dependencies [a1a4140]
|
|
146
|
+
- Updated dependencies [217e2e6]
|
|
147
|
+
- Updated dependencies [86a71d1]
|
|
148
|
+
- Updated dependencies [d5c75e2]
|
|
149
|
+
- Updated dependencies [03d26f7]
|
|
150
|
+
- Updated dependencies [4384921]
|
|
151
|
+
- Updated dependencies [3c628ce]
|
|
152
|
+
- Updated dependencies [7cb922e]
|
|
153
|
+
- Updated dependencies [1d22114]
|
|
154
|
+
- Updated dependencies [b5f9397]
|
|
155
|
+
- Updated dependencies [ed77493]
|
|
156
|
+
- Updated dependencies [58a03d2]
|
|
157
|
+
- Updated dependencies [dc530b4]
|
|
158
|
+
- Updated dependencies [e59786e]
|
|
159
|
+
- Updated dependencies [bcf1112]
|
|
160
|
+
- Updated dependencies [9774b78]
|
|
161
|
+
- Updated dependencies [b07d829]
|
|
162
|
+
- Updated dependencies [a648e96]
|
|
163
|
+
- Updated dependencies [a47ac06]
|
|
164
|
+
- Updated dependencies [e4c61a7]
|
|
165
|
+
- Updated dependencies [cc60165]
|
|
166
|
+
- Updated dependencies [081aa6f]
|
|
167
|
+
- Updated dependencies [91f4c78]
|
|
168
|
+
- Updated dependencies [e8d0c21]
|
|
169
|
+
- Updated dependencies [45dc446]
|
|
170
|
+
- Updated dependencies [c1d44f7]
|
|
171
|
+
- Updated dependencies [ab9fb5c]
|
|
172
|
+
- Updated dependencies [f985b3f]
|
|
173
|
+
- Updated dependencies [9a4932a]
|
|
174
|
+
- Updated dependencies [f9fc874]
|
|
175
|
+
- Updated dependencies [011b386]
|
|
176
|
+
- Updated dependencies [7777e8f]
|
|
177
|
+
- Updated dependencies [507b92a]
|
|
178
|
+
- Updated dependencies [7309c81]
|
|
179
|
+
- Updated dependencies [20bc1ec]
|
|
180
|
+
- Updated dependencies [90c2b15]
|
|
181
|
+
- Updated dependencies [42eeb7d]
|
|
182
|
+
- Updated dependencies [01e124d]
|
|
183
|
+
- Updated dependencies [7ce02eb]
|
|
184
|
+
- Updated dependencies [a13827e]
|
|
185
|
+
- Updated dependencies [7733604]
|
|
186
|
+
- Updated dependencies [40e420f]
|
|
187
|
+
- Updated dependencies [d13004a]
|
|
188
|
+
- Updated dependencies [be7360c]
|
|
189
|
+
- Updated dependencies [5b47ab5]
|
|
190
|
+
- Updated dependencies [b09d8d9]
|
|
191
|
+
- Updated dependencies [b09d8d9]
|
|
192
|
+
- Updated dependencies [8675db6]
|
|
193
|
+
- Updated dependencies [b09d8d9]
|
|
194
|
+
- Updated dependencies [3eb1b2b]
|
|
195
|
+
- Updated dependencies [59b85c0]
|
|
196
|
+
- Updated dependencies [6e357ed]
|
|
197
|
+
- Updated dependencies [d6938bf]
|
|
198
|
+
- Updated dependencies [31e0be9]
|
|
199
|
+
- Updated dependencies [4bfd455]
|
|
200
|
+
- Updated dependencies [ffd2ce2]
|
|
201
|
+
- Updated dependencies [62f8017]
|
|
202
|
+
- Updated dependencies [a831df1]
|
|
203
|
+
- Updated dependencies [f752ee3]
|
|
204
|
+
- Updated dependencies [a1b61e0]
|
|
205
|
+
- Updated dependencies [cd6b9f2]
|
|
206
|
+
- Updated dependencies [2cb6d3c]
|
|
207
|
+
- Updated dependencies [af2a095]
|
|
208
|
+
- Updated dependencies [ec796d5]
|
|
209
|
+
- Updated dependencies [e87fea1]
|
|
210
|
+
- Updated dependencies [c65e529]
|
|
211
|
+
- Updated dependencies [3ca34c1]
|
|
212
|
+
- Updated dependencies [239c3a3]
|
|
213
|
+
- Updated dependencies [94a0bbc]
|
|
214
|
+
- Updated dependencies [d6bfb3d]
|
|
215
|
+
- Updated dependencies [a2266a6]
|
|
216
|
+
- Updated dependencies [d25a0ec]
|
|
217
|
+
- Updated dependencies [667b83e]
|
|
218
|
+
- Updated dependencies [627b188]
|
|
219
|
+
- Updated dependencies [8d4eae7]
|
|
220
|
+
- Updated dependencies [857a6cf]
|
|
221
|
+
- Updated dependencies [65a3a84]
|
|
222
|
+
- Updated dependencies [ccd9397]
|
|
223
|
+
- Updated dependencies [bca935b]
|
|
224
|
+
- Updated dependencies [d92c72d]
|
|
225
|
+
- Updated dependencies [c54c822]
|
|
226
|
+
- Updated dependencies [8dcc0f5]
|
|
227
|
+
- Updated dependencies [75b9e51]
|
|
228
|
+
- Updated dependencies [0a2f233]
|
|
229
|
+
- Updated dependencies [8621cdd]
|
|
230
|
+
- Updated dependencies [6f23667]
|
|
231
|
+
- Updated dependencies [5d21a48]
|
|
232
|
+
- Updated dependencies [19365b7]
|
|
233
|
+
- Updated dependencies [b7ed26d]
|
|
234
|
+
- Updated dependencies [68dea0b]
|
|
235
|
+
- Updated dependencies [64f8cbe]
|
|
236
|
+
- Updated dependencies [b3a3d83]
|
|
237
|
+
- Updated dependencies [7a55913]
|
|
238
|
+
- Updated dependencies [35accbf]
|
|
239
|
+
- Updated dependencies [6038de7]
|
|
240
|
+
- Updated dependencies [eb95d97]
|
|
241
|
+
- Updated dependencies [e4c2dc8]
|
|
242
|
+
- Updated dependencies [1bd2795]
|
|
243
|
+
- Updated dependencies [8186a70]
|
|
244
|
+
- Updated dependencies [a329cca]
|
|
245
|
+
- Updated dependencies [6eec18c]
|
|
246
|
+
- Updated dependencies [4d7bebf]
|
|
247
|
+
- Updated dependencies [821ac7a]
|
|
248
|
+
- Updated dependencies [8f81731]
|
|
249
|
+
- Updated dependencies [8b50cb3]
|
|
250
|
+
- Updated dependencies [8c2db68]
|
|
251
|
+
- Updated dependencies [22b5e54]
|
|
252
|
+
- Updated dependencies [0166bd5]
|
|
253
|
+
- Updated dependencies [9b702dc]
|
|
254
|
+
- Updated dependencies [ab16331]
|
|
255
|
+
- @objectstack/spec@17.0.0-rc.1
|
|
256
|
+
- @objectstack/platform-objects@17.0.0-rc.1
|
|
257
|
+
- @objectstack/core@17.0.0-rc.1
|
|
258
|
+
|
|
3
259
|
## 17.0.0-rc.0
|
|
4
260
|
|
|
5
261
|
### Major Changes
|
package/dist/index.js
CHANGED
|
@@ -213,7 +213,7 @@ var ReportService = class {
|
|
|
213
213
|
/** Raw metadata read of a saved report by id (no authz — callers gate). */
|
|
214
214
|
async loadReportRow(reportId) {
|
|
215
215
|
const rows = await this.engine.find("sys_saved_report", {
|
|
216
|
-
|
|
216
|
+
where: { id: reportId },
|
|
217
217
|
limit: 1,
|
|
218
218
|
context: SYSTEM_CTX
|
|
219
219
|
});
|
|
@@ -262,7 +262,7 @@ var ReportService = class {
|
|
|
262
262
|
f.owner_id = context.userId;
|
|
263
263
|
}
|
|
264
264
|
const rows = await this.engine.find("sys_saved_report", {
|
|
265
|
-
|
|
265
|
+
where: f,
|
|
266
266
|
limit: 500,
|
|
267
267
|
orderBy: [{ field: "updated_at", order: "desc" }],
|
|
268
268
|
context: SYSTEM_CTX
|
|
@@ -282,7 +282,7 @@ var ReportService = class {
|
|
|
282
282
|
throw new Error(`REPORT_NOT_FOUND: ${reportId}`);
|
|
283
283
|
}
|
|
284
284
|
const schedules = await this.engine.find("sys_report_schedule", {
|
|
285
|
-
|
|
285
|
+
where: { report_id: reportId },
|
|
286
286
|
limit: 500,
|
|
287
287
|
context: SYSTEM_CTX
|
|
288
288
|
});
|
|
@@ -319,7 +319,7 @@ var ReportService = class {
|
|
|
319
319
|
const q = report.query ?? {};
|
|
320
320
|
const limit = Math.min(q.limit ?? DEFAULT_LIMIT, this.maxRows);
|
|
321
321
|
const rows = await this.engine.find(report.object_name, {
|
|
322
|
-
|
|
322
|
+
where: q.filter,
|
|
323
323
|
fields: q.fields,
|
|
324
324
|
orderBy: q.orderBy,
|
|
325
325
|
limit,
|
|
@@ -409,7 +409,7 @@ var ReportService = class {
|
|
|
409
409
|
const f = {};
|
|
410
410
|
if (filter?.reportId) f.report_id = filter.reportId;
|
|
411
411
|
const rows = await this.engine.find("sys_report_schedule", {
|
|
412
|
-
|
|
412
|
+
where: f,
|
|
413
413
|
limit: 500,
|
|
414
414
|
orderBy: [{ field: "next_run_at", order: "asc" }],
|
|
415
415
|
context: SYSTEM_CTX
|
|
@@ -420,7 +420,7 @@ var ReportService = class {
|
|
|
420
420
|
async dispatchDue(now) {
|
|
421
421
|
const ts = (now ?? this.clock.now()).toISOString();
|
|
422
422
|
const due = await this.engine.find("sys_report_schedule", {
|
|
423
|
-
|
|
423
|
+
where: { active: true },
|
|
424
424
|
limit: 200,
|
|
425
425
|
context: SYSTEM_CTX
|
|
426
426
|
});
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/report-service.ts","../src/reports-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * @objectstack/plugin-reports\n *\n * Saved reports + scheduled email digests for ObjectStack.\n * Persists `sys_saved_report` definitions and `sys_report_schedule`\n * rows, then drives a dispatcher that runs due schedules and emails\n * the rendered output via the configured `email` service.\n */\n\nexport { SysSavedReport, SysReportSchedule } from '@objectstack/platform-objects/audit';\nexport {\n ReportService,\n renderReport,\n type ReportEngine,\n type ReportEmail,\n type ReportClock,\n type ReportServiceOptions,\n} from './report-service.js';\nexport {\n ReportsServicePlugin,\n type ReportsPluginOptions,\n} from './reports-plugin.js';\nexport type {\n IReportService,\n SavedReport,\n ReportSchedule,\n ReportQuery,\n ReportRunResult,\n ReportFormat,\n SaveReportInput,\n ScheduleReportInput,\n} from '@objectstack/spec/contracts';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type {\n IReportService,\n SavedReport,\n ReportSchedule,\n ReportQuery,\n ReportRunResult,\n ReportFormat,\n SaveReportInput,\n ScheduleReportInput,\n SharingExecutionContext,\n} from '@objectstack/spec/contracts';\nimport { Cron } from 'croner';\n\n/**\n * Narrow engine surface — keeps the service testable without booting\n * a real ObjectQL kernel.\n */\nexport interface ReportEngine {\n find(object: string, options?: any): Promise<any[]>;\n findOne?(object: string, options?: any): Promise<any>;\n insert(object: string, data: any, options?: any): Promise<any>;\n update(object: string, idOrData: any, dataOrOptions?: any, options?: any): Promise<any>;\n delete(object: string, options?: any): Promise<any>;\n}\n\n/**\n * Minimum email surface — implementations may pass the full\n * `IEmailService` instance straight through.\n */\nexport interface ReportEmail {\n send(input: {\n to: string | string[];\n subject: string;\n text?: string;\n html?: string;\n attachments?: Array<{ filename: string; content: string; contentType?: string }>;\n relatedObject?: string;\n relatedId?: string;\n }): Promise<{ status: 'sent' | 'queued' | 'failed' }>;\n}\n\n/** Stamped only in tests / specialised callers to make `now` deterministic. */\nexport interface ReportClock { now(): Date }\n\nconst SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] } as const;\n\nconst DEFAULT_FORMAT: ReportFormat = 'csv';\nconst DEFAULT_INTERVAL_MIN = 1440;\nconst DEFAULT_LIMIT = 1000;\n\nfunction uid(prefix: string): string {\n const g: any = globalThis as any;\n if (g.crypto?.randomUUID) return `${prefix}_${g.crypto.randomUUID()}`;\n return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction parseQuery(raw: unknown): ReportQuery {\n if (!raw) return {};\n if (typeof raw === 'string') {\n try { return JSON.parse(raw) as ReportQuery; }\n catch { return {}; }\n }\n if (typeof raw === 'object') return raw as ReportQuery;\n return {};\n}\n\nfunction rowFromSaved(row: any): SavedReport {\n return {\n id: String(row.id),\n name: String(row.name ?? ''),\n description: row.description ?? undefined,\n object_name: String(row.object_name ?? ''),\n query: parseQuery(row.query_json),\n format: (row.format as ReportFormat) ?? DEFAULT_FORMAT,\n owner_id: row.owner_id ?? undefined,\n last_run_at: row.last_run_at ?? undefined,\n last_row_count: row.last_row_count ?? undefined,\n created_at: row.created_at ?? undefined,\n updated_at: row.updated_at ?? undefined,\n };\n}\n\nfunction rowFromSchedule(row: any): ReportSchedule {\n return {\n id: String(row.id),\n report_id: String(row.report_id),\n name: row.name ?? undefined,\n interval_minutes: row.interval_minutes ?? undefined,\n cron_expression: row.cron_expression ?? undefined,\n timezone: row.timezone ?? undefined,\n active: row.active !== false,\n recipients: String(row.recipients ?? ''),\n format: row.format ?? undefined,\n subject_template: row.subject_template ?? undefined,\n owner_id: row.owner_id ?? undefined,\n next_run_at: row.next_run_at ?? undefined,\n last_sent_at: row.last_sent_at ?? undefined,\n last_status: row.last_status ?? undefined,\n last_error: row.last_error ?? undefined,\n };\n}\n\n// ─── Rendering ─────────────────────────────────────────────────────\n\nfunction escapeCsvCell(v: unknown): string {\n if (v == null) return '';\n const s = typeof v === 'string' ? v : (typeof v === 'object' ? JSON.stringify(v) : String(v));\n if (/[\",\\r\\n]/.test(s)) return `\"${s.replace(/\"/g, '\"\"')}\"`;\n return s;\n}\n\nfunction pickFields(rows: any[], explicit?: string[]): string[] {\n if (explicit && explicit.length > 0) return explicit;\n const seen = new Set<string>();\n for (const r of rows.slice(0, 50)) {\n if (r && typeof r === 'object') for (const k of Object.keys(r)) seen.add(k);\n }\n return Array.from(seen);\n}\n\nfunction renderCsv(rows: any[], fields?: string[]): string {\n const cols = pickFields(rows, fields);\n const head = cols.join(',');\n const body = rows.map(r => cols.map(c => escapeCsvCell(r?.[c])).join(',')).join('\\r\\n');\n return body.length > 0 ? `${head}\\r\\n${body}` : head;\n}\n\nfunction renderJson(rows: any[]): string {\n return JSON.stringify(rows, null, 2);\n}\n\nfunction escapeHtml(s: string): string {\n return s.replace(/[&<>\"']/g, c => ({\n '&': '&', '<': '<', '>': '>', '\"': '"', \"'\": ''',\n } as Record<string, string>)[c]);\n}\n\nfunction renderHtmlTable(rows: any[], fields?: string[]): string {\n const cols = pickFields(rows, fields);\n const th = cols.map(c => `<th style=\"text-align:left;padding:4px 8px;border-bottom:1px solid #ccc;\">${escapeHtml(c)}</th>`).join('');\n const trs = rows.map(r => {\n const tds = cols.map(c => {\n const v = r?.[c];\n const s = v == null ? '' : (typeof v === 'string' ? v : (typeof v === 'object' ? JSON.stringify(v) : String(v)));\n return `<td style=\"padding:4px 8px;border-bottom:1px solid #eee;\">${escapeHtml(s)}</td>`;\n }).join('');\n return `<tr>${tds}</tr>`;\n }).join('');\n return `<table style=\"border-collapse:collapse;font-family:system-ui,Arial,sans-serif;font-size:13px;\">`\n + `<thead><tr>${th}</tr></thead><tbody>${trs}</tbody></table>`;\n}\n\nexport function renderReport(rows: any[], format: ReportFormat, fields?: string[]): string {\n switch (format) {\n case 'json': return renderJson(rows);\n case 'html_table': return renderHtmlTable(rows, fields);\n case 'csv':\n default: return renderCsv(rows, fields);\n }\n}\n\n// ─── Subject templating (minimal {{var}}) ─────────────────────────\n\nfunction renderSubject(template: string | undefined, vars: Record<string, string>): string {\n const tpl = template ?? '{{name}} — {{date}}';\n return tpl.replace(/\\{\\{\\s*(\\w+)\\s*\\}\\}/g, (_m, k) => vars[String(k)] ?? '');\n}\n\n// ─── Service ──────────────────────────────────────────────────────\n\n/**\n * Resolves a saved report's owner (`owner_id`) into a real, RLS-bearing\n * `ExecutionContext` so a **scheduled** report executes under the owner's\n * authority — the same rows the owner would see interactively — instead of\n * bypassing RLS with a system context. Returns `null` when the owner cannot\n * be resolved (unknown/disabled user), in which case the scheduler fails the\n * run closed rather than running elevated (#2849 / #2980). Supplying this\n * resolver is the reports-surface consumer of ADR-0073's user-less identity\n * resolution.\n */\nexport type OwnerContextResolver = (\n ownerId: string,\n) => Promise<SharingExecutionContext | null>;\n\nexport interface ReportServiceOptions {\n engine: ReportEngine;\n email?: ReportEmail;\n clock?: ReportClock;\n logger?: { info?: (msg: any, ...rest: any[]) => void; warn?: (msg: any, ...rest: any[]) => void; error?: (msg: any, ...rest: any[]) => void };\n /** Cap rows per report to protect both DB and email size. */\n maxRows?: number;\n /**\n * Resolves a report owner into an RLS-bearing context for scheduled runs\n * (see {@link OwnerContextResolver}). When omitted, scheduled reports fail\n * closed instead of running with RLS bypassed (#2980).\n */\n resolveOwnerContext?: OwnerContextResolver;\n /**\n * [#3544 / #3710] The user-level export axis —\n * `ISecurityService.canExport(object, context)`, wired by the reports plugin\n * from `getService('security')`.\n *\n * A report rendered as `csv`/`json` IS a bulk machine-readable copy of the\n * object, so it is the same privilege `GET /data/:object/export` gates.\n * Without this the axis had a side door: a caller refused at that route could\n * save a report on the same object, run it as CSV — or schedule one to their\n * own inbox — and receive the identical rows.\n *\n * Omitted (no `plugin-security`, so no permission sets exist anywhere) → the\n * axis does not apply, matching the REST export route's own fail-open.\n */\n canExport?: (object: string, context: unknown) => Promise<boolean>;\n}\n\n/**\n * [#3544 / #3710] Report formats that constitute a BULK EXPORT rather than a\n * rendering.\n *\n * `csv`/`json` are machine-readable copies — re-importable elsewhere, and\n * exactly what `GET /data/:object/export` serves. `html_table` is a PRESENTED\n * view: the report equivalent of reading rows on screen, which any caller\n * holding `allowRead` may already do. Gating it would restrict reading rather\n * than exporting and would take the axis past what it is for.\n */\nconst BULK_EXPORT_FORMATS: ReadonlySet<string> = new Set(['csv', 'json']);\n\nexport class ReportService implements IReportService {\n private readonly engine: ReportEngine;\n private readonly email?: ReportEmail;\n private readonly clock: ReportClock;\n private readonly logger: NonNullable<ReportServiceOptions['logger']>;\n private readonly maxRows: number;\n private readonly resolveOwnerContext?: OwnerContextResolver;\n private readonly canExportFn?: (object: string, context: unknown) => Promise<boolean>;\n\n constructor(opts: ReportServiceOptions) {\n this.engine = opts.engine;\n this.email = opts.email;\n this.clock = opts.clock ?? { now: () => new Date() };\n this.logger = opts.logger ?? {};\n this.maxRows = Math.max(1, opts.maxRows ?? 5000);\n this.resolveOwnerContext = opts.resolveOwnerContext;\n this.canExportFn = opts.canExport;\n }\n\n /**\n * [#3544 / #3710] Gate a report rendering on the user-level export axis.\n *\n * Throws `EXPORT_NOT_PERMITTED` when the principal behind `context` may not\n * take a bulk copy of `object`. A no-op for non-bulk formats (`html_table`),\n * for a system context, and when no `canExport` is wired.\n *\n * Deliberately checked HERE — one place — rather than at each of the three\n * callers (`runReport`, the ad-hoc run, and the scheduled dispatch): a gate\n * per call site is how a fourth call site later ships ungated. `dispatchDue`\n * routes through `executeReport` too, so the scheduled CSV is covered by the\n * same line. (`scheduleReport` additionally pre-checks, so an author is\n * refused when they create the schedule rather than silently at 3am — but\n * that is UX, and THIS is the enforcement: a grant revoked after the schedule\n * was created must still stop the delivery.)\n *\n * Fails CLOSED on a throw — it resolves permission sets to decide, and a\n * resolution failure must never read as a grant (ADR-0049).\n */\n private async assertExportAllowed(\n object: string,\n format: string,\n context: SharingExecutionContext | undefined,\n ): Promise<void> {\n if (!BULK_EXPORT_FORMATS.has(format)) return;\n if (context?.isSystem) return;\n if (!this.canExportFn) return;\n let allowed: boolean;\n try {\n allowed = await this.canExportFn(object, context);\n } catch (err) {\n this.logger.warn?.('ReportService: canExport check failed — denying export', err);\n allowed = false;\n }\n if (!allowed) {\n throw new Error(\n `EXPORT_NOT_PERMITTED: exporting '${object}' as ${format} is not permitted for this user`,\n );\n }\n }\n\n // ── Access control ─────────────────────────────────────────────\n\n /**\n * Authorization for a saved-report row. `sys_saved_report` is a\n * protection-locked system object, so its rows are *read* with\n * `SYSTEM_CTX`; the caller's right to see/mutate a specific report is\n * enforced HERE, by owner match, not by the metadata read's own RLS —\n * otherwise any authenticated caller could read/delete/overwrite any\n * report by id (#2980). An explicit elevated context (`isSystem`) — the\n * scheduler / server tooling — sees everything.\n */\n private canAccessReport(row: { owner_id?: unknown } | null | undefined, context: SharingExecutionContext | undefined): boolean {\n if (!row) return false;\n if (context?.isSystem) return true;\n const userId = context?.userId;\n return !!userId && row.owner_id === userId;\n }\n\n /** Raw metadata read of a saved report by id (no authz — callers gate). */\n private async loadReportRow(reportId: string): Promise<any | null> {\n const rows = await this.engine.find('sys_saved_report', {\n filter: { id: reportId }, limit: 1, context: SYSTEM_CTX,\n });\n return Array.isArray(rows) && rows[0] ? rows[0] : null;\n }\n\n // ── Report CRUD ────────────────────────────────────────────────\n\n async saveReport(input: SaveReportInput, context: SharingExecutionContext): Promise<SavedReport> {\n if (!input.name) throw new Error('VALIDATION_FAILED: name is required');\n if (!input.object) throw new Error('VALIDATION_FAILED: object is required');\n if (!input.query) throw new Error('VALIDATION_FAILED: query is required');\n\n const now = this.clock.now().toISOString();\n // A non-system caller always owns what they create — a caller-supplied\n // ownerId cannot assign the report to someone else (#2980). Only an\n // explicit elevated context (server tooling / import) may set it.\n const ownerId = context.isSystem ? (input.ownerId ?? context.userId ?? null) : (context.userId ?? null);\n const payload: any = {\n name: input.name,\n description: input.description ?? null,\n object_name: input.object,\n query_json: JSON.stringify(input.query ?? {}),\n format: input.format ?? DEFAULT_FORMAT,\n owner_id: ownerId,\n updated_at: now,\n };\n\n if (input.id) {\n const existing = await this.loadReportRow(input.id);\n if (existing) {\n // An update to an existing report is a mutation — a caller may only\n // overwrite a report they own (#2980). Not-found for others so the\n // response doesn't leak that the id exists.\n if (!this.canAccessReport(existing, context)) {\n throw new Error(`REPORT_NOT_FOUND: ${input.id}`);\n }\n // Never let a non-system caller reassign ownership away from the row.\n if (!context.isSystem) payload.owner_id = existing.owner_id ?? payload.owner_id;\n await this.engine.update('sys_saved_report', { id: input.id, ...payload }, { context: SYSTEM_CTX });\n return rowFromSaved({ ...existing, ...payload, id: input.id });\n }\n }\n\n const id = input.id ?? uid('rpt');\n const row = { id, ...payload, created_at: now };\n await this.engine.insert('sys_saved_report', row, { context: SYSTEM_CTX });\n return rowFromSaved(row);\n }\n\n async listReports(\n filter: { object?: string; ownerId?: string } | undefined,\n context: SharingExecutionContext,\n ): Promise<SavedReport[]> {\n const f: any = {};\n if (filter?.object) f.object_name = filter.object;\n // Owner scoping (#2980): a non-system caller sees ONLY their own reports —\n // a caller-supplied ownerId can never widen past their own id. A caller\n // with no identity sees nothing (fail closed). System/tooling sees all,\n // honouring an explicit ownerId narrow.\n if (context?.isSystem) {\n if (filter?.ownerId) f.owner_id = filter.ownerId;\n } else {\n if (!context?.userId) return [];\n if (filter?.ownerId && filter.ownerId !== context.userId) return [];\n f.owner_id = context.userId;\n }\n const rows = await this.engine.find('sys_saved_report', {\n filter: f, limit: 500, orderBy: [{ field: 'updated_at', order: 'desc' }], context: SYSTEM_CTX,\n });\n return Array.isArray(rows) ? rows.map(rowFromSaved) : [];\n }\n\n async getReport(reportId: string, context: SharingExecutionContext): Promise<SavedReport | null> {\n const row = await this.loadReportRow(reportId);\n // Unauthorized reads are indistinguishable from a genuine miss (#2980).\n if (!this.canAccessReport(row, context)) return null;\n return rowFromSaved(row);\n }\n\n async deleteReport(reportId: string, context: SharingExecutionContext): Promise<void> {\n if (!reportId) throw new Error('VALIDATION_FAILED: reportId is required');\n const row = await this.loadReportRow(reportId);\n if (!row) return; // idempotent — nothing to drop\n // A caller may only delete a report they own (#2980); others get a\n // not-found so the delete neither fires nor reveals the report's existence.\n if (!this.canAccessReport(row, context)) {\n throw new Error(`REPORT_NOT_FOUND: ${reportId}`);\n }\n // Cascade — drop attached schedules first.\n const schedules = await this.engine.find('sys_report_schedule', {\n filter: { report_id: reportId }, limit: 500, context: SYSTEM_CTX,\n });\n for (const s of (schedules ?? [])) {\n await this.engine.delete('sys_report_schedule', { where: { id: (s as any).id }, context: SYSTEM_CTX });\n }\n await this.engine.delete('sys_saved_report', { where: { id: reportId }, context: SYSTEM_CTX });\n }\n\n // ── Execution ───────────────────────────────────────────────────\n\n async run(reportId: string, context: SharingExecutionContext): Promise<ReportRunResult> {\n const report = await this.getReport(reportId, context);\n if (!report) throw new Error(`REPORT_NOT_FOUND: ${reportId}`);\n return this.executeReport(report, context);\n }\n\n async runAdHoc(input: SaveReportInput, context: SharingExecutionContext): Promise<ReportRunResult> {\n if (!input.object) throw new Error('VALIDATION_FAILED: object is required');\n if (!input.query) throw new Error('VALIDATION_FAILED: query is required');\n const adhoc: SavedReport = {\n id: '__adhoc__',\n name: input.name ?? 'Ad-hoc report',\n object_name: input.object,\n query: input.query,\n format: input.format ?? DEFAULT_FORMAT,\n };\n return this.executeReport(adhoc, context, /* stamp */ false);\n }\n\n private async executeReport(\n report: SavedReport,\n context: SharingExecutionContext,\n stamp = true,\n ): Promise<ReportRunResult> {\n // [#3544 / #3710] The export axis, BEFORE any row is read — a refusal must\n // not be reachable after the data has already been pulled.\n await this.assertExportAllowed(report.object_name, report.format, context);\n const q = report.query ?? {};\n const limit = Math.min(q.limit ?? DEFAULT_LIMIT, this.maxRows);\n const rows = await this.engine.find(report.object_name, {\n filter: q.filter,\n fields: q.fields,\n orderBy: q.orderBy,\n limit,\n // Reports execute with the caller's identity so sharing rules\n // (if installed) apply. Falls back to system bypass only when\n // the report definition was created by a system writer.\n context: {\n userId: context.userId,\n tenantId: context.tenantId,\n positions: context.positions ?? [],\n permissions: context.permissions ?? [],\n isSystem: context.isSystem ?? false,\n },\n });\n const list = Array.isArray(rows) ? rows : [];\n const body = renderReport(list, report.format, q.fields);\n const ranAt = this.clock.now().toISOString();\n\n if (stamp && report.id !== '__adhoc__') {\n try {\n await this.engine.update('sys_saved_report', {\n id: report.id,\n last_run_at: ranAt,\n last_row_count: list.length,\n updated_at: ranAt,\n }, { context: SYSTEM_CTX });\n } catch (err) {\n this.logger.warn?.('ReportService: failed to stamp last_run_at', err);\n }\n }\n\n return {\n reportId: report.id,\n rowCount: list.length,\n format: report.format,\n body,\n rows: list,\n ranAt,\n };\n }\n\n // ── Schedules ──────────────────────────────────────────────────\n\n async scheduleReport(input: ScheduleReportInput, context: SharingExecutionContext): Promise<ReportSchedule> {\n if (!input.reportId) throw new Error('VALIDATION_FAILED: reportId is required');\n if (!input.recipients || input.recipients.length === 0) {\n throw new Error('VALIDATION_FAILED: recipients must be a non-empty array');\n }\n const report = await this.getReport(input.reportId, context);\n if (!report) throw new Error(`REPORT_NOT_FOUND: ${input.reportId}`);\n\n // [#3544 / #3710] Refuse a bulk-format schedule the author could not run\n // themselves, at CREATE time — otherwise the refusal only surfaces on the\n // first silent 3am sweep. Advisory only: `executeReport` re-checks on every\n // dispatch, which is what catches a grant revoked after this point.\n await this.assertExportAllowed(report.object_name, input.format ?? 'html_table', context);\n\n const now = this.clock.now();\n const interval = input.intervalMinutes ?? DEFAULT_INTERVAL_MIN;\n const cron = input.cronExpression?.trim() || null;\n if (cron) {\n // Validate eagerly so an author gets a clear error at schedule time\n // instead of a schedule that silently falls back to interval on sweep.\n try {\n new Cron(cron, { timezone: input.timezone || 'UTC' });\n } catch (err) {\n throw new Error(`VALIDATION_FAILED: invalid cron_expression '${cron}': ${(err as Error).message}`);\n }\n }\n const nextRun = this.nextRunAt(\n { cron_expression: cron, interval_minutes: interval, timezone: input.timezone ?? 'UTC' },\n now,\n ).toISOString();\n const id = uid('rsch');\n const row: any = {\n id,\n report_id: input.reportId,\n name: input.name ?? null,\n interval_minutes: interval,\n cron_expression: cron,\n timezone: input.timezone ?? 'UTC',\n active: input.active !== false,\n recipients: input.recipients.join(','),\n format: input.format ?? 'html_table',\n subject_template: input.subjectTemplate ?? null,\n owner_id: input.ownerId ?? context.userId ?? null,\n next_run_at: nextRun,\n created_at: now.toISOString(),\n updated_at: now.toISOString(),\n };\n await this.engine.insert('sys_report_schedule', row, { context: SYSTEM_CTX });\n return rowFromSchedule(row);\n }\n\n async unscheduleReport(scheduleId: string, _context: SharingExecutionContext): Promise<void> {\n if (!scheduleId) throw new Error('VALIDATION_FAILED: scheduleId is required');\n await this.engine.delete('sys_report_schedule', { where: { id: scheduleId }, context: SYSTEM_CTX });\n }\n\n async listSchedules(\n filter: { reportId?: string } | undefined,\n _context: SharingExecutionContext,\n ): Promise<ReportSchedule[]> {\n const f: any = {};\n if (filter?.reportId) f.report_id = filter.reportId;\n const rows = await this.engine.find('sys_report_schedule', {\n filter: f, limit: 500, orderBy: [{ field: 'next_run_at', order: 'asc' }], context: SYSTEM_CTX,\n });\n return Array.isArray(rows) ? rows.map(rowFromSchedule) : [];\n }\n\n // ── Dispatcher ─────────────────────────────────────────────────\n\n async dispatchDue(now?: Date): Promise<{ fired: number; failed: number; skipped: number }> {\n const ts = (now ?? this.clock.now()).toISOString();\n const due = await this.engine.find('sys_report_schedule', {\n filter: { active: true },\n limit: 200,\n context: SYSTEM_CTX,\n });\n const list = (Array.isArray(due) ? due : []).map(rowFromSchedule)\n .filter(s => !s.next_run_at || s.next_run_at <= ts);\n\n let fired = 0, failed = 0, skipped = 0;\n for (const schedule of list) {\n try {\n const row = await this.loadReportRow(schedule.report_id);\n if (!row) {\n skipped++;\n await this.markSchedule(schedule.id, {\n last_status: 'skipped',\n last_error: `report ${schedule.report_id} missing`,\n });\n continue;\n }\n const report = rowFromSaved(row);\n\n // Run the report under the OWNER's authority, not system (#2980).\n // A scheduled run must not read rows the report's owner cannot see —\n // that was a silent RLS bypass (a member's scheduled report emailed\n // the target object's entire table). Resolve the owner to a real\n // RLS-bearing context; if we can't (no resolver wired, or unknown/\n // disabled owner), FAIL CLOSED rather than run elevated.\n const ownerId = report.owner_id;\n const runContext = ownerId && this.resolveOwnerContext\n ? await this.resolveOwnerContext(ownerId).catch((err) => {\n this.logger.warn?.('ReportService.dispatchDue: owner context resolution failed', err);\n return null;\n })\n : null;\n if (!runContext) {\n failed++;\n await this.markSchedule(schedule.id, {\n last_status: 'failed',\n last_error: ownerId\n ? `owner '${ownerId}' context unavailable — refusing to run scheduled report with RLS bypassed (#2849/#2980)`\n : 'report has no owner — refusing to run scheduled report with RLS bypassed (#2849/#2980)',\n });\n continue;\n }\n\n // Force the schedule's own format so the recipient gets what\n // the admin configured (CSV attachment vs inline HTML table).\n const fmt: ReportFormat = (schedule.format ?? 'html_table') as ReportFormat;\n const result = await this.executeReport({ ...report, format: fmt }, runContext, false);\n\n const recipients = schedule.recipients.split(',').map(s => s.trim()).filter(Boolean);\n const subject = renderSubject(schedule.subject_template, {\n name: schedule.name ?? report.name,\n date: ts.slice(0, 10),\n rows: String(result.rowCount),\n });\n\n if (this.email && recipients.length > 0) {\n if (fmt === 'csv') {\n await this.email.send({\n to: recipients,\n subject,\n text: `Attached: ${result.rowCount} row(s).`,\n attachments: [{\n // Keep unicode letters (CJK schedule names) — only strip\n // filesystem-hostile characters, else 周报 becomes `__`.\n filename: `${(schedule.name ?? report.name).replace(/[^\\p{L}\\p{N}._-]+/gu, '_').replace(/^_+|_+$/g, '') || 'report'}-${ts.slice(0, 10)}.csv`,\n content: result.body,\n contentType: 'text/csv',\n }],\n relatedObject: 'sys_report_schedule',\n relatedId: schedule.id,\n });\n } else {\n await this.email.send({\n to: recipients,\n subject,\n html: `<p>${escapeHtml(report.name)} — ${result.rowCount} row(s)</p>${result.body}`,\n text: `${report.name} — ${result.rowCount} row(s)`,\n relatedObject: 'sys_report_schedule',\n relatedId: schedule.id,\n });\n }\n } else if (!this.email) {\n this.logger.warn?.('ReportService.dispatchDue: no email service — schedule fired but mail not sent');\n }\n\n await this.advanceSchedule(schedule, ts);\n fired++;\n } catch (err: any) {\n failed++;\n await this.markSchedule(schedule.id, {\n last_status: 'failed',\n last_error: String(err?.message ?? err ?? 'unknown').slice(0, 500),\n });\n this.logger.error?.('ReportService.dispatchDue: schedule failed', err);\n }\n }\n return { fired, failed, skipped };\n }\n\n /**\n * Compute the next fire time for a schedule. A `cron_expression` wins over\n * `interval_minutes` (the documented `sys_report_schedule` contract) and is\n * evaluated in the schedule's `timezone` (default UTC) via croner — the same\n * library the job scheduler uses. Falls back to `from + interval_minutes` for\n * interval schedules, and also if a cron expression is invalid or has no\n * future occurrence (logged; never throws into the sweep). `from` is the\n * reference instant (the injected clock), so `today()`-style boundaries honor\n * the test clock.\n */\n private nextRunAt(\n schedule: { cron_expression?: string | null; interval_minutes?: number | null; timezone?: string | null },\n from: Date,\n ): Date {\n const cron = (schedule.cron_expression ?? '').trim();\n if (cron) {\n try {\n const next = new Cron(cron, { timezone: schedule.timezone || 'UTC' }).nextRun(from);\n if (next) return next;\n this.logger.warn?.(`ReportService: cron '${cron}' has no next occurrence; falling back to interval`);\n } catch (err) {\n this.logger.warn?.(`ReportService: invalid cron '${cron}'; falling back to interval`, err);\n }\n }\n const interval = schedule.interval_minutes ?? DEFAULT_INTERVAL_MIN;\n return new Date(from.getTime() + interval * 60_000);\n }\n\n private async advanceSchedule(schedule: ReportSchedule, ranAt: string): Promise<void> {\n const nextRun = this.nextRunAt(schedule, this.clock.now()).toISOString();\n await this.engine.update('sys_report_schedule', {\n id: schedule.id,\n next_run_at: nextRun,\n last_sent_at: ranAt,\n last_status: 'ok',\n last_error: null,\n updated_at: ranAt,\n }, { context: SYSTEM_CTX });\n }\n\n private async markSchedule(id: string, patch: Record<string, unknown>): Promise<void> {\n try {\n await this.engine.update('sys_report_schedule', {\n id, ...patch, updated_at: this.clock.now().toISOString(),\n }, { context: SYSTEM_CTX });\n } catch (err) {\n this.logger.warn?.('ReportService: failed to mark schedule', err);\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport {\n SysSavedReport,\n SysReportSchedule,\n} from '@objectstack/platform-objects/audit';\nimport { ReportService, type ReportEngine, type ReportEmail } from './report-service.js';\n\nexport interface ReportsPluginOptions {\n /**\n * How often the dispatcher should poll `sys_report_schedule` for\n * due rows. Defaults to 60 seconds — short enough to honour\n * minute-grained schedules without flooding the DB.\n */\n dispatchIntervalMs?: number;\n /** Cap rows per report. Mirrors ReportServiceOptions.maxRows. */\n maxRows?: number;\n /** Disable the dispatcher tick entirely. */\n disableDispatcher?: boolean;\n}\n\n/**\n * ReportsServicePlugin — registers `sys_saved_report` /\n * `sys_report_schedule`, the `reports` service, and the dispatcher\n * loop that emails due schedules.\n *\n * The dispatcher uses `IJobService.schedule` when one is registered;\n * otherwise it falls back to a plain `setInterval` so single-kernel\n * deployments work without `service-job`.\n *\n * @example\n * ```ts\n * import { ReportsServicePlugin } from '@objectstack/plugin-reports';\n *\n * kernel.use(new ReportsServicePlugin({ dispatchIntervalMs: 60_000 }));\n * ```\n */\nexport class ReportsServicePlugin implements Plugin {\n name = 'com.objectstack.service.reports';\n version = '1.0.0';\n type = 'standard';\n dependencies = ['com.objectstack.engine.objectql'];\n\n private readonly options: ReportsPluginOptions;\n private service?: ReportService;\n private intervalHandle?: ReturnType<typeof setInterval>;\n private jobName?: string;\n private jobService?: any;\n\n constructor(options: ReportsPluginOptions = {}) {\n this.options = options;\n }\n\n async init(ctx: PluginContext): Promise<void> {\n ctx.getService<{ register(m: any): void }>('manifest').register({\n id: 'com.objectstack.service.reports',\n name: 'Reports Service',\n version: '1.0.0',\n type: 'plugin',\n scope: 'system',\n defaultDatasource: 'cloud',\n namespace: 'sys',\n objects: [SysSavedReport, SysReportSchedule],\n });\n ctx.logger.info('ReportsServicePlugin: schemas registered');\n }\n\n async start(ctx: PluginContext): Promise<void> {\n ctx.hook('kernel:ready', async () => {\n let engine: any = null;\n try { engine = ctx.getService<any>('objectql'); }\n catch { try { engine = ctx.getService<any>('data'); } catch { /* ignore */ } }\n if (!engine) {\n ctx.logger.warn('ReportsServicePlugin: no ObjectQL engine — service NOT registered');\n return;\n }\n\n let email: ReportEmail | undefined;\n try { email = ctx.getService<any>('email'); } catch { /* email is optional */ }\n if (!email) {\n ctx.logger.warn('ReportsServicePlugin: no email service — schedules will fire without delivery');\n }\n\n // [#3544 / #3710] The user-level export axis. A `csv`/`json` report is a\n // bulk machine-readable copy of its object — the same privilege\n // `GET /data/:object/export` gates — so the reports surface must ask the\n // SAME question of the SAME authority, or it is a side door around the\n // axis. Resolved lazily per call rather than captured here: `security` is\n // registered by plugin-security's own `kernel:ready` hook and may not\n // exist yet at construction time. Absent service (no plugin-security ⇒ no\n // permission sets anywhere) → the axis does not apply, matching the REST\n // export route's fail-open.\n const canExport = async (object: string, context: unknown): Promise<boolean> => {\n let security: any;\n try { security = ctx.getService<any>('security'); } catch { return true; }\n if (!security || typeof security.canExport !== 'function') return true;\n return await security.canExport(object, context);\n };\n\n this.service = new ReportService({\n engine: engine as ReportEngine,\n email,\n logger: ctx.logger,\n maxRows: this.options.maxRows,\n canExport,\n // Scheduled reports run under the owner's resolved RLS context, not a\n // system bypass (#2980). No owner-context resolver is wired yet — that\n // is the reports-surface consumer of ADR-0073's user-less identity\n // resolution (M2) — so until it lands, scheduled runs FAIL CLOSED\n // (skipped + marked failed) rather than exfiltrate. Interactive runs\n // (run/runAdHoc) are unaffected: they carry the caller's context.\n resolveOwnerContext: undefined,\n });\n ctx.registerService('reports', this.service);\n\n if (this.options.disableDispatcher) {\n ctx.logger.info('ReportsServicePlugin: dispatcher disabled (disableDispatcher=true)');\n return;\n }\n\n const intervalMs = Math.max(5_000, this.options.dispatchIntervalMs ?? 60_000);\n\n // Prefer the platform job service when available — it lets ops\n // see report dispatch alongside every other scheduled job.\n try {\n const job = ctx.getService<any>('job');\n if (job && typeof job.schedule === 'function') {\n this.jobService = job;\n this.jobName = 'reports.dispatch';\n await job.schedule(this.jobName, { type: 'interval', intervalMs }, async () => {\n try { await this.service?.dispatchDue(); }\n catch (err) { ctx.logger.warn('ReportsServicePlugin: dispatch tick failed', err as any); }\n });\n ctx.logger.info('ReportsServicePlugin: dispatcher registered with job service', { intervalMs });\n return;\n }\n } catch { /* fall through to setInterval */ }\n\n this.intervalHandle = setInterval(() => {\n this.service?.dispatchDue().catch(err => {\n ctx.logger.warn('ReportsServicePlugin: dispatch tick failed', err);\n });\n }, intervalMs);\n // Don't keep Node alive purely for the dispatcher — common\n // mistake in tests / serverless. unref is a no-op in some\n // runtimes which is fine.\n (this.intervalHandle as any)?.unref?.();\n ctx.logger.info('ReportsServicePlugin: dispatcher registered (setInterval fallback)', { intervalMs });\n });\n }\n\n async stop(ctx: PluginContext): Promise<void> {\n if (this.intervalHandle) clearInterval(this.intervalHandle);\n this.intervalHandle = undefined;\n if (this.jobService && this.jobName && typeof this.jobService.cancel === 'function') {\n try { await this.jobService.cancel(this.jobName); }\n catch (err) { ctx.logger.warn('ReportsServicePlugin: failed to cancel job', err as any); }\n }\n }\n}\n"],"mappings":"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","names":["import_audit"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/report-service.ts","../src/reports-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * @objectstack/plugin-reports\n *\n * Saved reports + scheduled email digests for ObjectStack.\n * Persists `sys_saved_report` definitions and `sys_report_schedule`\n * rows, then drives a dispatcher that runs due schedules and emails\n * the rendered output via the configured `email` service.\n */\n\nexport { SysSavedReport, SysReportSchedule } from '@objectstack/platform-objects/audit';\nexport {\n ReportService,\n renderReport,\n type ReportEngine,\n type ReportEmail,\n type ReportClock,\n type ReportServiceOptions,\n} from './report-service.js';\nexport {\n ReportsServicePlugin,\n type ReportsPluginOptions,\n} from './reports-plugin.js';\nexport type {\n IReportService,\n SavedReport,\n ReportSchedule,\n ReportQuery,\n ReportRunResult,\n ReportFormat,\n SaveReportInput,\n ScheduleReportInput,\n} from '@objectstack/spec/contracts';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type {\n IReportService,\n SavedReport,\n ReportSchedule,\n ReportQuery,\n ReportRunResult,\n ReportFormat,\n SaveReportInput,\n ScheduleReportInput,\n SharingExecutionContext,\n} from '@objectstack/spec/contracts';\nimport { Cron } from 'croner';\n\n/**\n * Narrow engine surface — keeps the service testable without booting\n * a real ObjectQL kernel.\n */\nexport interface ReportEngine {\n find(object: string, options?: any): Promise<any[]>;\n findOne?(object: string, options?: any): Promise<any>;\n insert(object: string, data: any, options?: any): Promise<any>;\n update(object: string, idOrData: any, dataOrOptions?: any, options?: any): Promise<any>;\n delete(object: string, options?: any): Promise<any>;\n}\n\n/**\n * Minimum email surface — implementations may pass the full\n * `IEmailService` instance straight through.\n */\nexport interface ReportEmail {\n send(input: {\n to: string | string[];\n subject: string;\n text?: string;\n html?: string;\n attachments?: Array<{ filename: string; content: string; contentType?: string }>;\n relatedObject?: string;\n relatedId?: string;\n }): Promise<{ status: 'sent' | 'queued' | 'failed' }>;\n}\n\n/** Stamped only in tests / specialised callers to make `now` deterministic. */\nexport interface ReportClock { now(): Date }\n\nconst SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] } as const;\n\nconst DEFAULT_FORMAT: ReportFormat = 'csv';\nconst DEFAULT_INTERVAL_MIN = 1440;\nconst DEFAULT_LIMIT = 1000;\n\nfunction uid(prefix: string): string {\n const g: any = globalThis as any;\n if (g.crypto?.randomUUID) return `${prefix}_${g.crypto.randomUUID()}`;\n return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction parseQuery(raw: unknown): ReportQuery {\n if (!raw) return {};\n if (typeof raw === 'string') {\n try { return JSON.parse(raw) as ReportQuery; }\n catch { return {}; }\n }\n if (typeof raw === 'object') return raw as ReportQuery;\n return {};\n}\n\nfunction rowFromSaved(row: any): SavedReport {\n return {\n id: String(row.id),\n name: String(row.name ?? ''),\n description: row.description ?? undefined,\n object_name: String(row.object_name ?? ''),\n query: parseQuery(row.query_json),\n format: (row.format as ReportFormat) ?? DEFAULT_FORMAT,\n owner_id: row.owner_id ?? undefined,\n last_run_at: row.last_run_at ?? undefined,\n last_row_count: row.last_row_count ?? undefined,\n created_at: row.created_at ?? undefined,\n updated_at: row.updated_at ?? undefined,\n };\n}\n\nfunction rowFromSchedule(row: any): ReportSchedule {\n return {\n id: String(row.id),\n report_id: String(row.report_id),\n name: row.name ?? undefined,\n interval_minutes: row.interval_minutes ?? undefined,\n cron_expression: row.cron_expression ?? undefined,\n timezone: row.timezone ?? undefined,\n active: row.active !== false,\n recipients: String(row.recipients ?? ''),\n format: row.format ?? undefined,\n subject_template: row.subject_template ?? undefined,\n owner_id: row.owner_id ?? undefined,\n next_run_at: row.next_run_at ?? undefined,\n last_sent_at: row.last_sent_at ?? undefined,\n last_status: row.last_status ?? undefined,\n last_error: row.last_error ?? undefined,\n };\n}\n\n// ─── Rendering ─────────────────────────────────────────────────────\n\nfunction escapeCsvCell(v: unknown): string {\n if (v == null) return '';\n const s = typeof v === 'string' ? v : (typeof v === 'object' ? JSON.stringify(v) : String(v));\n if (/[\",\\r\\n]/.test(s)) return `\"${s.replace(/\"/g, '\"\"')}\"`;\n return s;\n}\n\nfunction pickFields(rows: any[], explicit?: string[]): string[] {\n if (explicit && explicit.length > 0) return explicit;\n const seen = new Set<string>();\n for (const r of rows.slice(0, 50)) {\n if (r && typeof r === 'object') for (const k of Object.keys(r)) seen.add(k);\n }\n return Array.from(seen);\n}\n\nfunction renderCsv(rows: any[], fields?: string[]): string {\n const cols = pickFields(rows, fields);\n const head = cols.join(',');\n const body = rows.map(r => cols.map(c => escapeCsvCell(r?.[c])).join(',')).join('\\r\\n');\n return body.length > 0 ? `${head}\\r\\n${body}` : head;\n}\n\nfunction renderJson(rows: any[]): string {\n return JSON.stringify(rows, null, 2);\n}\n\nfunction escapeHtml(s: string): string {\n return s.replace(/[&<>\"']/g, c => ({\n '&': '&', '<': '<', '>': '>', '\"': '"', \"'\": ''',\n } as Record<string, string>)[c]);\n}\n\nfunction renderHtmlTable(rows: any[], fields?: string[]): string {\n const cols = pickFields(rows, fields);\n const th = cols.map(c => `<th style=\"text-align:left;padding:4px 8px;border-bottom:1px solid #ccc;\">${escapeHtml(c)}</th>`).join('');\n const trs = rows.map(r => {\n const tds = cols.map(c => {\n const v = r?.[c];\n const s = v == null ? '' : (typeof v === 'string' ? v : (typeof v === 'object' ? JSON.stringify(v) : String(v)));\n return `<td style=\"padding:4px 8px;border-bottom:1px solid #eee;\">${escapeHtml(s)}</td>`;\n }).join('');\n return `<tr>${tds}</tr>`;\n }).join('');\n return `<table style=\"border-collapse:collapse;font-family:system-ui,Arial,sans-serif;font-size:13px;\">`\n + `<thead><tr>${th}</tr></thead><tbody>${trs}</tbody></table>`;\n}\n\nexport function renderReport(rows: any[], format: ReportFormat, fields?: string[]): string {\n switch (format) {\n case 'json': return renderJson(rows);\n case 'html_table': return renderHtmlTable(rows, fields);\n case 'csv':\n default: return renderCsv(rows, fields);\n }\n}\n\n// ─── Subject templating (minimal {{var}}) ─────────────────────────\n\nfunction renderSubject(template: string | undefined, vars: Record<string, string>): string {\n const tpl = template ?? '{{name}} — {{date}}';\n return tpl.replace(/\\{\\{\\s*(\\w+)\\s*\\}\\}/g, (_m, k) => vars[String(k)] ?? '');\n}\n\n// ─── Service ──────────────────────────────────────────────────────\n\n/**\n * Resolves a saved report's owner (`owner_id`) into a real, RLS-bearing\n * `ExecutionContext` so a **scheduled** report executes under the owner's\n * authority — the same rows the owner would see interactively — instead of\n * bypassing RLS with a system context. Returns `null` when the owner cannot\n * be resolved (unknown/disabled user), in which case the scheduler fails the\n * run closed rather than running elevated (#2849 / #2980). Supplying this\n * resolver is the reports-surface consumer of ADR-0073's user-less identity\n * resolution.\n */\nexport type OwnerContextResolver = (\n ownerId: string,\n) => Promise<SharingExecutionContext | null>;\n\nexport interface ReportServiceOptions {\n engine: ReportEngine;\n email?: ReportEmail;\n clock?: ReportClock;\n logger?: { info?: (msg: any, ...rest: any[]) => void; warn?: (msg: any, ...rest: any[]) => void; error?: (msg: any, ...rest: any[]) => void };\n /** Cap rows per report to protect both DB and email size. */\n maxRows?: number;\n /**\n * Resolves a report owner into an RLS-bearing context for scheduled runs\n * (see {@link OwnerContextResolver}). When omitted, scheduled reports fail\n * closed instead of running with RLS bypassed (#2980).\n */\n resolveOwnerContext?: OwnerContextResolver;\n /**\n * [#3544 / #3710] The user-level export axis —\n * `ISecurityService.canExport(object, context)`, wired by the reports plugin\n * from `getService('security')`.\n *\n * A report rendered as `csv`/`json` IS a bulk machine-readable copy of the\n * object, so it is the same privilege `GET /data/:object/export` gates.\n * Without this the axis had a side door: a caller refused at that route could\n * save a report on the same object, run it as CSV — or schedule one to their\n * own inbox — and receive the identical rows.\n *\n * Omitted (no `plugin-security`, so no permission sets exist anywhere) → the\n * axis does not apply, matching the REST export route's own fail-open.\n */\n canExport?: (object: string, context: unknown) => Promise<boolean>;\n}\n\n/**\n * [#3544 / #3710] Report formats that constitute a BULK EXPORT rather than a\n * rendering.\n *\n * `csv`/`json` are machine-readable copies — re-importable elsewhere, and\n * exactly what `GET /data/:object/export` serves. `html_table` is a PRESENTED\n * view: the report equivalent of reading rows on screen, which any caller\n * holding `allowRead` may already do. Gating it would restrict reading rather\n * than exporting and would take the axis past what it is for.\n */\nconst BULK_EXPORT_FORMATS: ReadonlySet<string> = new Set(['csv', 'json']);\n\nexport class ReportService implements IReportService {\n private readonly engine: ReportEngine;\n private readonly email?: ReportEmail;\n private readonly clock: ReportClock;\n private readonly logger: NonNullable<ReportServiceOptions['logger']>;\n private readonly maxRows: number;\n private readonly resolveOwnerContext?: OwnerContextResolver;\n private readonly canExportFn?: (object: string, context: unknown) => Promise<boolean>;\n\n constructor(opts: ReportServiceOptions) {\n this.engine = opts.engine;\n this.email = opts.email;\n this.clock = opts.clock ?? { now: () => new Date() };\n this.logger = opts.logger ?? {};\n this.maxRows = Math.max(1, opts.maxRows ?? 5000);\n this.resolveOwnerContext = opts.resolveOwnerContext;\n this.canExportFn = opts.canExport;\n }\n\n /**\n * [#3544 / #3710] Gate a report rendering on the user-level export axis.\n *\n * Throws `EXPORT_NOT_PERMITTED` when the principal behind `context` may not\n * take a bulk copy of `object`. A no-op for non-bulk formats (`html_table`),\n * for a system context, and when no `canExport` is wired.\n *\n * Deliberately checked HERE — one place — rather than at each of the three\n * callers (`runReport`, the ad-hoc run, and the scheduled dispatch): a gate\n * per call site is how a fourth call site later ships ungated. `dispatchDue`\n * routes through `executeReport` too, so the scheduled CSV is covered by the\n * same line. (`scheduleReport` additionally pre-checks, so an author is\n * refused when they create the schedule rather than silently at 3am — but\n * that is UX, and THIS is the enforcement: a grant revoked after the schedule\n * was created must still stop the delivery.)\n *\n * Fails CLOSED on a throw — it resolves permission sets to decide, and a\n * resolution failure must never read as a grant (ADR-0049).\n */\n private async assertExportAllowed(\n object: string,\n format: string,\n context: SharingExecutionContext | undefined,\n ): Promise<void> {\n if (!BULK_EXPORT_FORMATS.has(format)) return;\n if (context?.isSystem) return;\n if (!this.canExportFn) return;\n let allowed: boolean;\n try {\n allowed = await this.canExportFn(object, context);\n } catch (err) {\n this.logger.warn?.('ReportService: canExport check failed — denying export', err);\n allowed = false;\n }\n if (!allowed) {\n throw new Error(\n `EXPORT_NOT_PERMITTED: exporting '${object}' as ${format} is not permitted for this user`,\n );\n }\n }\n\n // ── Access control ─────────────────────────────────────────────\n\n /**\n * Authorization for a saved-report row. `sys_saved_report` is a\n * protection-locked system object, so its rows are *read* with\n * `SYSTEM_CTX`; the caller's right to see/mutate a specific report is\n * enforced HERE, by owner match, not by the metadata read's own RLS —\n * otherwise any authenticated caller could read/delete/overwrite any\n * report by id (#2980). An explicit elevated context (`isSystem`) — the\n * scheduler / server tooling — sees everything.\n */\n private canAccessReport(row: { owner_id?: unknown } | null | undefined, context: SharingExecutionContext | undefined): boolean {\n if (!row) return false;\n if (context?.isSystem) return true;\n const userId = context?.userId;\n return !!userId && row.owner_id === userId;\n }\n\n /** Raw metadata read of a saved report by id (no authz — callers gate). */\n private async loadReportRow(reportId: string): Promise<any | null> {\n const rows = await this.engine.find('sys_saved_report', {\n where: { id: reportId }, limit: 1, context: SYSTEM_CTX,\n });\n return Array.isArray(rows) && rows[0] ? rows[0] : null;\n }\n\n // ── Report CRUD ────────────────────────────────────────────────\n\n async saveReport(input: SaveReportInput, context: SharingExecutionContext): Promise<SavedReport> {\n if (!input.name) throw new Error('VALIDATION_FAILED: name is required');\n if (!input.object) throw new Error('VALIDATION_FAILED: object is required');\n if (!input.query) throw new Error('VALIDATION_FAILED: query is required');\n\n const now = this.clock.now().toISOString();\n // A non-system caller always owns what they create — a caller-supplied\n // ownerId cannot assign the report to someone else (#2980). Only an\n // explicit elevated context (server tooling / import) may set it.\n const ownerId = context.isSystem ? (input.ownerId ?? context.userId ?? null) : (context.userId ?? null);\n const payload: any = {\n name: input.name,\n description: input.description ?? null,\n object_name: input.object,\n query_json: JSON.stringify(input.query ?? {}),\n format: input.format ?? DEFAULT_FORMAT,\n owner_id: ownerId,\n updated_at: now,\n };\n\n if (input.id) {\n const existing = await this.loadReportRow(input.id);\n if (existing) {\n // An update to an existing report is a mutation — a caller may only\n // overwrite a report they own (#2980). Not-found for others so the\n // response doesn't leak that the id exists.\n if (!this.canAccessReport(existing, context)) {\n throw new Error(`REPORT_NOT_FOUND: ${input.id}`);\n }\n // Never let a non-system caller reassign ownership away from the row.\n if (!context.isSystem) payload.owner_id = existing.owner_id ?? payload.owner_id;\n await this.engine.update('sys_saved_report', { id: input.id, ...payload }, { context: SYSTEM_CTX });\n return rowFromSaved({ ...existing, ...payload, id: input.id });\n }\n }\n\n const id = input.id ?? uid('rpt');\n const row = { id, ...payload, created_at: now };\n await this.engine.insert('sys_saved_report', row, { context: SYSTEM_CTX });\n return rowFromSaved(row);\n }\n\n async listReports(\n filter: { object?: string; ownerId?: string } | undefined,\n context: SharingExecutionContext,\n ): Promise<SavedReport[]> {\n const f: any = {};\n if (filter?.object) f.object_name = filter.object;\n // Owner scoping (#2980): a non-system caller sees ONLY their own reports —\n // a caller-supplied ownerId can never widen past their own id. A caller\n // with no identity sees nothing (fail closed). System/tooling sees all,\n // honouring an explicit ownerId narrow.\n if (context?.isSystem) {\n if (filter?.ownerId) f.owner_id = filter.ownerId;\n } else {\n if (!context?.userId) return [];\n if (filter?.ownerId && filter.ownerId !== context.userId) return [];\n f.owner_id = context.userId;\n }\n const rows = await this.engine.find('sys_saved_report', {\n where: f, limit: 500, orderBy: [{ field: 'updated_at', order: 'desc' }], context: SYSTEM_CTX,\n });\n return Array.isArray(rows) ? rows.map(rowFromSaved) : [];\n }\n\n async getReport(reportId: string, context: SharingExecutionContext): Promise<SavedReport | null> {\n const row = await this.loadReportRow(reportId);\n // Unauthorized reads are indistinguishable from a genuine miss (#2980).\n if (!this.canAccessReport(row, context)) return null;\n return rowFromSaved(row);\n }\n\n async deleteReport(reportId: string, context: SharingExecutionContext): Promise<void> {\n if (!reportId) throw new Error('VALIDATION_FAILED: reportId is required');\n const row = await this.loadReportRow(reportId);\n if (!row) return; // idempotent — nothing to drop\n // A caller may only delete a report they own (#2980); others get a\n // not-found so the delete neither fires nor reveals the report's existence.\n if (!this.canAccessReport(row, context)) {\n throw new Error(`REPORT_NOT_FOUND: ${reportId}`);\n }\n // Cascade — drop attached schedules first.\n const schedules = await this.engine.find('sys_report_schedule', {\n where: { report_id: reportId }, limit: 500, context: SYSTEM_CTX,\n });\n for (const s of (schedules ?? [])) {\n await this.engine.delete('sys_report_schedule', { where: { id: (s as any).id }, context: SYSTEM_CTX });\n }\n await this.engine.delete('sys_saved_report', { where: { id: reportId }, context: SYSTEM_CTX });\n }\n\n // ── Execution ───────────────────────────────────────────────────\n\n async run(reportId: string, context: SharingExecutionContext): Promise<ReportRunResult> {\n const report = await this.getReport(reportId, context);\n if (!report) throw new Error(`REPORT_NOT_FOUND: ${reportId}`);\n return this.executeReport(report, context);\n }\n\n async runAdHoc(input: SaveReportInput, context: SharingExecutionContext): Promise<ReportRunResult> {\n if (!input.object) throw new Error('VALIDATION_FAILED: object is required');\n if (!input.query) throw new Error('VALIDATION_FAILED: query is required');\n const adhoc: SavedReport = {\n id: '__adhoc__',\n name: input.name ?? 'Ad-hoc report',\n object_name: input.object,\n query: input.query,\n format: input.format ?? DEFAULT_FORMAT,\n };\n return this.executeReport(adhoc, context, /* stamp */ false);\n }\n\n private async executeReport(\n report: SavedReport,\n context: SharingExecutionContext,\n stamp = true,\n ): Promise<ReportRunResult> {\n // [#3544 / #3710] The export axis, BEFORE any row is read — a refusal must\n // not be reachable after the data has already been pulled.\n await this.assertExportAllowed(report.object_name, report.format, context);\n const q = report.query ?? {};\n const limit = Math.min(q.limit ?? DEFAULT_LIMIT, this.maxRows);\n const rows = await this.engine.find(report.object_name, {\n where: q.filter,\n fields: q.fields,\n orderBy: q.orderBy,\n limit,\n // Reports execute with the caller's identity so sharing rules\n // (if installed) apply. Falls back to system bypass only when\n // the report definition was created by a system writer.\n context: {\n userId: context.userId,\n tenantId: context.tenantId,\n positions: context.positions ?? [],\n permissions: context.permissions ?? [],\n isSystem: context.isSystem ?? false,\n },\n });\n const list = Array.isArray(rows) ? rows : [];\n const body = renderReport(list, report.format, q.fields);\n const ranAt = this.clock.now().toISOString();\n\n if (stamp && report.id !== '__adhoc__') {\n try {\n await this.engine.update('sys_saved_report', {\n id: report.id,\n last_run_at: ranAt,\n last_row_count: list.length,\n updated_at: ranAt,\n }, { context: SYSTEM_CTX });\n } catch (err) {\n this.logger.warn?.('ReportService: failed to stamp last_run_at', err);\n }\n }\n\n return {\n reportId: report.id,\n rowCount: list.length,\n format: report.format,\n body,\n rows: list,\n ranAt,\n };\n }\n\n // ── Schedules ──────────────────────────────────────────────────\n\n async scheduleReport(input: ScheduleReportInput, context: SharingExecutionContext): Promise<ReportSchedule> {\n if (!input.reportId) throw new Error('VALIDATION_FAILED: reportId is required');\n if (!input.recipients || input.recipients.length === 0) {\n throw new Error('VALIDATION_FAILED: recipients must be a non-empty array');\n }\n const report = await this.getReport(input.reportId, context);\n if (!report) throw new Error(`REPORT_NOT_FOUND: ${input.reportId}`);\n\n // [#3544 / #3710] Refuse a bulk-format schedule the author could not run\n // themselves, at CREATE time — otherwise the refusal only surfaces on the\n // first silent 3am sweep. Advisory only: `executeReport` re-checks on every\n // dispatch, which is what catches a grant revoked after this point.\n await this.assertExportAllowed(report.object_name, input.format ?? 'html_table', context);\n\n const now = this.clock.now();\n const interval = input.intervalMinutes ?? DEFAULT_INTERVAL_MIN;\n const cron = input.cronExpression?.trim() || null;\n if (cron) {\n // Validate eagerly so an author gets a clear error at schedule time\n // instead of a schedule that silently falls back to interval on sweep.\n try {\n new Cron(cron, { timezone: input.timezone || 'UTC' });\n } catch (err) {\n throw new Error(`VALIDATION_FAILED: invalid cron_expression '${cron}': ${(err as Error).message}`);\n }\n }\n const nextRun = this.nextRunAt(\n { cron_expression: cron, interval_minutes: interval, timezone: input.timezone ?? 'UTC' },\n now,\n ).toISOString();\n const id = uid('rsch');\n const row: any = {\n id,\n report_id: input.reportId,\n name: input.name ?? null,\n interval_minutes: interval,\n cron_expression: cron,\n timezone: input.timezone ?? 'UTC',\n active: input.active !== false,\n recipients: input.recipients.join(','),\n format: input.format ?? 'html_table',\n subject_template: input.subjectTemplate ?? null,\n owner_id: input.ownerId ?? context.userId ?? null,\n next_run_at: nextRun,\n created_at: now.toISOString(),\n updated_at: now.toISOString(),\n };\n await this.engine.insert('sys_report_schedule', row, { context: SYSTEM_CTX });\n return rowFromSchedule(row);\n }\n\n async unscheduleReport(scheduleId: string, _context: SharingExecutionContext): Promise<void> {\n if (!scheduleId) throw new Error('VALIDATION_FAILED: scheduleId is required');\n await this.engine.delete('sys_report_schedule', { where: { id: scheduleId }, context: SYSTEM_CTX });\n }\n\n async listSchedules(\n filter: { reportId?: string } | undefined,\n _context: SharingExecutionContext,\n ): Promise<ReportSchedule[]> {\n const f: any = {};\n if (filter?.reportId) f.report_id = filter.reportId;\n const rows = await this.engine.find('sys_report_schedule', {\n where: f, limit: 500, orderBy: [{ field: 'next_run_at', order: 'asc' }], context: SYSTEM_CTX,\n });\n return Array.isArray(rows) ? rows.map(rowFromSchedule) : [];\n }\n\n // ── Dispatcher ─────────────────────────────────────────────────\n\n async dispatchDue(now?: Date): Promise<{ fired: number; failed: number; skipped: number }> {\n const ts = (now ?? this.clock.now()).toISOString();\n const due = await this.engine.find('sys_report_schedule', {\n where: { active: true },\n limit: 200,\n context: SYSTEM_CTX,\n });\n const list = (Array.isArray(due) ? due : []).map(rowFromSchedule)\n .filter(s => !s.next_run_at || s.next_run_at <= ts);\n\n let fired = 0, failed = 0, skipped = 0;\n for (const schedule of list) {\n try {\n const row = await this.loadReportRow(schedule.report_id);\n if (!row) {\n skipped++;\n await this.markSchedule(schedule.id, {\n last_status: 'skipped',\n last_error: `report ${schedule.report_id} missing`,\n });\n continue;\n }\n const report = rowFromSaved(row);\n\n // Run the report under the OWNER's authority, not system (#2980).\n // A scheduled run must not read rows the report's owner cannot see —\n // that was a silent RLS bypass (a member's scheduled report emailed\n // the target object's entire table). Resolve the owner to a real\n // RLS-bearing context; if we can't (no resolver wired, or unknown/\n // disabled owner), FAIL CLOSED rather than run elevated.\n const ownerId = report.owner_id;\n const runContext = ownerId && this.resolveOwnerContext\n ? await this.resolveOwnerContext(ownerId).catch((err) => {\n this.logger.warn?.('ReportService.dispatchDue: owner context resolution failed', err);\n return null;\n })\n : null;\n if (!runContext) {\n failed++;\n await this.markSchedule(schedule.id, {\n last_status: 'failed',\n last_error: ownerId\n ? `owner '${ownerId}' context unavailable — refusing to run scheduled report with RLS bypassed (#2849/#2980)`\n : 'report has no owner — refusing to run scheduled report with RLS bypassed (#2849/#2980)',\n });\n continue;\n }\n\n // Force the schedule's own format so the recipient gets what\n // the admin configured (CSV attachment vs inline HTML table).\n const fmt: ReportFormat = (schedule.format ?? 'html_table') as ReportFormat;\n const result = await this.executeReport({ ...report, format: fmt }, runContext, false);\n\n const recipients = schedule.recipients.split(',').map(s => s.trim()).filter(Boolean);\n const subject = renderSubject(schedule.subject_template, {\n name: schedule.name ?? report.name,\n date: ts.slice(0, 10),\n rows: String(result.rowCount),\n });\n\n if (this.email && recipients.length > 0) {\n if (fmt === 'csv') {\n await this.email.send({\n to: recipients,\n subject,\n text: `Attached: ${result.rowCount} row(s).`,\n attachments: [{\n // Keep unicode letters (CJK schedule names) — only strip\n // filesystem-hostile characters, else 周报 becomes `__`.\n filename: `${(schedule.name ?? report.name).replace(/[^\\p{L}\\p{N}._-]+/gu, '_').replace(/^_+|_+$/g, '') || 'report'}-${ts.slice(0, 10)}.csv`,\n content: result.body,\n contentType: 'text/csv',\n }],\n relatedObject: 'sys_report_schedule',\n relatedId: schedule.id,\n });\n } else {\n await this.email.send({\n to: recipients,\n subject,\n html: `<p>${escapeHtml(report.name)} — ${result.rowCount} row(s)</p>${result.body}`,\n text: `${report.name} — ${result.rowCount} row(s)`,\n relatedObject: 'sys_report_schedule',\n relatedId: schedule.id,\n });\n }\n } else if (!this.email) {\n this.logger.warn?.('ReportService.dispatchDue: no email service — schedule fired but mail not sent');\n }\n\n await this.advanceSchedule(schedule, ts);\n fired++;\n } catch (err: any) {\n failed++;\n await this.markSchedule(schedule.id, {\n last_status: 'failed',\n last_error: String(err?.message ?? err ?? 'unknown').slice(0, 500),\n });\n this.logger.error?.('ReportService.dispatchDue: schedule failed', err);\n }\n }\n return { fired, failed, skipped };\n }\n\n /**\n * Compute the next fire time for a schedule. A `cron_expression` wins over\n * `interval_minutes` (the documented `sys_report_schedule` contract) and is\n * evaluated in the schedule's `timezone` (default UTC) via croner — the same\n * library the job scheduler uses. Falls back to `from + interval_minutes` for\n * interval schedules, and also if a cron expression is invalid or has no\n * future occurrence (logged; never throws into the sweep). `from` is the\n * reference instant (the injected clock), so `today()`-style boundaries honor\n * the test clock.\n */\n private nextRunAt(\n schedule: { cron_expression?: string | null; interval_minutes?: number | null; timezone?: string | null },\n from: Date,\n ): Date {\n const cron = (schedule.cron_expression ?? '').trim();\n if (cron) {\n try {\n const next = new Cron(cron, { timezone: schedule.timezone || 'UTC' }).nextRun(from);\n if (next) return next;\n this.logger.warn?.(`ReportService: cron '${cron}' has no next occurrence; falling back to interval`);\n } catch (err) {\n this.logger.warn?.(`ReportService: invalid cron '${cron}'; falling back to interval`, err);\n }\n }\n const interval = schedule.interval_minutes ?? DEFAULT_INTERVAL_MIN;\n return new Date(from.getTime() + interval * 60_000);\n }\n\n private async advanceSchedule(schedule: ReportSchedule, ranAt: string): Promise<void> {\n const nextRun = this.nextRunAt(schedule, this.clock.now()).toISOString();\n await this.engine.update('sys_report_schedule', {\n id: schedule.id,\n next_run_at: nextRun,\n last_sent_at: ranAt,\n last_status: 'ok',\n last_error: null,\n updated_at: ranAt,\n }, { context: SYSTEM_CTX });\n }\n\n private async markSchedule(id: string, patch: Record<string, unknown>): Promise<void> {\n try {\n await this.engine.update('sys_report_schedule', {\n id, ...patch, updated_at: this.clock.now().toISOString(),\n }, { context: SYSTEM_CTX });\n } catch (err) {\n this.logger.warn?.('ReportService: failed to mark schedule', err);\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport {\n SysSavedReport,\n SysReportSchedule,\n} from '@objectstack/platform-objects/audit';\nimport { ReportService, type ReportEngine, type ReportEmail } from './report-service.js';\n\nexport interface ReportsPluginOptions {\n /**\n * How often the dispatcher should poll `sys_report_schedule` for\n * due rows. Defaults to 60 seconds — short enough to honour\n * minute-grained schedules without flooding the DB.\n */\n dispatchIntervalMs?: number;\n /** Cap rows per report. Mirrors ReportServiceOptions.maxRows. */\n maxRows?: number;\n /** Disable the dispatcher tick entirely. */\n disableDispatcher?: boolean;\n}\n\n/**\n * ReportsServicePlugin — registers `sys_saved_report` /\n * `sys_report_schedule`, the `reports` service, and the dispatcher\n * loop that emails due schedules.\n *\n * The dispatcher uses `IJobService.schedule` when one is registered;\n * otherwise it falls back to a plain `setInterval` so single-kernel\n * deployments work without `service-job`.\n *\n * @example\n * ```ts\n * import { ReportsServicePlugin } from '@objectstack/plugin-reports';\n *\n * kernel.use(new ReportsServicePlugin({ dispatchIntervalMs: 60_000 }));\n * ```\n */\nexport class ReportsServicePlugin implements Plugin {\n name = 'com.objectstack.service.reports';\n version = '1.0.0';\n type = 'standard';\n dependencies = ['com.objectstack.engine.objectql'];\n\n private readonly options: ReportsPluginOptions;\n private service?: ReportService;\n private intervalHandle?: ReturnType<typeof setInterval>;\n private jobName?: string;\n private jobService?: any;\n\n constructor(options: ReportsPluginOptions = {}) {\n this.options = options;\n }\n\n async init(ctx: PluginContext): Promise<void> {\n ctx.getService<{ register(m: any): void }>('manifest').register({\n id: 'com.objectstack.service.reports',\n name: 'Reports Service',\n version: '1.0.0',\n type: 'plugin',\n scope: 'system',\n defaultDatasource: 'cloud',\n namespace: 'sys',\n objects: [SysSavedReport, SysReportSchedule],\n });\n ctx.logger.info('ReportsServicePlugin: schemas registered');\n }\n\n async start(ctx: PluginContext): Promise<void> {\n ctx.hook('kernel:ready', async () => {\n let engine: any = null;\n try { engine = ctx.getService<any>('objectql'); }\n catch { try { engine = ctx.getService<any>('data'); } catch { /* ignore */ } }\n if (!engine) {\n ctx.logger.warn('ReportsServicePlugin: no ObjectQL engine — service NOT registered');\n return;\n }\n\n let email: ReportEmail | undefined;\n try { email = ctx.getService<any>('email'); } catch { /* email is optional */ }\n if (!email) {\n ctx.logger.warn('ReportsServicePlugin: no email service — schedules will fire without delivery');\n }\n\n // [#3544 / #3710] The user-level export axis. A `csv`/`json` report is a\n // bulk machine-readable copy of its object — the same privilege\n // `GET /data/:object/export` gates — so the reports surface must ask the\n // SAME question of the SAME authority, or it is a side door around the\n // axis. Resolved lazily per call rather than captured here: `security` is\n // registered by plugin-security's own `kernel:ready` hook and may not\n // exist yet at construction time. Absent service (no plugin-security ⇒ no\n // permission sets anywhere) → the axis does not apply, matching the REST\n // export route's fail-open.\n const canExport = async (object: string, context: unknown): Promise<boolean> => {\n let security: any;\n try { security = ctx.getService<any>('security'); } catch { return true; }\n if (!security || typeof security.canExport !== 'function') return true;\n return await security.canExport(object, context);\n };\n\n this.service = new ReportService({\n engine: engine as ReportEngine,\n email,\n logger: ctx.logger,\n maxRows: this.options.maxRows,\n canExport,\n // Scheduled reports run under the owner's resolved RLS context, not a\n // system bypass (#2980). No owner-context resolver is wired yet — that\n // is the reports-surface consumer of ADR-0073's user-less identity\n // resolution (M2) — so until it lands, scheduled runs FAIL CLOSED\n // (skipped + marked failed) rather than exfiltrate. Interactive runs\n // (run/runAdHoc) are unaffected: they carry the caller's context.\n resolveOwnerContext: undefined,\n });\n ctx.registerService('reports', this.service);\n\n if (this.options.disableDispatcher) {\n ctx.logger.info('ReportsServicePlugin: dispatcher disabled (disableDispatcher=true)');\n return;\n }\n\n const intervalMs = Math.max(5_000, this.options.dispatchIntervalMs ?? 60_000);\n\n // Prefer the platform job service when available — it lets ops\n // see report dispatch alongside every other scheduled job.\n try {\n const job = ctx.getService<any>('job');\n if (job && typeof job.schedule === 'function') {\n this.jobService = job;\n this.jobName = 'reports.dispatch';\n await job.schedule(this.jobName, { type: 'interval', intervalMs }, async () => {\n try { await this.service?.dispatchDue(); }\n catch (err) { ctx.logger.warn('ReportsServicePlugin: dispatch tick failed', err as any); }\n });\n ctx.logger.info('ReportsServicePlugin: dispatcher registered with job service', { intervalMs });\n return;\n }\n } catch { /* fall through to setInterval */ }\n\n this.intervalHandle = setInterval(() => {\n this.service?.dispatchDue().catch(err => {\n ctx.logger.warn('ReportsServicePlugin: dispatch tick failed', err);\n });\n }, intervalMs);\n // Don't keep Node alive purely for the dispatcher — common\n // mistake in tests / serverless. unref is a no-op in some\n // runtimes which is fine.\n (this.intervalHandle as any)?.unref?.();\n ctx.logger.info('ReportsServicePlugin: dispatcher registered (setInterval fallback)', { intervalMs });\n });\n }\n\n async stop(ctx: PluginContext): Promise<void> {\n if (this.intervalHandle) clearInterval(this.intervalHandle);\n this.intervalHandle = undefined;\n if (this.jobService && this.jobName && typeof this.jobService.cancel === 'function') {\n try { await this.jobService.cancel(this.jobName); }\n catch (err) { ctx.logger.warn('ReportsServicePlugin: failed to cancel job', err as any); }\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAWA,IAAAA,gBAAkD;;;ACElD,oBAAqB;AAiCrB,IAAM,aAAa,EAAE,UAAU,MAAM,WAAW,CAAC,GAAG,aAAa,CAAC,EAAE;AAEpE,IAAM,iBAA+B;AACrC,IAAM,uBAAuB;AAC7B,IAAM,gBAAgB;AAEtB,SAAS,IAAI,QAAwB;AACnC,QAAM,IAAS;AACf,MAAI,EAAE,QAAQ,WAAY,QAAO,GAAG,MAAM,IAAI,EAAE,OAAO,WAAW,CAAC;AACnE,SAAO,GAAG,MAAM,IAAI,KAAK,IAAI,EAAE,SAAS,EAAE,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC;AACxF;AAEA,SAAS,WAAW,KAA2B;AAC7C,MAAI,CAAC,IAAK,QAAO,CAAC;AAClB,MAAI,OAAO,QAAQ,UAAU;AAC3B,QAAI;AAAE,aAAO,KAAK,MAAM,GAAG;AAAA,IAAkB,QACvC;AAAE,aAAO,CAAC;AAAA,IAAG;AAAA,EACrB;AACA,MAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,SAAO,CAAC;AACV;AAEA,SAAS,aAAa,KAAuB;AAC3C,SAAO;AAAA,IACL,IAAI,OAAO,IAAI,EAAE;AAAA,IACjB,MAAM,OAAO,IAAI,QAAQ,EAAE;AAAA,IAC3B,aAAa,IAAI,eAAe;AAAA,IAChC,aAAa,OAAO,IAAI,eAAe,EAAE;AAAA,IACzC,OAAO,WAAW,IAAI,UAAU;AAAA,IAChC,QAAS,IAAI,UAA2B;AAAA,IACxC,UAAU,IAAI,YAAY;AAAA,IAC1B,aAAa,IAAI,eAAe;AAAA,IAChC,gBAAgB,IAAI,kBAAkB;AAAA,IACtC,YAAY,IAAI,cAAc;AAAA,IAC9B,YAAY,IAAI,cAAc;AAAA,EAChC;AACF;AAEA,SAAS,gBAAgB,KAA0B;AACjD,SAAO;AAAA,IACL,IAAI,OAAO,IAAI,EAAE;AAAA,IACjB,WAAW,OAAO,IAAI,SAAS;AAAA,IAC/B,MAAM,IAAI,QAAQ;AAAA,IAClB,kBAAkB,IAAI,oBAAoB;AAAA,IAC1C,iBAAiB,IAAI,mBAAmB;AAAA,IACxC,UAAU,IAAI,YAAY;AAAA,IAC1B,QAAQ,IAAI,WAAW;AAAA,IACvB,YAAY,OAAO,IAAI,cAAc,EAAE;AAAA,IACvC,QAAQ,IAAI,UAAU;AAAA,IACtB,kBAAkB,IAAI,oBAAoB;AAAA,IAC1C,UAAU,IAAI,YAAY;AAAA,IAC1B,aAAa,IAAI,eAAe;AAAA,IAChC,cAAc,IAAI,gBAAgB;AAAA,IAClC,aAAa,IAAI,eAAe;AAAA,IAChC,YAAY,IAAI,cAAc;AAAA,EAChC;AACF;AAIA,SAAS,cAAc,GAAoB;AACzC,MAAI,KAAK,KAAM,QAAO;AACtB,QAAM,IAAI,OAAO,MAAM,WAAW,IAAK,OAAO,MAAM,WAAW,KAAK,UAAU,CAAC,IAAI,OAAO,CAAC;AAC3F,MAAI,WAAW,KAAK,CAAC,EAAG,QAAO,IAAI,EAAE,QAAQ,MAAM,IAAI,CAAC;AACxD,SAAO;AACT;AAEA,SAAS,WAAW,MAAa,UAA+B;AAC9D,MAAI,YAAY,SAAS,SAAS,EAAG,QAAO;AAC5C,QAAM,OAAO,oBAAI,IAAY;AAC7B,aAAW,KAAK,KAAK,MAAM,GAAG,EAAE,GAAG;AACjC,QAAI,KAAK,OAAO,MAAM,SAAU,YAAW,KAAK,OAAO,KAAK,CAAC,EAAG,MAAK,IAAI,CAAC;AAAA,EAC5E;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;AAEA,SAAS,UAAU,MAAa,QAA2B;AACzD,QAAM,OAAO,WAAW,MAAM,MAAM;AACpC,QAAM,OAAO,KAAK,KAAK,GAAG;AAC1B,QAAM,OAAO,KAAK,IAAI,OAAK,KAAK,IAAI,OAAK,cAAc,IAAI,CAAC,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,KAAK,MAAM;AACtF,SAAO,KAAK,SAAS,IAAI,GAAG,IAAI;AAAA,EAAO,IAAI,KAAK;AAClD;AAEA,SAAS,WAAW,MAAqB;AACvC,SAAO,KAAK,UAAU,MAAM,MAAM,CAAC;AACrC;AAEA,SAAS,WAAW,GAAmB;AACrC,SAAO,EAAE,QAAQ,YAAY,QAAM;AAAA,IACjC,KAAK;AAAA,IAAS,KAAK;AAAA,IAAQ,KAAK;AAAA,IAAQ,KAAK;AAAA,IAAU,KAAK;AAAA,EAC9D,GAA6B,CAAC,CAAC;AACjC;AAEA,SAAS,gBAAgB,MAAa,QAA2B;AAC/D,QAAM,OAAO,WAAW,MAAM,MAAM;AACpC,QAAM,KAAK,KAAK,IAAI,OAAK,6EAA6E,WAAW,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE;AACnI,QAAM,MAAM,KAAK,IAAI,OAAK;AACxB,UAAM,MAAM,KAAK,IAAI,OAAK;AACxB,YAAM,IAAI,IAAI,CAAC;AACf,YAAM,IAAI,KAAK,OAAO,KAAM,OAAO,MAAM,WAAW,IAAK,OAAO,MAAM,WAAW,KAAK,UAAU,CAAC,IAAI,OAAO,CAAC;AAC7G,aAAO,6DAA6D,WAAW,CAAC,CAAC;AAAA,IACnF,CAAC,EAAE,KAAK,EAAE;AACV,WAAO,OAAO,GAAG;AAAA,EACnB,CAAC,EAAE,KAAK,EAAE;AACV,SAAO,6GACW,EAAE,uBAAuB,GAAG;AAChD;AAEO,SAAS,aAAa,MAAa,QAAsB,QAA2B;AACzF,UAAQ,QAAQ;AAAA,IACd,KAAK;AAAQ,aAAO,WAAW,IAAI;AAAA,IACnC,KAAK;AAAc,aAAO,gBAAgB,MAAM,MAAM;AAAA,IACtD,KAAK;AAAA,IACL;AAAS,aAAO,UAAU,MAAM,MAAM;AAAA,EACxC;AACF;AAIA,SAAS,cAAc,UAA8B,MAAsC;AACzF,QAAM,MAAM,YAAY;AACxB,SAAO,IAAI,QAAQ,wBAAwB,CAAC,IAAI,MAAM,KAAK,OAAO,CAAC,CAAC,KAAK,EAAE;AAC7E;AA0DA,IAAM,sBAA2C,oBAAI,IAAI,CAAC,OAAO,MAAM,CAAC;AAEjE,IAAM,gBAAN,MAA8C;AAAA,EASnD,YAAY,MAA4B;AACtC,SAAK,SAAS,KAAK;AACnB,SAAK,QAAQ,KAAK;AAClB,SAAK,QAAQ,KAAK,SAAS,EAAE,KAAK,MAAM,oBAAI,KAAK,EAAE;AACnD,SAAK,SAAS,KAAK,UAAU,CAAC;AAC9B,SAAK,UAAU,KAAK,IAAI,GAAG,KAAK,WAAW,GAAI;AAC/C,SAAK,sBAAsB,KAAK;AAChC,SAAK,cAAc,KAAK;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAqBA,MAAc,oBACZ,QACA,QACA,SACe;AACf,QAAI,CAAC,oBAAoB,IAAI,MAAM,EAAG;AACtC,QAAI,SAAS,SAAU;AACvB,QAAI,CAAC,KAAK,YAAa;AACvB,QAAI;AACJ,QAAI;AACF,gBAAU,MAAM,KAAK,YAAY,QAAQ,OAAO;AAAA,IAClD,SAAS,KAAK;AACZ,WAAK,OAAO,OAAO,+DAA0D,GAAG;AAChF,gBAAU;AAAA,IACZ;AACA,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI;AAAA,QACR,oCAAoC,MAAM,QAAQ,MAAM;AAAA,MAC1D;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaQ,gBAAgB,KAAgD,SAAuD;AAC7H,QAAI,CAAC,IAAK,QAAO;AACjB,QAAI,SAAS,SAAU,QAAO;AAC9B,UAAM,SAAS,SAAS;AACxB,WAAO,CAAC,CAAC,UAAU,IAAI,aAAa;AAAA,EACtC;AAAA;AAAA,EAGA,MAAc,cAAc,UAAuC;AACjE,UAAM,OAAO,MAAM,KAAK,OAAO,KAAK,oBAAoB;AAAA,MACtD,OAAO,EAAE,IAAI,SAAS;AAAA,MAAG,OAAO;AAAA,MAAG,SAAS;AAAA,IAC9C,CAAC;AACD,WAAO,MAAM,QAAQ,IAAI,KAAK,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI;AAAA,EACpD;AAAA;AAAA,EAIA,MAAM,WAAW,OAAwB,SAAwD;AAC/F,QAAI,CAAC,MAAM,KAAM,OAAM,IAAI,MAAM,qCAAqC;AACtE,QAAI,CAAC,MAAM,OAAQ,OAAM,IAAI,MAAM,uCAAuC;AAC1E,QAAI,CAAC,MAAM,MAAO,OAAM,IAAI,MAAM,sCAAsC;AAExE,UAAM,MAAM,KAAK,MAAM,IAAI,EAAE,YAAY;AAIzC,UAAM,UAAU,QAAQ,WAAY,MAAM,WAAW,QAAQ,UAAU,OAAS,QAAQ,UAAU;AAClG,UAAM,UAAe;AAAA,MACnB,MAAM,MAAM;AAAA,MACZ,aAAa,MAAM,eAAe;AAAA,MAClC,aAAa,MAAM;AAAA,MACnB,YAAY,KAAK,UAAU,MAAM,SAAS,CAAC,CAAC;AAAA,MAC5C,QAAQ,MAAM,UAAU;AAAA,MACxB,UAAU;AAAA,MACV,YAAY;AAAA,IACd;AAEA,QAAI,MAAM,IAAI;AACZ,YAAM,WAAW,MAAM,KAAK,cAAc,MAAM,EAAE;AAClD,UAAI,UAAU;AAIZ,YAAI,CAAC,KAAK,gBAAgB,UAAU,OAAO,GAAG;AAC5C,gBAAM,IAAI,MAAM,qBAAqB,MAAM,EAAE,EAAE;AAAA,QACjD;AAEA,YAAI,CAAC,QAAQ,SAAU,SAAQ,WAAW,SAAS,YAAY,QAAQ;AACvE,cAAM,KAAK,OAAO,OAAO,oBAAoB,EAAE,IAAI,MAAM,IAAI,GAAG,QAAQ,GAAG,EAAE,SAAS,WAAW,CAAC;AAClG,eAAO,aAAa,EAAE,GAAG,UAAU,GAAG,SAAS,IAAI,MAAM,GAAG,CAAC;AAAA,MAC/D;AAAA,IACF;AAEA,UAAM,KAAK,MAAM,MAAM,IAAI,KAAK;AAChC,UAAM,MAAM,EAAE,IAAI,GAAG,SAAS,YAAY,IAAI;AAC9C,UAAM,KAAK,OAAO,OAAO,oBAAoB,KAAK,EAAE,SAAS,WAAW,CAAC;AACzE,WAAO,aAAa,GAAG;AAAA,EACzB;AAAA,EAEA,MAAM,YACJ,QACA,SACwB;AACxB,UAAM,IAAS,CAAC;AAChB,QAAI,QAAQ,OAAQ,GAAE,cAAc,OAAO;AAK3C,QAAI,SAAS,UAAU;AACrB,UAAI,QAAQ,QAAS,GAAE,WAAW,OAAO;AAAA,IAC3C,OAAO;AACL,UAAI,CAAC,SAAS,OAAQ,QAAO,CAAC;AAC9B,UAAI,QAAQ,WAAW,OAAO,YAAY,QAAQ,OAAQ,QAAO,CAAC;AAClE,QAAE,WAAW,QAAQ;AAAA,IACvB;AACA,UAAM,OAAO,MAAM,KAAK,OAAO,KAAK,oBAAoB;AAAA,MACtD,OAAO;AAAA,MAAG,OAAO;AAAA,MAAK,SAAS,CAAC,EAAE,OAAO,cAAc,OAAO,OAAO,CAAC;AAAA,MAAG,SAAS;AAAA,IACpF,CAAC;AACD,WAAO,MAAM,QAAQ,IAAI,IAAI,KAAK,IAAI,YAAY,IAAI,CAAC;AAAA,EACzD;AAAA,EAEA,MAAM,UAAU,UAAkB,SAA+D;AAC/F,UAAM,MAAM,MAAM,KAAK,cAAc,QAAQ;AAE7C,QAAI,CAAC,KAAK,gBAAgB,KAAK,OAAO,EAAG,QAAO;AAChD,WAAO,aAAa,GAAG;AAAA,EACzB;AAAA,EAEA,MAAM,aAAa,UAAkB,SAAiD;AACpF,QAAI,CAAC,SAAU,OAAM,IAAI,MAAM,yCAAyC;AACxE,UAAM,MAAM,MAAM,KAAK,cAAc,QAAQ;AAC7C,QAAI,CAAC,IAAK;AAGV,QAAI,CAAC,KAAK,gBAAgB,KAAK,OAAO,GAAG;AACvC,YAAM,IAAI,MAAM,qBAAqB,QAAQ,EAAE;AAAA,IACjD;AAEA,UAAM,YAAY,MAAM,KAAK,OAAO,KAAK,uBAAuB;AAAA,MAC9D,OAAO,EAAE,WAAW,SAAS;AAAA,MAAG,OAAO;AAAA,MAAK,SAAS;AAAA,IACvD,CAAC;AACD,eAAW,KAAM,aAAa,CAAC,GAAI;AACjC,YAAM,KAAK,OAAO,OAAO,uBAAuB,EAAE,OAAO,EAAE,IAAK,EAAU,GAAG,GAAG,SAAS,WAAW,CAAC;AAAA,IACvG;AACA,UAAM,KAAK,OAAO,OAAO,oBAAoB,EAAE,OAAO,EAAE,IAAI,SAAS,GAAG,SAAS,WAAW,CAAC;AAAA,EAC/F;AAAA;AAAA,EAIA,MAAM,IAAI,UAAkB,SAA4D;AACtF,UAAM,SAAS,MAAM,KAAK,UAAU,UAAU,OAAO;AACrD,QAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,qBAAqB,QAAQ,EAAE;AAC5D,WAAO,KAAK,cAAc,QAAQ,OAAO;AAAA,EAC3C;AAAA,EAEA,MAAM,SAAS,OAAwB,SAA4D;AACjG,QAAI,CAAC,MAAM,OAAQ,OAAM,IAAI,MAAM,uCAAuC;AAC1E,QAAI,CAAC,MAAM,MAAO,OAAM,IAAI,MAAM,sCAAsC;AACxE,UAAM,QAAqB;AAAA,MACzB,IAAI;AAAA,MACJ,MAAM,MAAM,QAAQ;AAAA,MACpB,aAAa,MAAM;AAAA,MACnB,OAAO,MAAM;AAAA,MACb,QAAQ,MAAM,UAAU;AAAA,IAC1B;AACA,WAAO,KAAK;AAAA,MAAc;AAAA,MAAO;AAAA;AAAA,MAAqB;AAAA,IAAK;AAAA,EAC7D;AAAA,EAEA,MAAc,cACZ,QACA,SACA,QAAQ,MACkB;AAG1B,UAAM,KAAK,oBAAoB,OAAO,aAAa,OAAO,QAAQ,OAAO;AACzE,UAAM,IAAI,OAAO,SAAS,CAAC;AAC3B,UAAM,QAAQ,KAAK,IAAI,EAAE,SAAS,eAAe,KAAK,OAAO;AAC7D,UAAM,OAAO,MAAM,KAAK,OAAO,KAAK,OAAO,aAAa;AAAA,MACtD,OAAO,EAAE;AAAA,MACT,QAAQ,EAAE;AAAA,MACV,SAAS,EAAE;AAAA,MACX;AAAA;AAAA;AAAA;AAAA,MAIA,SAAS;AAAA,QACP,QAAQ,QAAQ;AAAA,QAChB,UAAU,QAAQ;AAAA,QAClB,WAAW,QAAQ,aAAa,CAAC;AAAA,QACjC,aAAa,QAAQ,eAAe,CAAC;AAAA,QACrC,UAAU,QAAQ,YAAY;AAAA,MAChC;AAAA,IACF,CAAC;AACD,UAAM,OAAO,MAAM,QAAQ,IAAI,IAAI,OAAO,CAAC;AAC3C,UAAM,OAAO,aAAa,MAAM,OAAO,QAAQ,EAAE,MAAM;AACvD,UAAM,QAAQ,KAAK,MAAM,IAAI,EAAE,YAAY;AAE3C,QAAI,SAAS,OAAO,OAAO,aAAa;AACtC,UAAI;AACF,cAAM,KAAK,OAAO,OAAO,oBAAoB;AAAA,UAC3C,IAAI,OAAO;AAAA,UACX,aAAa;AAAA,UACb,gBAAgB,KAAK;AAAA,UACrB,YAAY;AAAA,QACd,GAAG,EAAE,SAAS,WAAW,CAAC;AAAA,MAC5B,SAAS,KAAK;AACZ,aAAK,OAAO,OAAO,8CAA8C,GAAG;AAAA,MACtE;AAAA,IACF;AAEA,WAAO;AAAA,MACL,UAAU,OAAO;AAAA,MACjB,UAAU,KAAK;AAAA,MACf,QAAQ,OAAO;AAAA,MACf;AAAA,MACA,MAAM;AAAA,MACN;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAIA,MAAM,eAAe,OAA4B,SAA2D;AAC1G,QAAI,CAAC,MAAM,SAAU,OAAM,IAAI,MAAM,yCAAyC;AAC9E,QAAI,CAAC,MAAM,cAAc,MAAM,WAAW,WAAW,GAAG;AACtD,YAAM,IAAI,MAAM,yDAAyD;AAAA,IAC3E;AACA,UAAM,SAAS,MAAM,KAAK,UAAU,MAAM,UAAU,OAAO;AAC3D,QAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,qBAAqB,MAAM,QAAQ,EAAE;AAMlE,UAAM,KAAK,oBAAoB,OAAO,aAAa,MAAM,UAAU,cAAc,OAAO;AAExF,UAAM,MAAM,KAAK,MAAM,IAAI;AAC3B,UAAM,WAAW,MAAM,mBAAmB;AAC1C,UAAM,OAAO,MAAM,gBAAgB,KAAK,KAAK;AAC7C,QAAI,MAAM;AAGR,UAAI;AACF,YAAI,mBAAK,MAAM,EAAE,UAAU,MAAM,YAAY,MAAM,CAAC;AAAA,MACtD,SAAS,KAAK;AACZ,cAAM,IAAI,MAAM,+CAA+C,IAAI,MAAO,IAAc,OAAO,EAAE;AAAA,MACnG;AAAA,IACF;AACA,UAAM,UAAU,KAAK;AAAA,MACnB,EAAE,iBAAiB,MAAM,kBAAkB,UAAU,UAAU,MAAM,YAAY,MAAM;AAAA,MACvF;AAAA,IACF,EAAE,YAAY;AACd,UAAM,KAAK,IAAI,MAAM;AACrB,UAAM,MAAW;AAAA,MACf;AAAA,MACA,WAAW,MAAM;AAAA,MACjB,MAAM,MAAM,QAAQ;AAAA,MACpB,kBAAkB;AAAA,MAClB,iBAAiB;AAAA,MACjB,UAAU,MAAM,YAAY;AAAA,MAC5B,QAAQ,MAAM,WAAW;AAAA,MACzB,YAAY,MAAM,WAAW,KAAK,GAAG;AAAA,MACrC,QAAQ,MAAM,UAAU;AAAA,MACxB,kBAAkB,MAAM,mBAAmB;AAAA,MAC3C,UAAU,MAAM,WAAW,QAAQ,UAAU;AAAA,MAC7C,aAAa;AAAA,MACb,YAAY,IAAI,YAAY;AAAA,MAC5B,YAAY,IAAI,YAAY;AAAA,IAC9B;AACA,UAAM,KAAK,OAAO,OAAO,uBAAuB,KAAK,EAAE,SAAS,WAAW,CAAC;AAC5E,WAAO,gBAAgB,GAAG;AAAA,EAC5B;AAAA,EAEA,MAAM,iBAAiB,YAAoB,UAAkD;AAC3F,QAAI,CAAC,WAAY,OAAM,IAAI,MAAM,2CAA2C;AAC5E,UAAM,KAAK,OAAO,OAAO,uBAAuB,EAAE,OAAO,EAAE,IAAI,WAAW,GAAG,SAAS,WAAW,CAAC;AAAA,EACpG;AAAA,EAEA,MAAM,cACJ,QACA,UAC2B;AAC3B,UAAM,IAAS,CAAC;AAChB,QAAI,QAAQ,SAAU,GAAE,YAAY,OAAO;AAC3C,UAAM,OAAO,MAAM,KAAK,OAAO,KAAK,uBAAuB;AAAA,MACzD,OAAO;AAAA,MAAG,OAAO;AAAA,MAAK,SAAS,CAAC,EAAE,OAAO,eAAe,OAAO,MAAM,CAAC;AAAA,MAAG,SAAS;AAAA,IACpF,CAAC;AACD,WAAO,MAAM,QAAQ,IAAI,IAAI,KAAK,IAAI,eAAe,IAAI,CAAC;AAAA,EAC5D;AAAA;AAAA,EAIA,MAAM,YAAY,KAAyE;AACzF,UAAM,MAAM,OAAO,KAAK,MAAM,IAAI,GAAG,YAAY;AACjD,UAAM,MAAM,MAAM,KAAK,OAAO,KAAK,uBAAuB;AAAA,MACxD,OAAO,EAAE,QAAQ,KAAK;AAAA,MACtB,OAAO;AAAA,MACP,SAAS;AAAA,IACX,CAAC;AACD,UAAM,QAAQ,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAC,GAAG,IAAI,eAAe,EAC7D,OAAO,OAAK,CAAC,EAAE,eAAe,EAAE,eAAe,EAAE;AAEpD,QAAI,QAAQ,GAAG,SAAS,GAAG,UAAU;AACrC,eAAW,YAAY,MAAM;AAC3B,UAAI;AACF,cAAM,MAAM,MAAM,KAAK,cAAc,SAAS,SAAS;AACvD,YAAI,CAAC,KAAK;AACR;AACA,gBAAM,KAAK,aAAa,SAAS,IAAI;AAAA,YACnC,aAAa;AAAA,YACb,YAAY,UAAU,SAAS,SAAS;AAAA,UAC1C,CAAC;AACD;AAAA,QACF;AACA,cAAM,SAAS,aAAa,GAAG;AAQ/B,cAAM,UAAU,OAAO;AACvB,cAAM,aAAa,WAAW,KAAK,sBAC/B,MAAM,KAAK,oBAAoB,OAAO,EAAE,MAAM,CAAC,QAAQ;AACrD,eAAK,OAAO,OAAO,8DAA8D,GAAG;AACpF,iBAAO;AAAA,QACT,CAAC,IACD;AACJ,YAAI,CAAC,YAAY;AACf;AACA,gBAAM,KAAK,aAAa,SAAS,IAAI;AAAA,YACnC,aAAa;AAAA,YACb,YAAY,UACR,UAAU,OAAO,kGACjB;AAAA,UACN,CAAC;AACD;AAAA,QACF;AAIA,cAAM,MAAqB,SAAS,UAAU;AAC9C,cAAM,SAAS,MAAM,KAAK,cAAc,EAAE,GAAG,QAAQ,QAAQ,IAAI,GAAG,YAAY,KAAK;AAErF,cAAM,aAAa,SAAS,WAAW,MAAM,GAAG,EAAE,IAAI,OAAK,EAAE,KAAK,CAAC,EAAE,OAAO,OAAO;AACnF,cAAM,UAAU,cAAc,SAAS,kBAAkB;AAAA,UACvD,MAAM,SAAS,QAAQ,OAAO;AAAA,UAC9B,MAAM,GAAG,MAAM,GAAG,EAAE;AAAA,UACpB,MAAM,OAAO,OAAO,QAAQ;AAAA,QAC9B,CAAC;AAED,YAAI,KAAK,SAAS,WAAW,SAAS,GAAG;AACvC,cAAI,QAAQ,OAAO;AACjB,kBAAM,KAAK,MAAM,KAAK;AAAA,cACpB,IAAI;AAAA,cACJ;AAAA,cACA,MAAM,aAAa,OAAO,QAAQ;AAAA,cAClC,aAAa,CAAC;AAAA;AAAA;AAAA,gBAGZ,UAAU,IAAI,SAAS,QAAQ,OAAO,MAAM,QAAQ,uBAAuB,GAAG,EAAE,QAAQ,YAAY,EAAE,KAAK,QAAQ,IAAI,GAAG,MAAM,GAAG,EAAE,CAAC;AAAA,gBACtI,SAAS,OAAO;AAAA,gBAChB,aAAa;AAAA,cACf,CAAC;AAAA,cACD,eAAe;AAAA,cACf,WAAW,SAAS;AAAA,YACtB,CAAC;AAAA,UACH,OAAO;AACL,kBAAM,KAAK,MAAM,KAAK;AAAA,cACpB,IAAI;AAAA,cACJ;AAAA,cACA,MAAM,MAAM,WAAW,OAAO,IAAI,CAAC,WAAM,OAAO,QAAQ,cAAc,OAAO,IAAI;AAAA,cACjF,MAAM,GAAG,OAAO,IAAI,WAAM,OAAO,QAAQ;AAAA,cACzC,eAAe;AAAA,cACf,WAAW,SAAS;AAAA,YACtB,CAAC;AAAA,UACH;AAAA,QACF,WAAW,CAAC,KAAK,OAAO;AACtB,eAAK,OAAO,OAAO,qFAAgF;AAAA,QACrG;AAEA,cAAM,KAAK,gBAAgB,UAAU,EAAE;AACvC;AAAA,MACF,SAAS,KAAU;AACjB;AACA,cAAM,KAAK,aAAa,SAAS,IAAI;AAAA,UACnC,aAAa;AAAA,UACb,YAAY,OAAO,KAAK,WAAW,OAAO,SAAS,EAAE,MAAM,GAAG,GAAG;AAAA,QACnE,CAAC;AACD,aAAK,OAAO,QAAQ,8CAA8C,GAAG;AAAA,MACvE;AAAA,IACF;AACA,WAAO,EAAE,OAAO,QAAQ,QAAQ;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYQ,UACN,UACA,MACM;AACN,UAAM,QAAQ,SAAS,mBAAmB,IAAI,KAAK;AACnD,QAAI,MAAM;AACR,UAAI;AACF,cAAM,OAAO,IAAI,mBAAK,MAAM,EAAE,UAAU,SAAS,YAAY,MAAM,CAAC,EAAE,QAAQ,IAAI;AAClF,YAAI,KAAM,QAAO;AACjB,aAAK,OAAO,OAAO,wBAAwB,IAAI,oDAAoD;AAAA,MACrG,SAAS,KAAK;AACZ,aAAK,OAAO,OAAO,gCAAgC,IAAI,+BAA+B,GAAG;AAAA,MAC3F;AAAA,IACF;AACA,UAAM,WAAW,SAAS,oBAAoB;AAC9C,WAAO,IAAI,KAAK,KAAK,QAAQ,IAAI,WAAW,GAAM;AAAA,EACpD;AAAA,EAEA,MAAc,gBAAgB,UAA0B,OAA8B;AACpF,UAAM,UAAU,KAAK,UAAU,UAAU,KAAK,MAAM,IAAI,CAAC,EAAE,YAAY;AACvE,UAAM,KAAK,OAAO,OAAO,uBAAuB;AAAA,MAC9C,IAAI,SAAS;AAAA,MACb,aAAa;AAAA,MACb,cAAc;AAAA,MACd,aAAa;AAAA,MACb,YAAY;AAAA,MACZ,YAAY;AAAA,IACd,GAAG,EAAE,SAAS,WAAW,CAAC;AAAA,EAC5B;AAAA,EAEA,MAAc,aAAa,IAAY,OAA+C;AACpF,QAAI;AACF,YAAM,KAAK,OAAO,OAAO,uBAAuB;AAAA,QAC9C;AAAA,QAAI,GAAG;AAAA,QAAO,YAAY,KAAK,MAAM,IAAI,EAAE,YAAY;AAAA,MACzD,GAAG,EAAE,SAAS,WAAW,CAAC;AAAA,IAC5B,SAAS,KAAK;AACZ,WAAK,OAAO,OAAO,0CAA0C,GAAG;AAAA,IAClE;AAAA,EACF;AACF;;;AC/rBA,mBAGO;AAgCA,IAAM,uBAAN,MAA6C;AAAA,EAYlD,YAAY,UAAgC,CAAC,GAAG;AAXhD,gBAAO;AACP,mBAAU;AACV,gBAAO;AACP,wBAAe,CAAC,iCAAiC;AAS/C,SAAK,UAAU;AAAA,EACjB;AAAA,EAEA,MAAM,KAAK,KAAmC;AAC5C,QAAI,WAAuC,UAAU,EAAE,SAAS;AAAA,MAC9D,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,SAAS;AAAA,MACT,MAAM;AAAA,MACN,OAAO;AAAA,MACP,mBAAmB;AAAA,MACnB,WAAW;AAAA,MACX,SAAS,CAAC,6BAAgB,8BAAiB;AAAA,IAC7C,CAAC;AACD,QAAI,OAAO,KAAK,0CAA0C;AAAA,EAC5D;AAAA,EAEA,MAAM,MAAM,KAAmC;AAC7C,QAAI,KAAK,gBAAgB,YAAY;AACnC,UAAI,SAAc;AAClB,UAAI;AAAE,iBAAS,IAAI,WAAgB,UAAU;AAAA,MAAG,QAC1C;AAAE,YAAI;AAAE,mBAAS,IAAI,WAAgB,MAAM;AAAA,QAAG,QAAQ;AAAA,QAAe;AAAA,MAAE;AAC7E,UAAI,CAAC,QAAQ;AACX,YAAI,OAAO,KAAK,wEAAmE;AACnF;AAAA,MACF;AAEA,UAAI;AACJ,UAAI;AAAE,gBAAQ,IAAI,WAAgB,OAAO;AAAA,MAAG,QAAQ;AAAA,MAA0B;AAC9E,UAAI,CAAC,OAAO;AACV,YAAI,OAAO,KAAK,oFAA+E;AAAA,MACjG;AAWA,YAAM,YAAY,OAAO,QAAgB,YAAuC;AAC9E,YAAI;AACJ,YAAI;AAAE,qBAAW,IAAI,WAAgB,UAAU;AAAA,QAAG,QAAQ;AAAE,iBAAO;AAAA,QAAM;AACzE,YAAI,CAAC,YAAY,OAAO,SAAS,cAAc,WAAY,QAAO;AAClE,eAAO,MAAM,SAAS,UAAU,QAAQ,OAAO;AAAA,MACjD;AAEA,WAAK,UAAU,IAAI,cAAc;AAAA,QAC/B;AAAA,QACA;AAAA,QACA,QAAQ,IAAI;AAAA,QACZ,SAAS,KAAK,QAAQ;AAAA,QACtB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAOA,qBAAqB;AAAA,MACvB,CAAC;AACD,UAAI,gBAAgB,WAAW,KAAK,OAAO;AAE3C,UAAI,KAAK,QAAQ,mBAAmB;AAClC,YAAI,OAAO,KAAK,oEAAoE;AACpF;AAAA,MACF;AAEA,YAAM,aAAa,KAAK,IAAI,KAAO,KAAK,QAAQ,sBAAsB,GAAM;AAI5E,UAAI;AACF,cAAM,MAAM,IAAI,WAAgB,KAAK;AACrC,YAAI,OAAO,OAAO,IAAI,aAAa,YAAY;AAC7C,eAAK,aAAa;AAClB,eAAK,UAAU;AACf,gBAAM,IAAI,SAAS,KAAK,SAAS,EAAE,MAAM,YAAY,WAAW,GAAG,YAAY;AAC7E,gBAAI;AAAE,oBAAM,KAAK,SAAS,YAAY;AAAA,YAAG,SAClC,KAAK;AAAE,kBAAI,OAAO,KAAK,8CAA8C,GAAU;AAAA,YAAG;AAAA,UAC3F,CAAC;AACD,cAAI,OAAO,KAAK,gEAAgE,EAAE,WAAW,CAAC;AAC9F;AAAA,QACF;AAAA,MACF,QAAQ;AAAA,MAAoC;AAE5C,WAAK,iBAAiB,YAAY,MAAM;AACtC,aAAK,SAAS,YAAY,EAAE,MAAM,SAAO;AACvC,cAAI,OAAO,KAAK,8CAA8C,GAAG;AAAA,QACnE,CAAC;AAAA,MACH,GAAG,UAAU;AAIb,MAAC,KAAK,gBAAwB,QAAQ;AACtC,UAAI,OAAO,KAAK,sEAAsE,EAAE,WAAW,CAAC;AAAA,IACtG,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,KAAK,KAAmC;AAC5C,QAAI,KAAK,eAAgB,eAAc,KAAK,cAAc;AAC1D,SAAK,iBAAiB;AACtB,QAAI,KAAK,cAAc,KAAK,WAAW,OAAO,KAAK,WAAW,WAAW,YAAY;AACnF,UAAI;AAAE,cAAM,KAAK,WAAW,OAAO,KAAK,OAAO;AAAA,MAAG,SAC3C,KAAK;AAAE,YAAI,OAAO,KAAK,8CAA8C,GAAU;AAAA,MAAG;AAAA,IAC3F;AAAA,EACF;AACF;","names":["import_audit"]}
|
package/dist/index.mjs
CHANGED
|
@@ -185,7 +185,7 @@ var ReportService = class {
|
|
|
185
185
|
/** Raw metadata read of a saved report by id (no authz — callers gate). */
|
|
186
186
|
async loadReportRow(reportId) {
|
|
187
187
|
const rows = await this.engine.find("sys_saved_report", {
|
|
188
|
-
|
|
188
|
+
where: { id: reportId },
|
|
189
189
|
limit: 1,
|
|
190
190
|
context: SYSTEM_CTX
|
|
191
191
|
});
|
|
@@ -234,7 +234,7 @@ var ReportService = class {
|
|
|
234
234
|
f.owner_id = context.userId;
|
|
235
235
|
}
|
|
236
236
|
const rows = await this.engine.find("sys_saved_report", {
|
|
237
|
-
|
|
237
|
+
where: f,
|
|
238
238
|
limit: 500,
|
|
239
239
|
orderBy: [{ field: "updated_at", order: "desc" }],
|
|
240
240
|
context: SYSTEM_CTX
|
|
@@ -254,7 +254,7 @@ var ReportService = class {
|
|
|
254
254
|
throw new Error(`REPORT_NOT_FOUND: ${reportId}`);
|
|
255
255
|
}
|
|
256
256
|
const schedules = await this.engine.find("sys_report_schedule", {
|
|
257
|
-
|
|
257
|
+
where: { report_id: reportId },
|
|
258
258
|
limit: 500,
|
|
259
259
|
context: SYSTEM_CTX
|
|
260
260
|
});
|
|
@@ -291,7 +291,7 @@ var ReportService = class {
|
|
|
291
291
|
const q = report.query ?? {};
|
|
292
292
|
const limit = Math.min(q.limit ?? DEFAULT_LIMIT, this.maxRows);
|
|
293
293
|
const rows = await this.engine.find(report.object_name, {
|
|
294
|
-
|
|
294
|
+
where: q.filter,
|
|
295
295
|
fields: q.fields,
|
|
296
296
|
orderBy: q.orderBy,
|
|
297
297
|
limit,
|
|
@@ -381,7 +381,7 @@ var ReportService = class {
|
|
|
381
381
|
const f = {};
|
|
382
382
|
if (filter?.reportId) f.report_id = filter.reportId;
|
|
383
383
|
const rows = await this.engine.find("sys_report_schedule", {
|
|
384
|
-
|
|
384
|
+
where: f,
|
|
385
385
|
limit: 500,
|
|
386
386
|
orderBy: [{ field: "next_run_at", order: "asc" }],
|
|
387
387
|
context: SYSTEM_CTX
|
|
@@ -392,7 +392,7 @@ var ReportService = class {
|
|
|
392
392
|
async dispatchDue(now) {
|
|
393
393
|
const ts = (now ?? this.clock.now()).toISOString();
|
|
394
394
|
const due = await this.engine.find("sys_report_schedule", {
|
|
395
|
-
|
|
395
|
+
where: { active: true },
|
|
396
396
|
limit: 200,
|
|
397
397
|
context: SYSTEM_CTX
|
|
398
398
|
});
|