fase(15): reporting module with pdf generation

This commit is contained in:
debian
2026-03-06 05:57:05 -05:00
parent 3ff36f0b6a
commit cffa1aeea9
64 changed files with 3462 additions and 87 deletions

View File

@@ -0,0 +1,33 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GenerateReportCommand = void 0;
const Result_1 = require("../../../../shared/domain/Result");
const Report_1 = require("../../domain/entities/Report");
const ReportFormat_1 = require("../../domain/value-objects/ReportFormat");
class GenerateReportCommand {
constructor(reportRepository, eventBus) {
this.reportRepository = reportRepository;
this.eventBus = eventBus;
}
async execute(request) {
let format;
try {
format = ReportFormat_1.ReportFormat.fromString(request.format);
}
catch {
return (0, Result_1.Err)(`Invalid format: ${request.format}`);
}
const report = Report_1.Report.create({
title: request.title,
format,
filters: request.filters ?? {},
});
await this.reportRepository.save(report);
const events = report.clearEvents();
for (const event of events) {
await this.eventBus.publish(event);
}
return (0, Result_1.Ok)({ reportId: report.id.toString(), status: report.status.value });
}
}
exports.GenerateReportCommand = GenerateReportCommand;

View File

@@ -0,0 +1,59 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Report = void 0;
const AggregateRoot_1 = require("../../../../shared/domain/AggregateRoot");
const UniqueId_1 = require("../../../../shared/domain/UniqueId");
const ReportStatus_1 = require("../value-objects/ReportStatus");
const ReportRequested_1 = require("../events/ReportRequested");
const ReportGenerated_1 = require("../events/ReportGenerated");
const ReportFailed_1 = require("../events/ReportFailed");
class Report extends AggregateRoot_1.AggregateRoot {
static create(props, id) {
const reportId = id ?? UniqueId_1.UniqueId.create();
const report = new Report({
...props,
status: ReportStatus_1.ReportStatus.pending(),
totalFindings: 0,
createdAt: new Date(),
}, reportId);
report.addDomainEvent(new ReportRequested_1.ReportRequested(reportId.toString(), {
title: props.title,
format: props.format.value,
filters: props.filters,
}));
return report;
}
static reconstitute(props, id) {
return new Report(props, id);
}
get title() { return this.props.title; }
get format() { return this.props.format; }
get status() { return this.props.status; }
get filters() { return this.props.filters; }
get filePath() { return this.props.filePath; }
get errorMessage() { return this.props.errorMessage; }
get totalFindings() { return this.props.totalFindings; }
get createdAt() { return this.props.createdAt; }
get completedAt() { return this.props.completedAt; }
markGenerating() {
this.props.status = ReportStatus_1.ReportStatus.generating();
}
markReady(filePath, totalFindings) {
this.props.status = ReportStatus_1.ReportStatus.ready();
this.props.filePath = filePath;
this.props.totalFindings = totalFindings;
this.props.completedAt = new Date();
this.addDomainEvent(new ReportGenerated_1.ReportGenerated(this.id.toString(), {
filePath,
totalFindings,
format: this.props.format.value,
}));
}
markFailed(errorMessage) {
this.props.status = ReportStatus_1.ReportStatus.failed();
this.props.errorMessage = errorMessage;
this.props.completedAt = new Date();
this.addDomainEvent(new ReportFailed_1.ReportFailed(this.id.toString(), { errorMessage }));
}
}
exports.Report = Report;

View File

@@ -0,0 +1,14 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ReportFailed = void 0;
const crypto_1 = require("crypto");
class ReportFailed {
constructor(aggregateId, payload) {
this.aggregateId = aggregateId;
this.payload = payload;
this.eventId = (0, crypto_1.randomUUID)();
this.eventName = 'reporting.report_failed';
this.occurredOn = new Date();
}
}
exports.ReportFailed = ReportFailed;

View File

