@objectstack/plugin-reports 15.0.0 → 15.1.0
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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +79 -0
- package/dist/index.d.mts +34 -4
- package/dist/index.d.ts +34 -4
- package/dist/index.js +78 -22
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +78 -22
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/report-service.test.ts +99 -4
- package/src/report-service.ts +118 -17
- package/src/reports-plugin.ts +7 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @objectstack/plugin-reports@15.
|
|
2
|
+
> @objectstack/plugin-reports@15.1.0 build /home/runner/work/framework/framework/packages/plugins/plugin-reports
|
|
3
3
|
> tsup --config ../../../tsup.config.ts
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.ts
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
[34mCLI[39m Cleaning output folder
|
|
11
11
|
[34mESM[39m Build start
|
|
12
12
|
[34mCJS[39m Build start
|
|
13
|
-
[32mESM[39m [1mdist/index.mjs [22m[
|
|
14
|
-
[32mESM[39m [1mdist/index.mjs.map [22m[
|
|
15
|
-
[32mESM[39m ⚡️ Build success in
|
|
16
|
-
[32mCJS[39m [1mdist/index.js [22m[
|
|
17
|
-
[32mCJS[39m [1mdist/index.js.map [22m[
|
|
18
|
-
[32mCJS[39m ⚡️ Build success in
|
|
13
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m22.17 KB[39m
|
|
14
|
+
[32mESM[39m [1mdist/index.mjs.map [22m[32m46.00 KB[39m
|
|
15
|
+
[32mESM[39m ⚡️ Build success in 165ms
|
|
16
|
+
[32mCJS[39m [1mdist/index.js [22m[32m23.30 KB[39m
|
|
17
|
+
[32mCJS[39m [1mdist/index.js.map [22m[32m46.00 KB[39m
|
|
18
|
+
[32mCJS[39m ⚡️ Build success in 171ms
|
|
19
19
|
[34mDTS[39m Build start
|
|
20
|
-
[32mDTS[39m ⚡️ Build success in
|
|
21
|
-
[32mDTS[39m [1mdist/index.d.mts [22m[
|
|
22
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[
|
|
20
|
+
[32mDTS[39m ⚡️ Build success in 19372ms
|
|
21
|
+
[32mDTS[39m [1mdist/index.d.mts [22m[32m7.17 KB[39m
|
|
22
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m7.17 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,84 @@
|
|
|
1
1
|
# @objectstack/plugin-reports
|
|
2
2
|
|
|
3
|
+
## 15.1.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f531a26: fix(security): close three execution-surface authz holes surfaced by the #2849 class sweep (#2980, #2981, #2982)
|
|
8
|
+
|
|
9
|
+
Three independent, confirmed-exploitable defects where an execution surface
|
|
10
|
+
ignored the caller's identity or fell open on a missing one. Each is fixed at
|
|
11
|
+
its own enforcement point; none change behaviour for correctly-scoped callers.
|
|
12
|
+
|
|
13
|
+
- **#2980 — reports IDOR + scheduled-report RLS bypass.** `ReportService`
|
|
14
|
+
discarded the caller's context and read/wrote `sys_saved_report` with a system
|
|
15
|
+
context, so any authenticated user could read, delete, or overwrite any saved
|
|
16
|
+
report by id (cross-owner / cross-tenant), and `listReports` enumerated all
|
|
17
|
+
owners. `getReport`/`deleteReport`/`saveReport`/`listReports` are now
|
|
18
|
+
owner-scoped (system read of the protection-locked metadata object, but
|
|
19
|
+
authorization enforced by owner match); create/overwrite can no longer spoof
|
|
20
|
+
ownership. Scheduled dispatch no longer runs `isSystem` (which emailed the
|
|
21
|
+
target object's entire table past the owner's RLS): it resolves the owner to a
|
|
22
|
+
real RLS-bearing context via a new `resolveOwnerContext` seam and **fails
|
|
23
|
+
closed** (skips + marks the schedule failed) when the owner can't be resolved,
|
|
24
|
+
rather than running elevated. Wiring that resolver is the reports-surface
|
|
25
|
+
consumer of ADR-0073's user-less identity resolution.
|
|
26
|
+
|
|
27
|
+
- **#2981 — knowledge/RAG retrieval fall-open.** `applyPermissionFilter` returned
|
|
28
|
+
every hit when the context was missing _or_ system. A missing identity is no
|
|
29
|
+
longer treated as a grant: object-backed hits fail closed (dropped, keeping
|
|
30
|
+
ACL-less file/http hits), and only an **explicit** system context passes
|
|
31
|
+
through. Closes the agent path where an omitted `ToolExecutionContext.actor`
|
|
32
|
+
yielded unfiltered semantic search over the whole corpus.
|
|
33
|
+
|
|
34
|
+
- **#2982 — bulk-write OWD gap.** `update({multi:true})` / `deleteMany` had no
|
|
35
|
+
single id to `canEdit`-gate, so owner scoping was skipped on private (and
|
|
36
|
+
public_read) objects. A new `SharingService.buildWriteFilter` (the edit-set
|
|
37
|
+
analogue of `buildReadFilter`) is AND-ed into the write AST for multi writes,
|
|
38
|
+
constraining them to rows the caller may edit — including the on-behalf-of
|
|
39
|
+
delegator intersection.
|
|
40
|
+
|
|
41
|
+
Tracked as the motivating evidence of ADR-0096 (execution-surface identity
|
|
42
|
+
admission); the mechanism that would prevent the class structurally is separate.
|
|
43
|
+
|
|
44
|
+
- Updated dependencies [f531a26]
|
|
45
|
+
- Updated dependencies [f531a26]
|
|
46
|
+
- Updated dependencies [f531a26]
|
|
47
|
+
- Updated dependencies [f531a26]
|
|
48
|
+
- Updated dependencies [f531a26]
|
|
49
|
+
- Updated dependencies [f531a26]
|
|
50
|
+
- Updated dependencies [3fe9df1]
|
|
51
|
+
- Updated dependencies [f531a26]
|
|
52
|
+
- Updated dependencies [f531a26]
|
|
53
|
+
- Updated dependencies [f531a26]
|
|
54
|
+
- Updated dependencies [f531a26]
|
|
55
|
+
- Updated dependencies [f531a26]
|
|
56
|
+
- Updated dependencies [f531a26]
|
|
57
|
+
- Updated dependencies [f531a26]
|
|
58
|
+
- Updated dependencies [f531a26]
|
|
59
|
+
- Updated dependencies [f531a26]
|
|
60
|
+
- Updated dependencies [f531a26]
|
|
61
|
+
- Updated dependencies [f531a26]
|
|
62
|
+
- Updated dependencies [f531a26]
|
|
63
|
+
- Updated dependencies [f531a26]
|
|
64
|
+
- Updated dependencies [f531a26]
|
|
65
|
+
- Updated dependencies [f531a26]
|
|
66
|
+
- Updated dependencies [4109153]
|
|
67
|
+
- Updated dependencies [f531a26]
|
|
68
|
+
- Updated dependencies [f531a26]
|
|
69
|
+
- Updated dependencies [f531a26]
|
|
70
|
+
- Updated dependencies [f531a26]
|
|
71
|
+
- Updated dependencies [f531a26]
|
|
72
|
+
- Updated dependencies [f531a26]
|
|
73
|
+
- Updated dependencies [f531a26]
|
|
74
|
+
- Updated dependencies [627f225]
|
|
75
|
+
- Updated dependencies [f531a26]
|
|
76
|
+
- Updated dependencies [f531a26]
|
|
77
|
+
- Updated dependencies [f531a26]
|
|
78
|
+
- @objectstack/spec@15.1.0
|
|
79
|
+
- @objectstack/platform-objects@15.1.0
|
|
80
|
+
- @objectstack/core@15.1.0
|
|
81
|
+
|
|
3
82
|
## 15.0.0
|
|
4
83
|
|
|
5
84
|
### Patch Changes
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { SysReportSchedule, SysSavedReport } from '@objectstack/platform-objects/audit';
|
|
2
|
-
import { IReportService,
|
|
2
|
+
import { IReportService, SharingExecutionContext, SaveReportInput, SavedReport, ReportRunResult, ScheduleReportInput, ReportSchedule, ReportFormat } from '@objectstack/spec/contracts';
|
|
3
3
|
export { IReportService, ReportFormat, ReportQuery, ReportRunResult, ReportSchedule, SaveReportInput, SavedReport, ScheduleReportInput } from '@objectstack/spec/contracts';
|
|
4
4
|
import { Plugin, PluginContext } from '@objectstack/core';
|
|
5
5
|
|
|
@@ -40,6 +40,17 @@ interface ReportClock {
|
|
|
40
40
|
now(): Date;
|
|
41
41
|
}
|
|
42
42
|
declare function renderReport(rows: any[], format: ReportFormat, fields?: string[]): string;
|
|
43
|
+
/**
|
|
44
|
+
* Resolves a saved report's owner (`owner_id`) into a real, RLS-bearing
|
|
45
|
+
* `ExecutionContext` so a **scheduled** report executes under the owner's
|
|
46
|
+
* authority — the same rows the owner would see interactively — instead of
|
|
47
|
+
* bypassing RLS with a system context. Returns `null` when the owner cannot
|
|
48
|
+
* be resolved (unknown/disabled user), in which case the scheduler fails the
|
|
49
|
+
* run closed rather than running elevated (#2849 / #2980). Supplying this
|
|
50
|
+
* resolver is the reports-surface consumer of ADR-0073's user-less identity
|
|
51
|
+
* resolution.
|
|
52
|
+
*/
|
|
53
|
+
type OwnerContextResolver = (ownerId: string) => Promise<SharingExecutionContext | null>;
|
|
43
54
|
interface ReportServiceOptions {
|
|
44
55
|
engine: ReportEngine;
|
|
45
56
|
email?: ReportEmail;
|
|
@@ -51,6 +62,12 @@ interface ReportServiceOptions {
|
|
|
51
62
|
};
|
|
52
63
|
/** Cap rows per report to protect both DB and email size. */
|
|
53
64
|
maxRows?: number;
|
|
65
|
+
/**
|
|
66
|
+
* Resolves a report owner into an RLS-bearing context for scheduled runs
|
|
67
|
+
* (see {@link OwnerContextResolver}). When omitted, scheduled reports fail
|
|
68
|
+
* closed instead of running with RLS bypassed (#2980).
|
|
69
|
+
*/
|
|
70
|
+
resolveOwnerContext?: OwnerContextResolver;
|
|
54
71
|
}
|
|
55
72
|
declare class ReportService implements IReportService {
|
|
56
73
|
private readonly engine;
|
|
@@ -58,14 +75,27 @@ declare class ReportService implements IReportService {
|
|
|
58
75
|
private readonly clock;
|
|
59
76
|
private readonly logger;
|
|
60
77
|
private readonly maxRows;
|
|
78
|
+
private readonly resolveOwnerContext?;
|
|
61
79
|
constructor(opts: ReportServiceOptions);
|
|
80
|
+
/**
|
|
81
|
+
* Authorization for a saved-report row. `sys_saved_report` is a
|
|
82
|
+
* protection-locked system object, so its rows are *read* with
|
|
83
|
+
* `SYSTEM_CTX`; the caller's right to see/mutate a specific report is
|
|
84
|
+
* enforced HERE, by owner match, not by the metadata read's own RLS —
|
|
85
|
+
* otherwise any authenticated caller could read/delete/overwrite any
|
|
86
|
+
* report by id (#2980). An explicit elevated context (`isSystem`) — the
|
|
87
|
+
* scheduler / server tooling — sees everything.
|
|
88
|
+
*/
|
|
89
|
+
private canAccessReport;
|
|
90
|
+
/** Raw metadata read of a saved report by id (no authz — callers gate). */
|
|
91
|
+
private loadReportRow;
|
|
62
92
|
saveReport(input: SaveReportInput, context: SharingExecutionContext): Promise<SavedReport>;
|
|
63
93
|
listReports(filter: {
|
|
64
94
|
object?: string;
|
|
65
95
|
ownerId?: string;
|
|
66
|
-
} | undefined,
|
|
67
|
-
getReport(reportId: string,
|
|
68
|
-
deleteReport(reportId: string,
|
|
96
|
+
} | undefined, context: SharingExecutionContext): Promise<SavedReport[]>;
|
|
97
|
+
getReport(reportId: string, context: SharingExecutionContext): Promise<SavedReport | null>;
|
|
98
|
+
deleteReport(reportId: string, context: SharingExecutionContext): Promise<void>;
|
|
69
99
|
run(reportId: string, context: SharingExecutionContext): Promise<ReportRunResult>;
|
|
70
100
|
runAdHoc(input: SaveReportInput, context: SharingExecutionContext): Promise<ReportRunResult>;
|
|
71
101
|
private executeReport;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { SysReportSchedule, SysSavedReport } from '@objectstack/platform-objects/audit';
|
|
2
|
-
import { IReportService,
|
|
2
|
+
import { IReportService, SharingExecutionContext, SaveReportInput, SavedReport, ReportRunResult, ScheduleReportInput, ReportSchedule, ReportFormat } from '@objectstack/spec/contracts';
|
|
3
3
|
export { IReportService, ReportFormat, ReportQuery, ReportRunResult, ReportSchedule, SaveReportInput, SavedReport, ScheduleReportInput } from '@objectstack/spec/contracts';
|
|
4
4
|
import { Plugin, PluginContext } from '@objectstack/core';
|
|
5
5
|
|
|
@@ -40,6 +40,17 @@ interface ReportClock {
|
|
|
40
40
|
now(): Date;
|
|
41
41
|
}
|
|
42
42
|
declare function renderReport(rows: any[], format: ReportFormat, fields?: string[]): string;
|
|
43
|
+
/**
|
|
44
|
+
* Resolves a saved report's owner (`owner_id`) into a real, RLS-bearing
|
|
45
|
+
* `ExecutionContext` so a **scheduled** report executes under the owner's
|
|
46
|
+
* authority — the same rows the owner would see interactively — instead of
|
|
47
|
+
* bypassing RLS with a system context. Returns `null` when the owner cannot
|
|
48
|
+
* be resolved (unknown/disabled user), in which case the scheduler fails the
|
|
49
|
+
* run closed rather than running elevated (#2849 / #2980). Supplying this
|
|
50
|
+
* resolver is the reports-surface consumer of ADR-0073's user-less identity
|
|
51
|
+
* resolution.
|
|
52
|
+
*/
|
|
53
|
+
type OwnerContextResolver = (ownerId: string) => Promise<SharingExecutionContext | null>;
|
|
43
54
|
interface ReportServiceOptions {
|
|
44
55
|
engine: ReportEngine;
|
|
45
56
|
email?: ReportEmail;
|
|
@@ -51,6 +62,12 @@ interface ReportServiceOptions {
|
|
|
51
62
|
};
|
|
52
63
|
/** Cap rows per report to protect both DB and email size. */
|
|
53
64
|
maxRows?: number;
|
|
65
|
+
/**
|
|
66
|
+
* Resolves a report owner into an RLS-bearing context for scheduled runs
|
|
67
|
+
* (see {@link OwnerContextResolver}). When omitted, scheduled reports fail
|
|
68
|
+
* closed instead of running with RLS bypassed (#2980).
|
|
69
|
+
*/
|
|
70
|
+
resolveOwnerContext?: OwnerContextResolver;
|
|
54
71
|
}
|
|
55
72
|
declare class ReportService implements IReportService {
|
|
56
73
|
private readonly engine;
|
|
@@ -58,14 +75,27 @@ declare class ReportService implements IReportService {
|
|
|
58
75
|
private readonly clock;
|
|
59
76
|
private readonly logger;
|
|
60
77
|
private readonly maxRows;
|
|
78
|
+
private readonly resolveOwnerContext?;
|
|
61
79
|
constructor(opts: ReportServiceOptions);
|
|
80
|
+
/**
|
|
81
|
+
* Authorization for a saved-report row. `sys_saved_report` is a
|
|
82
|
+
* protection-locked system object, so its rows are *read* with
|
|
83
|
+
* `SYSTEM_CTX`; the caller's right to see/mutate a specific report is
|
|
84
|
+
* enforced HERE, by owner match, not by the metadata read's own RLS —
|
|
85
|
+
* otherwise any authenticated caller could read/delete/overwrite any
|
|
86
|
+
* report by id (#2980). An explicit elevated context (`isSystem`) — the
|
|
87
|
+
* scheduler / server tooling — sees everything.
|
|
88
|
+
*/
|
|
89
|
+
private canAccessReport;
|
|
90
|
+
/** Raw metadata read of a saved report by id (no authz — callers gate). */
|
|
91
|
+
private loadReportRow;
|
|
62
92
|
saveReport(input: SaveReportInput, context: SharingExecutionContext): Promise<SavedReport>;
|
|
63
93
|
listReports(filter: {
|
|
64
94
|
object?: string;
|
|
65
95
|
ownerId?: string;
|
|
66
|
-
} | undefined,
|
|
67
|
-
getReport(reportId: string,
|
|
68
|
-
deleteReport(reportId: string,
|
|
96
|
+
} | undefined, context: SharingExecutionContext): Promise<SavedReport[]>;
|
|
97
|
+
getReport(reportId: string, context: SharingExecutionContext): Promise<SavedReport | null>;
|
|
98
|
+
deleteReport(reportId: string, context: SharingExecutionContext): Promise<void>;
|
|
69
99
|
run(reportId: string, context: SharingExecutionContext): Promise<ReportRunResult>;
|
|
70
100
|
runAdHoc(input: SaveReportInput, context: SharingExecutionContext): Promise<ReportRunResult>;
|
|
71
101
|
private executeReport;
|
package/dist/index.js
CHANGED
|
@@ -154,6 +154,32 @@ var ReportService = class {
|
|
|
154
154
|
this.clock = opts.clock ?? { now: () => /* @__PURE__ */ new Date() };
|
|
155
155
|
this.logger = opts.logger ?? {};
|
|
156
156
|
this.maxRows = Math.max(1, opts.maxRows ?? 5e3);
|
|
157
|
+
this.resolveOwnerContext = opts.resolveOwnerContext;
|
|
158
|
+
}
|
|
159
|
+
// ── Access control ─────────────────────────────────────────────
|
|
160
|
+
/**
|
|
161
|
+
* Authorization for a saved-report row. `sys_saved_report` is a
|
|
162
|
+
* protection-locked system object, so its rows are *read* with
|
|
163
|
+
* `SYSTEM_CTX`; the caller's right to see/mutate a specific report is
|
|
164
|
+
* enforced HERE, by owner match, not by the metadata read's own RLS —
|
|
165
|
+
* otherwise any authenticated caller could read/delete/overwrite any
|
|
166
|
+
* report by id (#2980). An explicit elevated context (`isSystem`) — the
|
|
167
|
+
* scheduler / server tooling — sees everything.
|
|
168
|
+
*/
|
|
169
|
+
canAccessReport(row, context) {
|
|
170
|
+
if (!row) return false;
|
|
171
|
+
if (context?.isSystem) return true;
|
|
172
|
+
const userId = context?.userId;
|
|
173
|
+
return !!userId && row.owner_id === userId;
|
|
174
|
+
}
|
|
175
|
+
/** Raw metadata read of a saved report by id (no authz — callers gate). */
|
|
176
|
+
async loadReportRow(reportId) {
|
|
177
|
+
const rows = await this.engine.find("sys_saved_report", {
|
|
178
|
+
filter: { id: reportId },
|
|
179
|
+
limit: 1,
|
|
180
|
+
context: SYSTEM_CTX
|
|
181
|
+
});
|
|
182
|
+
return Array.isArray(rows) && rows[0] ? rows[0] : null;
|
|
157
183
|
}
|
|
158
184
|
// ── Report CRUD ────────────────────────────────────────────────
|
|
159
185
|
async saveReport(input, context) {
|
|
@@ -161,24 +187,25 @@ var ReportService = class {
|
|
|
161
187
|
if (!input.object) throw new Error("VALIDATION_FAILED: object is required");
|
|
162
188
|
if (!input.query) throw new Error("VALIDATION_FAILED: query is required");
|
|
163
189
|
const now = this.clock.now().toISOString();
|
|
190
|
+
const ownerId = context.isSystem ? input.ownerId ?? context.userId ?? null : context.userId ?? null;
|
|
164
191
|
const payload = {
|
|
165
192
|
name: input.name,
|
|
166
193
|
description: input.description ?? null,
|
|
167
194
|
object_name: input.object,
|
|
168
195
|
query_json: JSON.stringify(input.query ?? {}),
|
|
169
196
|
format: input.format ?? DEFAULT_FORMAT,
|
|
170
|
-
owner_id:
|
|
197
|
+
owner_id: ownerId,
|
|
171
198
|
updated_at: now
|
|
172
199
|
};
|
|
173
200
|
if (input.id) {
|
|
174
|
-
const existing = await this.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
201
|
+
const existing = await this.loadReportRow(input.id);
|
|
202
|
+
if (existing) {
|
|
203
|
+
if (!this.canAccessReport(existing, context)) {
|
|
204
|
+
throw new Error(`REPORT_NOT_FOUND: ${input.id}`);
|
|
205
|
+
}
|
|
206
|
+
if (!context.isSystem) payload.owner_id = existing.owner_id ?? payload.owner_id;
|
|
180
207
|
await this.engine.update("sys_saved_report", { id: input.id, ...payload }, { context: SYSTEM_CTX });
|
|
181
|
-
return rowFromSaved({ ...existing
|
|
208
|
+
return rowFromSaved({ ...existing, ...payload, id: input.id });
|
|
182
209
|
}
|
|
183
210
|
}
|
|
184
211
|
const id = input.id ?? uid("rpt");
|
|
@@ -186,10 +213,16 @@ var ReportService = class {
|
|
|
186
213
|
await this.engine.insert("sys_saved_report", row, { context: SYSTEM_CTX });
|
|
187
214
|
return rowFromSaved(row);
|
|
188
215
|
}
|
|
189
|
-
async listReports(filter,
|
|
216
|
+
async listReports(filter, context) {
|
|
190
217
|
const f = {};
|
|
191
218
|
if (filter?.object) f.object_name = filter.object;
|
|
192
|
-
if (
|
|
219
|
+
if (context?.isSystem) {
|
|
220
|
+
if (filter?.ownerId) f.owner_id = filter.ownerId;
|
|
221
|
+
} else {
|
|
222
|
+
if (!context?.userId) return [];
|
|
223
|
+
if (filter?.ownerId && filter.ownerId !== context.userId) return [];
|
|
224
|
+
f.owner_id = context.userId;
|
|
225
|
+
}
|
|
193
226
|
const rows = await this.engine.find("sys_saved_report", {
|
|
194
227
|
filter: f,
|
|
195
228
|
limit: 500,
|
|
@@ -198,16 +231,18 @@ var ReportService = class {
|
|
|
198
231
|
});
|
|
199
232
|
return Array.isArray(rows) ? rows.map(rowFromSaved) : [];
|
|
200
233
|
}
|
|
201
|
-
async getReport(reportId,
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
context: SYSTEM_CTX
|
|
206
|
-
});
|
|
207
|
-
return Array.isArray(rows) && rows[0] ? rowFromSaved(rows[0]) : null;
|
|
234
|
+
async getReport(reportId, context) {
|
|
235
|
+
const row = await this.loadReportRow(reportId);
|
|
236
|
+
if (!this.canAccessReport(row, context)) return null;
|
|
237
|
+
return rowFromSaved(row);
|
|
208
238
|
}
|
|
209
|
-
async deleteReport(reportId,
|
|
239
|
+
async deleteReport(reportId, context) {
|
|
210
240
|
if (!reportId) throw new Error("VALIDATION_FAILED: reportId is required");
|
|
241
|
+
const row = await this.loadReportRow(reportId);
|
|
242
|
+
if (!row) return;
|
|
243
|
+
if (!this.canAccessReport(row, context)) {
|
|
244
|
+
throw new Error(`REPORT_NOT_FOUND: ${reportId}`);
|
|
245
|
+
}
|
|
211
246
|
const schedules = await this.engine.find("sys_report_schedule", {
|
|
212
247
|
filter: { report_id: reportId },
|
|
213
248
|
limit: 500,
|
|
@@ -353,8 +388,8 @@ var ReportService = class {
|
|
|
353
388
|
let fired = 0, failed = 0, skipped = 0;
|
|
354
389
|
for (const schedule of list) {
|
|
355
390
|
try {
|
|
356
|
-
const
|
|
357
|
-
if (!
|
|
391
|
+
const row = await this.loadReportRow(schedule.report_id);
|
|
392
|
+
if (!row) {
|
|
358
393
|
skipped++;
|
|
359
394
|
await this.markSchedule(schedule.id, {
|
|
360
395
|
last_status: "skipped",
|
|
@@ -362,8 +397,22 @@ var ReportService = class {
|
|
|
362
397
|
});
|
|
363
398
|
continue;
|
|
364
399
|
}
|
|
400
|
+
const report = rowFromSaved(row);
|
|
401
|
+
const ownerId = report.owner_id;
|
|
402
|
+
const runContext = ownerId && this.resolveOwnerContext ? await this.resolveOwnerContext(ownerId).catch((err) => {
|
|
403
|
+
this.logger.warn?.("ReportService.dispatchDue: owner context resolution failed", err);
|
|
404
|
+
return null;
|
|
405
|
+
}) : null;
|
|
406
|
+
if (!runContext) {
|
|
407
|
+
failed++;
|
|
408
|
+
await this.markSchedule(schedule.id, {
|
|
409
|
+
last_status: "failed",
|
|
410
|
+
last_error: ownerId ? `owner '${ownerId}' context unavailable \u2014 refusing to run scheduled report with RLS bypassed (#2849/#2980)` : "report has no owner \u2014 refusing to run scheduled report with RLS bypassed (#2849/#2980)"
|
|
411
|
+
});
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
365
414
|
const fmt = schedule.format ?? "html_table";
|
|
366
|
-
const result = await this.executeReport({ ...report, format: fmt },
|
|
415
|
+
const result = await this.executeReport({ ...report, format: fmt }, runContext, false);
|
|
367
416
|
const recipients = schedule.recipients.split(",").map((s) => s.trim()).filter(Boolean);
|
|
368
417
|
const subject = renderSubject(schedule.subject_template, {
|
|
369
418
|
name: schedule.name ?? report.name,
|
|
@@ -510,7 +559,14 @@ var ReportsServicePlugin = class {
|
|
|
510
559
|
engine,
|
|
511
560
|
email,
|
|
512
561
|
logger: ctx.logger,
|
|
513
|
-
maxRows: this.options.maxRows
|
|
562
|
+
maxRows: this.options.maxRows,
|
|
563
|
+
// Scheduled reports run under the owner's resolved RLS context, not a
|
|
564
|
+
// system bypass (#2980). No owner-context resolver is wired yet — that
|
|
565
|
+
// is the reports-surface consumer of ADR-0073's user-less identity
|
|
566
|
+
// resolution (M2) — so until it lands, scheduled runs FAIL CLOSED
|
|
567
|
+
// (skipped + marked failed) rather than exfiltrate. Interactive runs
|
|
568
|
+
// (run/runAdHoc) are unaffected: they carry the caller's context.
|
|
569
|
+
resolveOwnerContext: void 0
|
|
514
570
|
});
|
|
515
571
|
ctx.registerService("reports", this.service);
|
|
516
572
|
if (this.options.disableDispatcher) {
|
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\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\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\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 }\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 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: input.ownerId ?? context.userId ?? null,\n updated_at: now,\n };\n\n if (input.id) {\n const existing = await this.engine.find('sys_saved_report', {\n filter: { id: input.id }, limit: 1, context: SYSTEM_CTX,\n });\n if (Array.isArray(existing) && existing[0]) {\n await this.engine.update('sys_saved_report', { id: input.id, ...payload }, { context: SYSTEM_CTX });\n return rowFromSaved({ ...existing[0], ...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 if (filter?.ownerId) f.owner_id = filter.ownerId;\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 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] ? rowFromSaved(rows[0]) : null;\n }\n\n async deleteReport(reportId: string, _context: SharingExecutionContext): Promise<void> {\n if (!reportId) throw new Error('VALIDATION_FAILED: reportId is required');\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 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 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 report = await this.getReport(schedule.report_id, { isSystem: true });\n if (!report) {\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 // 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 }, { isSystem: true }, 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 this.service = new ReportService({\n engine: engine as ReportEngine,\n email,\n logger: ctx.logger,\n maxRows: this.options.maxRows,\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\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\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 }\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 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 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 this.service = new ReportService({\n engine: engine as ReportEngine,\n email,\n logger: ctx.logger,\n maxRows: this.options.maxRows,\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"]}
|