@objectstack/plugin-reports 9.2.0 → 9.3.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 +7 -7
- package/CHANGELOG.md +16 -0
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/report-service.test.ts +17 -2
- package/src/report-service.ts +2 -2
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @objectstack/plugin-reports@9.
|
|
2
|
+
> @objectstack/plugin-reports@9.3.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[32m17.
|
|
14
|
-
[32mESM[39m [1mdist/index.mjs.map [22m[32m36.
|
|
15
|
-
[32mESM[39m ⚡️ Build success in
|
|
16
|
-
[32mCJS[39m [1mdist/index.js [22m[32m18.
|
|
13
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m17.77 KB[39m
|
|
14
|
+
[32mESM[39m [1mdist/index.mjs.map [22m[32m36.81 KB[39m
|
|
15
|
+
[32mESM[39m ⚡️ Build success in 188ms
|
|
16
|
+
[32mCJS[39m [1mdist/index.js [22m[32m18.86 KB[39m
|
|
17
17
|
[32mCJS[39m [1mdist/index.js.map [22m[32m36.81 KB[39m
|
|
18
|
-
[32mCJS[39m ⚡️ Build success in
|
|
18
|
+
[32mCJS[39m ⚡️ Build success in 188ms
|
|
19
19
|
[34mDTS[39m Build start
|
|
20
|
-
[32mDTS[39m ⚡️ Build success in
|
|
20
|
+
[32mDTS[39m ⚡️ Build success in 18079ms
|
|
21
21
|
[32mDTS[39m [1mdist/index.d.mts [22m[32m4.99 KB[39m
|
|
22
22
|
[32mDTS[39m [1mdist/index.d.ts [22m[32m4.99 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @objectstack/plugin-reports
|
|
2
2
|
|
|
3
|
+
## 9.3.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [1ada658]
|
|
8
|
+
- Updated dependencies [3219191]
|
|
9
|
+
- Updated dependencies [290f631]
|
|
10
|
+
- Updated dependencies [50b7b47]
|
|
11
|
+
- Updated dependencies [f15d6f6]
|
|
12
|
+
- Updated dependencies [f8684ea]
|
|
13
|
+
- Updated dependencies [c802327]
|
|
14
|
+
- Updated dependencies [b4765be]
|
|
15
|
+
- @objectstack/spec@9.3.0
|
|
16
|
+
- @objectstack/platform-objects@9.3.0
|
|
17
|
+
- @objectstack/core@9.3.0
|
|
18
|
+
|
|
3
19
|
## 9.2.0
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
package/dist/index.js
CHANGED
|
@@ -192,7 +192,7 @@ var ReportService = class {
|
|
|
192
192
|
const rows = await this.engine.find("sys_saved_report", {
|
|
193
193
|
filter: f,
|
|
194
194
|
limit: 500,
|
|
195
|
-
orderBy: [{ field: "updated_at",
|
|
195
|
+
orderBy: [{ field: "updated_at", order: "desc" }],
|
|
196
196
|
context: SYSTEM_CTX
|
|
197
197
|
});
|
|
198
198
|
return Array.isArray(rows) ? rows.map(rowFromSaved) : [];
|
|
@@ -324,7 +324,7 @@ var ReportService = class {
|
|
|
324
324
|
const rows = await this.engine.find("sys_report_schedule", {
|
|
325
325
|
filter: f,
|
|
326
326
|
limit: 500,
|
|
327
|
-
orderBy: [{ field: "next_run_at",
|
|
327
|
+
orderBy: [{ field: "next_run_at", order: "asc" }],
|
|
328
328
|
context: SYSTEM_CTX
|
|
329
329
|
});
|
|
330
330
|
return Array.isArray(rows) ? rows.map(rowFromSchedule) : [];
|
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';\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, roles: [], 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', direction: '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 roles: context.roles ?? [],\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 nextRun = new Date(now.getTime() + interval * 60_000).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: input.cronExpression ?? null,\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', direction: '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 filename: `${(schedule.name ?? report.name).replace(/[^\\w.-]+/g, '_')}-${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 private async advanceSchedule(schedule: ReportSchedule, ranAt: string): Promise<void> {\n const interval = schedule.interval_minutes ?? DEFAULT_INTERVAL_MIN;\n const nextRun = new Date(this.clock.now().getTime() + interval * 60_000).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';\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, roles: [], 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 roles: context.roles ?? [],\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 nextRun = new Date(now.getTime() + interval * 60_000).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: input.cronExpression ?? null,\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 filename: `${(schedule.name ?? report.name).replace(/[^\\w.-]+/g, '_')}-${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 private async advanceSchedule(schedule: ReportSchedule, ranAt: string): Promise<void> {\n const interval = schedule.interval_minutes ?? DEFAULT_INTERVAL_MIN;\n const nextRun = new Date(this.clock.now().getTime() + interval * 60_000).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"]}
|
package/dist/index.mjs
CHANGED
|
@@ -164,7 +164,7 @@ var ReportService = class {
|
|
|
164
164
|
const rows = await this.engine.find("sys_saved_report", {
|
|
165
165
|
filter: f,
|
|
166
166
|
limit: 500,
|
|
167
|
-
orderBy: [{ field: "updated_at",
|
|
167
|
+
orderBy: [{ field: "updated_at", order: "desc" }],
|
|
168
168
|
context: SYSTEM_CTX
|
|
169
169
|
});
|
|
170
170
|
return Array.isArray(rows) ? rows.map(rowFromSaved) : [];
|
|
@@ -296,7 +296,7 @@ var ReportService = class {
|
|
|
296
296
|
const rows = await this.engine.find("sys_report_schedule", {
|
|
297
297
|
filter: f,
|
|
298
298
|
limit: 500,
|
|
299
|
-
orderBy: [{ field: "next_run_at",
|
|
299
|
+
orderBy: [{ field: "next_run_at", order: "asc" }],
|
|
300
300
|
context: SYSTEM_CTX
|
|
301
301
|
});
|
|
302
302
|
return Array.isArray(rows) ? rows.map(rowFromSchedule) : [];
|
package/dist/index.mjs.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';\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, roles: [], 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', direction: '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 roles: context.roles ?? [],\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 nextRun = new Date(now.getTime() + interval * 60_000).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: input.cronExpression ?? null,\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', direction: '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 filename: `${(schedule.name ?? report.name).replace(/[^\\w.-]+/g, '_')}-${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 private async advanceSchedule(schedule: ReportSchedule, ranAt: string): Promise<void> {\n const interval = schedule.interval_minutes ?? DEFAULT_INTERVAL_MIN;\n const nextRun = new Date(this.clock.now().getTime() + interval * 60_000).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":["SysSavedReport","SysReportSchedule"]}
|
|
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';\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, roles: [], 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 roles: context.roles ?? [],\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 nextRun = new Date(now.getTime() + interval * 60_000).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: input.cronExpression ?? null,\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 filename: `${(schedule.name ?? report.name).replace(/[^\\w.-]+/g, '_')}-${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 private async advanceSchedule(schedule: ReportSchedule, ranAt: string): Promise<void> {\n const interval = schedule.interval_minutes ?? DEFAULT_INTERVAL_MIN;\n const nextRun = new Date(this.clock.now().getTime() + interval * 60_000).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":["SysSavedReport","SysReportSchedule"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/plugin-reports",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.3.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Saved reports + scheduled email digests for ObjectStack — sys_saved_report + sys_report_schedule + IReportService.",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@objectstack/core": "9.
|
|
17
|
-
"@objectstack/platform-objects": "9.
|
|
18
|
-
"@objectstack/spec": "9.
|
|
16
|
+
"@objectstack/core": "9.3.0",
|
|
17
|
+
"@objectstack/platform-objects": "9.3.0",
|
|
18
|
+
"@objectstack/spec": "9.3.0"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@types/node": "^25.9.2",
|
|
@@ -24,12 +24,15 @@ function makeFakeEngine() {
|
|
|
24
24
|
async find(object: string, options?: any) {
|
|
25
25
|
const rows = ensure(object).filter(r => matches(r, options?.filter ?? options?.where));
|
|
26
26
|
if (options?.orderBy?.[0]) {
|
|
27
|
-
|
|
27
|
+
// Canonical SortNode key only (spec/data/query.zod.ts): the real
|
|
28
|
+
// engine strips an unknown `direction:` key and defaults to asc, so
|
|
29
|
+
// the mock must too — honoring both keys masks wrong-key sorts.
|
|
30
|
+
const { field, order } = options.orderBy[0];
|
|
28
31
|
rows.sort((a, b) => {
|
|
29
32
|
const av = a[field]; const bv = b[field];
|
|
30
33
|
if (av === bv) return 0;
|
|
31
34
|
const cmp = av > bv ? 1 : -1;
|
|
32
|
-
return
|
|
35
|
+
return order === 'desc' ? -cmp : cmp;
|
|
33
36
|
});
|
|
34
37
|
}
|
|
35
38
|
return rows.slice(0, options?.limit ?? 1000);
|
|
@@ -163,6 +166,18 @@ describe('ReportService', () => {
|
|
|
163
166
|
expect(leads[0].name).toBe('A');
|
|
164
167
|
});
|
|
165
168
|
|
|
169
|
+
it('listReports: most recently updated first', async () => {
|
|
170
|
+
// Regression: the query sorted with the non-canonical `direction: 'desc'`
|
|
171
|
+
// key, which SortNode strips — so it sorted ascending (oldest first).
|
|
172
|
+
engine._tables['sys_saved_report'] = [
|
|
173
|
+
{ id: 'r_old', name: 'Old', object_name: 'lead', query_json: '{}', updated_at: '2026-01-01T00:00:00Z' },
|
|
174
|
+
{ id: 'r_new', name: 'New', object_name: 'lead', query_json: '{}', updated_at: '2026-03-01T00:00:00Z' },
|
|
175
|
+
{ id: 'r_mid', name: 'Mid', object_name: 'lead', query_json: '{}', updated_at: '2026-02-01T00:00:00Z' },
|
|
176
|
+
];
|
|
177
|
+
const rows = await svc.listReports({ object: 'lead' }, CTX);
|
|
178
|
+
expect(rows.map(r => r.id)).toEqual(['r_new', 'r_mid', 'r_old']);
|
|
179
|
+
});
|
|
180
|
+
|
|
166
181
|
it('getReport: returns null on miss', async () => {
|
|
167
182
|
expect(await svc.getReport('nope', CTX)).toBeNull();
|
|
168
183
|
});
|
package/src/report-service.ts
CHANGED
|
@@ -235,7 +235,7 @@ export class ReportService implements IReportService {
|
|
|
235
235
|
if (filter?.object) f.object_name = filter.object;
|
|
236
236
|
if (filter?.ownerId) f.owner_id = filter.ownerId;
|
|
237
237
|
const rows = await this.engine.find('sys_saved_report', {
|
|
238
|
-
filter: f, limit: 500, orderBy: [{ field: 'updated_at',
|
|
238
|
+
filter: f, limit: 500, orderBy: [{ field: 'updated_at', order: 'desc' }], context: SYSTEM_CTX,
|
|
239
239
|
});
|
|
240
240
|
return Array.isArray(rows) ? rows.map(rowFromSaved) : [];
|
|
241
241
|
}
|
|
@@ -376,7 +376,7 @@ export class ReportService implements IReportService {
|
|
|
376
376
|
const f: any = {};
|
|
377
377
|
if (filter?.reportId) f.report_id = filter.reportId;
|
|
378
378
|
const rows = await this.engine.find('sys_report_schedule', {
|
|
379
|
-
filter: f, limit: 500, orderBy: [{ field: 'next_run_at',
|
|
379
|
+
filter: f, limit: 500, orderBy: [{ field: 'next_run_at', order: 'asc' }], context: SYSTEM_CTX,
|
|
380
380
|
});
|
|
381
381
|
return Array.isArray(rows) ? rows.map(rowFromSchedule) : [];
|
|
382
382
|
}
|