@@ -0,0 +1,14 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ReportGenerated = void 0;
const crypto_1 = require("crypto");
class ReportGenerated {
constructor(aggregateId, payload) {
this.aggregateId = aggregateId;
this.payload = payload;
this.eventId = (0, crypto_1.randomUUID)();
this.eventName = 'reporting.report_generated';
this.occurredOn = new Date();
}
}
exports.ReportGenerated = ReportGenerated;

View File

@@ -0,0 +1,14 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ReportRequested = void 0;
const crypto_1 = require("crypto");
class ReportRequested {
constructor(aggregateId, payload) {
this.aggregateId = aggregateId;
this.payload = payload;
this.eventId = (0, crypto_1.randomUUID)();
this.eventName = 'reporting.report_requested';
this.occurredOn = new Date();
}
}
exports.ReportRequested = ReportRequested;

View File

@@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

View File

@@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

View File

@@ -0,0 +1,14 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DateRange = void 0;
const ValueObject_1 = require("../../../../shared/domain/ValueObject");
class DateRange extends ValueObject_1.ValueObject {
get startDate() { return this.props.startDate; }
get endDate() { return this.props.endDate; }
static create(startDate, endDate) {
if (startDate > endDate)
throw new Error('startDate must be before endDate');
return new DateRange({ startDate, endDate });
}
}
exports.DateRange = DateRange;

View File

@@ -0,0 +1,17 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ReportFormat = void 0;
const ValueObject_1 = require("../../../../shared/domain/ValueObject");
class ReportFormat extends ValueObject_1.ValueObject {
get value() { return this.props.value; }
static html() { return new ReportFormat({ value: 'html' }); }
static json() { return new ReportFormat({ value: 'json' }); }
static pdf() { return new ReportFormat({ value: 'pdf' }); }
static fromString(s) {
if (s === 'html' || s === 'json' || s === 'pdf') {
return new ReportFormat({ value: s });
}
throw new Error(`Invalid report format: ${s}`);
}
}
exports.ReportFormat = ReportFormat;

View File

@@ -0,0 +1,18 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ReportStatus = void 0;
const ValueObject_1 = require("../../../../shared/domain/ValueObject");
class ReportStatus extends ValueObject_1.ValueObject {
get value() { return this.props.value; }
static pending() { return new ReportStatus({ value: 'pending' }); }
static generating() { return new ReportStatus({ value: 'generating' }); }
static ready() { return new ReportStatus({ value: 'ready' }); }
static failed() { return new ReportStatus({ value: 'failed' }); }
static fromString(s) {
if (s === 'pending' || s === 'generating' || s === 'ready' || s === 'failed') {
return new ReportStatus({ value: s });
}
throw new Error(`Invalid report status: ${s}`);
}
}
exports.ReportStatus = ReportStatus;

View File

