@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.
@@ -1,161 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import type { Plugin, PluginContext } from '@objectstack/core';
4
- import {
5
- SysSavedReport,
6
- SysReportSchedule,
7
- } from '@objectstack/platform-objects/audit';
8
- import { ReportService, type ReportEngine, type ReportEmail } from './report-service.js';
9
-
10
- export interface ReportsPluginOptions {
11
- /**
12
- * How often the dispatcher should poll `sys_report_schedule` for
13
- * due rows. Defaults to 60 seconds — short enough to honour
14
- * minute-grained schedules without flooding the DB.
15
- */
16
- dispatchIntervalMs?: number;
17
- /** Cap rows per report. Mirrors ReportServiceOptions.maxRows. */
18
- maxRows?: number;
19
- /** Disable the dispatcher tick entirely. */
20
- disableDispatcher?: boolean;
21
- }
22
-
23
- /**
24
- * ReportsServicePlugin — registers `sys_saved_report` /
25
- * `sys_report_schedule`, the `reports` service, and the dispatcher
26
- * loop that emails due schedules.
27
- *
28
- * The dispatcher uses `IJobService.schedule` when one is registered;
29
- * otherwise it falls back to a plain `setInterval` so single-kernel
30
- * deployments work without `service-job`.
31
- *
32
- * @example
33
- * ```ts
34
- * import { ReportsServicePlugin } from '@objectstack/plugin-reports';
35
- *
36
- * kernel.use(new ReportsServicePlugin({ dispatchIntervalMs: 60_000 }));
37
- * ```
38
- */
39
- export class ReportsServicePlugin implements Plugin {
40
- name = 'com.objectstack.service.reports';
41
- version = '1.0.0';
42
- type = 'standard';
43
- dependencies = ['com.objectstack.engine.objectql'];
44
-
45
- private readonly options: ReportsPluginOptions;
46
- private service?: ReportService;
47
- private intervalHandle?: ReturnType<typeof setInterval>;
48
- private jobName?: string;
49
- private jobService?: any;
50
-
51
- constructor(options: ReportsPluginOptions = {}) {
52
- this.options = options;
53
- }
54
-
55
- async init(ctx: PluginContext): Promise<void> {
56
- ctx.getService<{ register(m: any): void }>('manifest').register({
57
- id: 'com.objectstack.service.reports',
58
- name: 'Reports Service',
59
- version: '1.0.0',
60
- type: 'plugin',
61
- scope: 'system',
62
- defaultDatasource: 'cloud',
63
- namespace: 'sys',
64
- objects: [SysSavedReport, SysReportSchedule],
65
- });
66
- ctx.logger.info('ReportsServicePlugin: schemas registered');
67
- }
68
-
69
- async start(ctx: PluginContext): Promise<void> {
70
- ctx.hook('kernel:ready', async () => {
71
- let engine: any = null;
72
- try { engine = ctx.getService<any>('objectql'); }
73
- catch { try { engine = ctx.getService<any>('data'); } catch { /* ignore */ } }
74
- if (!engine) {
75
- ctx.logger.warn('ReportsServicePlugin: no ObjectQL engine — service NOT registered');
76
- return;
77
- }
78
-
79
- let email: ReportEmail | undefined;
80
- try { email = ctx.getService<any>('email'); } catch { /* email is optional */ }
81
- if (!email) {
82
- ctx.logger.warn('ReportsServicePlugin: no email service — schedules will fire without delivery');
83
- }
84
-
85
- // [#3544 / #3710] The user-level export axis. A `csv`/`json` report is a
86
- // bulk machine-readable copy of its object — the same privilege
87
- // `GET /data/:object/export` gates — so the reports surface must ask the
88
- // SAME question of the SAME authority, or it is a side door around the
89
- // axis. Resolved lazily per call rather than captured here: `security` is
90
- // registered by plugin-security's own `kernel:ready` hook and may not
91
- // exist yet at construction time. Absent service (no plugin-security ⇒ no
92
- // permission sets anywhere) → the axis does not apply, matching the REST
93
- // export route's fail-open.
94
- const canExport = async (object: string, context: unknown): Promise<boolean> => {
95
- let security: any;
96
- try { security = ctx.getService<any>('security'); } catch { return true; }
97
- if (!security || typeof security.canExport !== 'function') return true;
98
- return await security.canExport(object, context);
99
- };
100
-
101
- this.service = new ReportService({
102
- engine: engine as ReportEngine,
103
- email,
104
- logger: ctx.logger,
105
- maxRows: this.options.maxRows,
106
- canExport,
107
- // Scheduled reports run under the owner's resolved RLS context, not a
108
- // system bypass (#2980). No owner-context resolver is wired yet — that
109
- // is the reports-surface consumer of ADR-0073's user-less identity
110
- // resolution (M2) — so until it lands, scheduled runs FAIL CLOSED
111
- // (skipped + marked failed) rather than exfiltrate. Interactive runs
112
- // (run/runAdHoc) are unaffected: they carry the caller's context.
113
- resolveOwnerContext: undefined,
114
- });
115
- ctx.registerService('reports', this.service);
116
-
117
- if (this.options.disableDispatcher) {
118
- ctx.logger.info('ReportsServicePlugin: dispatcher disabled (disableDispatcher=true)');
119
- return;
120
- }
121
-
122
- const intervalMs = Math.max(5_000, this.options.dispatchIntervalMs ?? 60_000);
123
-
124
- // Prefer the platform job service when available — it lets ops
125
- // see report dispatch alongside every other scheduled job.
126
- try {
127
- const job = ctx.getService<any>('job');
128
- if (job && typeof job.schedule === 'function') {
129
- this.jobService = job;
130
- this.jobName = 'reports.dispatch';
131
- await job.schedule(this.jobName, { type: 'interval', intervalMs }, async () => {
132
- try { await this.service?.dispatchDue(); }
133
- catch (err) { ctx.logger.warn('ReportsServicePlugin: dispatch tick failed', err as any); }
134
- });
135
- ctx.logger.info('ReportsServicePlugin: dispatcher registered with job service', { intervalMs });
136
- return;
137
- }
138
- } catch { /* fall through to setInterval */ }
139
-
140
- this.intervalHandle = setInterval(() => {
141
- this.service?.dispatchDue().catch(err => {
142
- ctx.logger.warn('ReportsServicePlugin: dispatch tick failed', err);
143
- });
144
- }, intervalMs);
145
- // Don't keep Node alive purely for the dispatcher — common
146
- // mistake in tests / serverless. unref is a no-op in some
147
- // runtimes which is fine.
148
- (this.intervalHandle as any)?.unref?.();
149
- ctx.logger.info('ReportsServicePlugin: dispatcher registered (setInterval fallback)', { intervalMs });
150
- });
151
- }
152
-
153
- async stop(ctx: PluginContext): Promise<void> {
154
- if (this.intervalHandle) clearInterval(this.intervalHandle);
155
- this.intervalHandle = undefined;
156
- if (this.jobService && this.jobName && typeof this.jobService.cancel === 'function') {
157
- try { await this.jobService.cancel(this.jobName); }
158
- catch (err) { ctx.logger.warn('ReportsServicePlugin: failed to cancel job', err as any); }
159
- }
160
- }
161
- }
package/tsconfig.json DELETED
@@ -1,10 +0,0 @@
1
- {
2
- "extends": "../../../tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "./dist",
5
- "rootDir": "./src",
6
- "types": ["node"]
7
- },
8
- "include": ["src/**/*"],
9
- "exclude": ["dist", "node_modules", "**/*.test.ts"]
10
- }