From 2e8a24065464127b28d15a8f8d2a5e0778ec3453 Mon Sep 17 00:00:00 2001 From: OpenClaw Subagent Date: Thu, 19 Mar 2026 08:12:30 +0100 Subject: [PATCH] fix: remove unnecessary 'as any' casts and add proper types to templates - Replace (req as any).requestId with req.requestId in index.ts, recover.ts, email-change.ts - Replace (err as any).status with proper Record narrowing in error handler - Add InvoiceData, ReceiptData, ContactInfo, InvoiceItem, ReceiptItem interfaces to templates.ts - Replace all 'any' params in template functions with proper types - Add type-safety regression tests (grep-based) - 818 tests pass, tsc --noEmit: 0 errors --- src/__tests__/templates.test.ts | 4 +- src/__tests__/type-safety.test.ts | 31 +++++++++++ src/index.ts | 4 +- src/routes/email-change.ts | 4 +- src/routes/recover.ts | 4 +- src/routes/templates.ts | 4 +- src/services/templates.ts | 90 +++++++++++++++++++++++-------- 7 files changed, 109 insertions(+), 32 deletions(-) create mode 100644 src/__tests__/type-safety.test.ts diff --git a/src/__tests__/templates.test.ts b/src/__tests__/templates.test.ts index 9c5147b..cfb73bc 100644 --- a/src/__tests__/templates.test.ts +++ b/src/__tests__/templates.test.ts @@ -1,10 +1,10 @@ import { describe, it, expect } from "vitest"; -import { renderTemplate, templates } from "../services/templates.js"; +import { renderTemplate, templates, TemplateData } from "../services/templates.js"; // Access esc via rendering — test that HTML entities are escaped in output describe("Template rendering", () => { it("throws for unknown template", () => { - expect(() => renderTemplate("nonexistent", {})).toThrow("not found"); + expect(() => renderTemplate("nonexistent", {} as TemplateData)).toThrow("not found"); }); it("invoice renders with correct totals", () => { diff --git a/src/__tests__/type-safety.test.ts b/src/__tests__/type-safety.test.ts new file mode 100644 index 0000000..14eabf5 --- /dev/null +++ b/src/__tests__/type-safety.test.ts @@ -0,0 +1,31 @@ +import { describe, it, expect } from "vitest"; +import { execSync } from "child_process"; +import path from "path"; + +describe("Type safety", () => { + const srcDir = path.resolve(__dirname, ".."); + + it("should not use (req as any).requestId in production code — Express.Request is already augmented", () => { + const result = execSync( + `grep -r "(req as any)\\.requestId" --include="*.ts" --exclude-dir=__tests__ "${srcDir}" || true`, + { encoding: "utf-8" } + ); + expect(result.trim()).toBe(""); + }); + + it("should not use (err as any) — use proper type narrowing instead", () => { + const result = execSync( + `grep -r "(err as any)" --include="*.ts" "${srcDir}" --exclude-dir=__tests__ || true`, + { encoding: "utf-8" } + ); + expect(result.trim()).toBe(""); + }); + + it("should not use any types in templates.ts function parameters", () => { + const result = execSync( + `grep -E "\\(d: any\\)|\\(data: any\\)|\\(item: any\\)" "${srcDir}/services/templates.ts" || true`, + { encoding: "utf-8" } + ); + expect(result.trim()).toBe(""); + }); +}); diff --git a/src/index.ts b/src/index.ts index b786f79..766c313 100644 --- a/src/index.ts +++ b/src/index.ts @@ -223,10 +223,10 @@ app.use((req, res) => { // Global error handler — must be after all routes app.use((err: unknown, req: Request, res: Response, _next: NextFunction) => { - const reqId = (req as any).requestId || "unknown"; + const reqId = req.requestId || "unknown"; // Check if this is a JSON parse error from express.json() - if (err instanceof SyntaxError && 'status' in err && (err as any).status === 400 && 'body' in err) { + if (err instanceof SyntaxError && 'status' in err && (err as Record).status === 400 && 'body' in err) { logger.warn({ err, requestId: reqId, method: req.method, path: req.path }, "Invalid JSON body"); if (!res.headersSent) { res.status(400).json({ error: "Invalid JSON in request body" }); diff --git a/src/routes/email-change.ts b/src/routes/email-change.ts index c64e4a8..28961e1 100644 --- a/src/routes/email-change.ts +++ b/src/routes/email-change.ts @@ -115,7 +115,7 @@ router.post("/", emailChangeLimiter, async (req: Request, res: Response) => { res.json({ status: "verification_sent", message: "A verification code has been sent to your new email address." }); } catch (err: unknown) { - const reqId = (req as any).requestId || "unknown"; + const reqId = req.requestId || "unknown"; logger.error({ err, requestId: reqId }, "Unhandled error in POST /email-change"); res.status(500).json({ error: "Internal server error" }); } @@ -207,7 +207,7 @@ router.post("/verify", async (req: Request, res: Response) => { break; } } catch (err: unknown) { - const reqId = (req as any).requestId || "unknown"; + const reqId = req.requestId || "unknown"; logger.error({ err, requestId: reqId }, "Unhandled error in POST /email-change/verify"); res.status(500).json({ error: "Internal server error" }); } diff --git a/src/routes/recover.ts b/src/routes/recover.ts index 6419f94..722406b 100644 --- a/src/routes/recover.ts +++ b/src/routes/recover.ts @@ -94,7 +94,7 @@ router.post("/", recoverLimiter, async (req: Request, res: Response) => { res.json({ status: "recovery_sent", message: "If an account exists for this email, a verification code has been sent." }); } catch (err: unknown) { - const reqId = (req as any).requestId || "unknown"; + const reqId = req.requestId || "unknown"; logger.error({ err, requestId: reqId }, "Unhandled error in POST /recover"); res.status(500).json({ error: "Internal server error" }); } @@ -210,7 +210,7 @@ router.post("/verify", recoverLimiter, async (req: Request, res: Response) => { break; } } catch (err: unknown) { - const reqId = (req as any).requestId || "unknown"; + const reqId = req.requestId || "unknown"; logger.error({ err, requestId: reqId }, "Unhandled error in POST /recover/verify"); res.status(500).json({ error: "Internal server error" }); } diff --git a/src/routes/templates.ts b/src/routes/templates.ts index 81e61e3..355ea5b 100644 --- a/src/routes/templates.ts +++ b/src/routes/templates.ts @@ -1,7 +1,7 @@ import { Router, Request, Response } from "express"; import { renderPdf } from "../services/browser.js"; import logger from "../services/logger.js"; -import { templates, renderTemplate } from "../services/templates.js"; +import { templates, renderTemplate, TemplateData } from "../services/templates.js"; import { sanitizeFilename } from "../utils/sanitize.js"; import { validatePdfOptions } from "../utils/pdf-options.js"; @@ -165,7 +165,7 @@ templatesRouter.post("/:id/render", async (req: Request, res: Response) => { } const sanitizedPdf = { format: "A4" as const, ...validation.sanitized }; - const html = renderTemplate(id, data); + const html = renderTemplate(id, data as TemplateData); const { pdf, durationMs } = await renderPdf(html, sanitizedPdf); const filename = sanitizeFilename(data._filename || `${id}.pdf`); diff --git a/src/services/templates.ts b/src/services/templates.ts index 388f3dc..d23d117 100644 --- a/src/services/templates.ts +++ b/src/services/templates.ts @@ -1,8 +1,52 @@ +export interface ContactInfo { + name: string; + address?: string; + email?: string; + phone?: string; + vatId?: string; +} + +export interface InvoiceItem { + description: string; + quantity?: number; + unitPrice?: number; + taxRate?: number; +} + +export interface InvoiceData { + invoiceNumber: string; + date: string; + dueDate?: string; + from: ContactInfo; + to: ContactInfo; + items: InvoiceItem[]; + currency?: string; + notes?: string; + paymentDetails?: string; +} + +export interface ReceiptItem { + description: string; + amount?: number; +} + +export interface ReceiptData { + receiptNumber: string; + date: string; + from: ContactInfo; + to?: ContactInfo; + items: ReceiptItem[]; + currency?: string; + paymentMethod?: string; +} + +export type TemplateData = InvoiceData | ReceiptData; + export interface TemplateDefinition { name: string; description: string; fields: { name: string; type: string; required: boolean; description: string }[]; - render: (data: any) => string; + render: (data: TemplateData) => string; } export const templates: Record = { @@ -47,14 +91,15 @@ function esc(s: string): string { .replace(/'/g, "'"); } -function renderInvoice(d: any): string { - const cur = esc(d.currency || "€"); - const items = d.items || []; +function renderInvoice(d: TemplateData): string { + const inv = d as InvoiceData; + const cur = esc(inv.currency || "€"); + const items = inv.items || []; let subtotal = 0; let totalTax = 0; const rows = items - .map((item: any) => { + .map((item: InvoiceItem) => { const qty = Number(item.quantity) || 1; const price = Number(item.unitPrice) || 0; const taxRate = Number(item.taxRate) || 0; @@ -73,8 +118,8 @@ function renderInvoice(d: any): string { .join(""); const total = subtotal + totalTax; - const from = d.from || {}; - const to = d.to || {}; + const from = inv.from || ({} as ContactInfo); + const to = inv.to || ({} as ContactInfo); return `