@@ -0,0 +1,138 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.HTMLReportGenerator = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
class HTMLReportGenerator {
async generate(report, findings) {
const outputDir = path.join(process.cwd(), 'reports', report.id.toString());
fs.mkdirSync(outputDir, { recursive: true });
const severityCounts = { critical: 0, high: 0, medium: 0, low: 0 };
for (const f of findings) {
const sev = f.severity.value;
severityCounts[sev] = (severityCounts[sev] ?? 0) + 1;
}
const findingsHtml = findings.map(f => `
<div class="finding severity-${f.severity.value}">
<div class="finding-header">
<span class="badge badge-${f.severity.value}">${f.severity.value.toUpperCase()}</span>
<span class="finding-type">${f.type.value}</span>
<span class="finding-status">${f.status.value}</span>
</div>
<p class="finding-desc">${escapeHtml(f.description)}</p>
<small class="finding-meta">
Session: ${f.sessionId} &nbsp;·&nbsp;
${new Date(f.createdAt).toLocaleString()}
</small>
</div>
`).join('\n');
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${escapeHtml(report.title)}</title>
<style>
body { font-family: system-ui, -apple-system, sans-serif; max-width: 960px; margin: 0 auto; padding: 2rem; color: #1a1a1a; }
h1 { font-size: 1.75rem; border-bottom: 2px solid #e2e8f0; padding-bottom: 0.5rem; }
.meta { color: #64748b; font-size: 0.875rem; margin-bottom: 2rem; }
.stats { display: flex; gap: 1rem; margin: 1.5rem 0; }
.stat-card { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 1rem 1.5rem; min-width: 100px; text-align: center; }
.stat-card .value { font-size: 2rem; font-weight: 700; }
.stat-card .label { font-size: 0.75rem; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em; }
.finding { border: 1px solid #e2e8f0; border-radius: 8px; padding: 1rem; margin-bottom: 0.75rem; }
.finding-header { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; }
.badge { padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.7rem; font-weight: 700; }
.badge-critical { background: #fee2e2; color: #dc2626; }
.badge-high { background: #ffedd5; color: #ea580c; }
.badge-medium { background: #fef9c3; color: #ca8a04; }
.badge-low { background: #dbeafe; color: #2563eb; }
.finding-type { font-family: monospace; font-size: 0.8rem; color: #475569; }
.finding-status { margin-left: auto; font-size: 0.75rem; color: #64748b; }
.finding-desc { margin: 0.25rem 0; font-size: 0.9rem; }
.finding-meta { color: #94a3b8; font-size: 0.75rem; }
footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid #e2e8f0; color: #94a3b8; font-size: 0.75rem; text-align: center; }
</style>
</head>
<body>
<h1>${escapeHtml(report.title)}</h1>
<div class="meta">
Generated by ABE &nbsp;·&nbsp; ${new Date().toLocaleString()}
${report.filters.sessionId ? `&nbsp;·&nbsp; Session: ${report.filters.sessionId}` : ''}
</div>
<div class="stats">
<div class="stat-card">
<div class="value">${findings.length}</div>
<div class="label">Total</div>
</div>
<div class="stat-card">
<div class="value" style="color:#dc2626">${severityCounts['critical'] ?? 0}</div>
<div class="label">Critical</div>
</div>
<div class="stat-card">
<div class="value" style="color:#ea580c">${severityCounts['high'] ?? 0}</div>
<div class="label">High</div>
</div>
<div class="stat-card">
<div class="value" style="color:#ca8a04">${severityCounts['medium'] ?? 0}</div>
<div class="label">Medium</div>
</div>
<div class="stat-card">
<div class="value" style="color:#2563eb">${severityCounts['low'] ?? 0}</div>
<div class="label">Low</div>
</div>
</div>
<h2>Findings (${findings.length})</h2>
${findings.length === 0 ? '<p style="color:#64748b">No findings match the selected filters.</p>' : findingsHtml}
<footer>Generated by ABE — Autonomous Bug Explorer</footer>
</body>
</html>`;
const filePath = path.join(outputDir, `report.html`);
fs.writeFileSync(filePath, html, 'utf8');
return filePath;
}
}
exports.HTMLReportGenerator = HTMLReportGenerator;
function escapeHtml(s) {
return s
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}

View File

@@ -0,0 +1,88 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.JSONReportGenerator = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
class JSONReportGenerator {
async generate(report, findings) {
const outputDir = path.join(process.cwd(), 'reports', report.id.toString());
fs.mkdirSync(outputDir, { recursive: true });
const data = {
reportId: report.id.toString(),
title: report.title,
generatedAt: new Date().toISOString(),
filters: report.filters,
summary: {
total: findings.length,
bySeverity: buildSeverityCount(findings),
byStatus: buildStatusCount(findings),
},
findings: findings.map(f => ({
id: f.id.toString(),
sessionId: f.sessionId,
type: f.type.value,
severity: f.severity.value,
description: f.description,
status: f.status.value,
browser: f.browser,
createdAt: f.createdAt.toISOString(),
resolvedAt: f.resolvedAt?.toISOString() ?? null,
evidence: f.evidence.toJSON(),
actionTraceLength: f.actionTrace.length,
})),
};
const filePath = path.join(outputDir, 'report.json');
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
return filePath;
}
}
exports.JSONReportGenerator = JSONReportGenerator;
function buildSeverityCount(findings) {
const counts = {};
for (const f of findings) {
const s = f.severity.value;
counts[s] = (counts[s] ?? 0) + 1;
}
return counts;
}
function buildStatusCount(findings) {
const counts = {};
for (const f of findings) {
const s = f.status.value;
counts[s] = (counts[s] ?? 0) + 1;
}
return counts;
}

View File

@@ -0,0 +1,81 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.PDFReportGenerator = void 0;
const path = __importStar(require("path"));
const fs = __importStar(require("fs"));
const HTMLReportGenerator_1 = require("./HTMLReportGenerator");
/**
* PDF report generator — uses Playwright to render the HTML report to PDF.
* Requires Playwright + Chromium to be installed.
*/
class PDFReportGenerator {
constructor() {
this.htmlGenerator = new HTMLReportGenerator_1.HTMLReportGenerator();
}
async generate(report, findings) {
// First generate the HTML version
const htmlPath = await this.htmlGenerator.generate(report, findings);
const outputDir = path.dirname(htmlPath);
const pdfPath = path.join(outputDir, 'report.pdf');
// Use Playwright to convert HTML to PDF
let chromium;
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const pw = require('playwright');
chromium = pw.chromium;
}
catch {
throw new Error('Playwright not available — install playwright to generate PDF reports');
}
const browser = await chromium.launch({ headless: true });
try {
const page = await browser.newPage();
const htmlContent = fs.readFileSync(htmlPath, 'utf8');
await page.setContent(htmlContent, { waitUntil: 'networkidle' });
await page.pdf({
path: pdfPath,
format: 'A4',
printBackground: true,
margin: { top: '1cm', bottom: '1cm', left: '1cm', right: '1cm' },
});
}
finally {
await browser.close();
}
return pdfPath;
}
}
exports.PDFReportGenerator = PDFReportGenerator;

View File

@@ -0,0 +1,134 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.createReportingRouter = createReportingRouter;
const express_1 = require("express");
const path = __importStar(require("path"));
const fs = __importStar(require("fs"));
const ReportWorker_1 = require("../../../../jobs/workers/ReportWorker");
function createReportingRouter(deps) {
const router = (0, express_1.Router)();
// POST /api/reports — create and enqueue report
router.post('/', async (req, res) => {
const { title, format, filters } = req.body;
if (!title || !format) {
res.status(400).json({ error: 'title and format are required' });
return;
}
const result = await deps.generateReport.execute({
title,
format: format,
filters: filters
? {
sessionId: filters.sessionId,
startDate: filters.startDate ? new Date(filters.startDate) : undefined,
endDate: filters.endDate ? new Date(filters.endDate) : undefined,
severity: filters.severity,
}
: undefined,
});
if (!result.ok) {
res.status(400).json({ error: result.error });
return;
}
// Enqueue background job
await deps.jobQueue.enqueue(ReportWorker_1.REPORT_JOB_TYPE, {
reportId: result.value.reportId,
format: format,
filters: filters,
});
res.status(201).json(result.value);
});
// GET /api/reports — list all reports
router.get('/', async (_req, res) => {
const reports = await deps.reportRepository.findAll();
res.json(reports.map(r => ({
id: r.id.toString(),
title: r.title,
format: r.format.value,
status: r.status.value,
totalFindings: r.totalFindings,
createdAt: r.createdAt.toISOString(),
completedAt: r.completedAt?.toISOString() ?? null,
})));
});
// GET /api/reports/:id — report detail
router.get('/:id', async (req, res) => {
const report = await deps.reportRepository.findById(req.params['id']);
if (!report) {
res.status(404).json({ error: 'Report not found' });
return;
}
res.json({
id: report.id.toString(),
title: report.title,
format: report.format.value,
status: report.status.value,
filters: report.filters,
totalFindings: report.totalFindings,
errorMessage: report.errorMessage,
createdAt: report.createdAt.toISOString(),
completedAt: report.completedAt?.toISOString() ?? null,
});
});
// GET /api/reports/:id/download — download the generated file
router.get('/:id/download', async (req, res) => {
const report = await deps.reportRepository.findById(req.params['id']);
if (!report) {
res.status(404).json({ error: 'Report not found' });
return;
}
if (report.status.value !== 'ready' || !report.filePath) {
res.status(409).json({ error: 'Report is not ready yet', status: report.status.value });
return;
}
if (!fs.existsSync(report.filePath)) {
res.status(410).json({ error: 'Report file no longer exists' });
return;
}
const ext = path.extname(report.filePath);
const contentTypes = {
'.html': 'text/html',
'.json': 'application/json',
'.pdf': 'application/pdf',
};
const contentType = contentTypes[ext] ?? 'application/octet-stream';
const filename = `report-${report.id.toString()}${ext}`;
res.setHeader('Content-Type', contentType);
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
fs.createReadStream(report.filePath).pipe(res);
});
return router;
}

View File

@@ -0,0 +1,85 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.KyselyReportRepository = void 0;
const Report_1 = require("../../domain/entities/Report");
const UniqueId_1 = require("../../../../shared/domain/UniqueId");
const ReportFormat_1 = require("../../domain/value-objects/ReportFormat");
const ReportStatus_1 = require("../../domain/value-objects/ReportStatus");
class KyselyReportRepository {
constructor(db) {
this.db = db;
}
async save(report) {
const row = {
id: report.id.toString(),
title: report.title,
format: report.format.value,
status: report.status.value,
filters_json: JSON.stringify(report.filters),
file_path: report.filePath ?? null,
error_message: report.errorMessage ?? null,
total_findings: report.totalFindings,
created_at: report.createdAt.getTime(),
completed_at: report.completedAt ? report.completedAt.getTime() : null,
};
await this.db.insertInto('reports').values(row).execute();
}
async findById(id) {
const row = await this.db
.selectFrom('reports')
.selectAll()
.where('id', '=', id)
.executeTakeFirst();
return row ? this.toDomain(row) : undefined;
}
async findAll() {
const rows = await this.db
.selectFrom('reports')
.selectAll()
.orderBy('created_at', 'desc')
.execute();
return rows.map(r => this.toDomain(r));
}
async update(report) {
await this.db
.updateTable('reports')
.set({
status: report.status.value,
file_path: report.filePath ?? null,
error_message: report.errorMessage ?? null,
total_findings: report.totalFindings,
completed_at: report.completedAt ? report.completedAt.getTime() : null,
})
.where('id', '=', report.id.toString())
.execute();
}
toDomain(row) {
const filters = this.parseJson(row.filters_json, {});
const props = {
title: row.title,
format: ReportFormat_1.ReportFormat.fromString(row.format),
status: ReportStatus_1.ReportStatus.fromString(row.status),
filters: {
sessionId: filters.sessionId,
startDate: filters.startDate ? new Date(filters.startDate) : undefined,
endDate: filters.endDate ? new Date(filters.endDate) : undefined,
severity: filters.severity,
},
filePath: row.file_path ?? undefined,
errorMessage: row.error_message ?? undefined,
totalFindings: row.total_findings,
createdAt: new Date(row.created_at),
completedAt: row.completed_at ? new Date(row.completed_at) : undefined,
};
return Report_1.Report.reconstitute(props, UniqueId_1.UniqueId.from(row.id));
}
parseJson(json, fallback) {
try {
return JSON.parse(json);
}
catch {
return fallback;
}
}
}
exports.KyselyReportRepository = KyselyReportRepository;