import type { IncomingMessage, ServerResponse } from "node:http"; import type { Plugin, Connect } from "vite"; import { randomUUID } from "node:crypto"; import { readJson, writeJson } from "./jsonStore.ts"; import type { Contributions, History, Movement, Portfolio, RecurringPlan, Settings } from "./types.ts"; function send(res: ServerResponse, status: number, body: unknown): void { const payload = JSON.stringify(body); res.statusCode = status; res.setHeader("Content-Type", "application/json"); res.end(payload); } function readBody(req: IncomingMessage): Promise { return new Promise((resolve, reject) => { let raw = ""; req.on("data", (chunk) => (raw += chunk)); req.on("end", () => { if (!raw) return resolve({}); try { resolve(JSON.parse(raw)); } catch (err) { reject(err); } }); req.on("error", reject); }); } type Handler = (req: IncomingMessage, res: ServerResponse, params: Record) => Promise | void; interface Route { method: string; pattern: RegExp; paramNames: string[]; handler: Handler; } function route(method: string, path: string, handler: Handler): Route { const paramNames: string[] = []; const patternStr = path .split("/") .map((segment) => { if (segment.startsWith(":")) { paramNames.push(segment.slice(1)); return "([^/]+)"; } return segment; }) .join("/"); return { method, pattern: new RegExp(`^${patternStr}$`), paramNames, handler }; } export function apiPlugin(): Plugin { const routes: Route[] = [ route("GET", "/api/portfolio", (_req, res) => { send(res, 200, readJson("portfolio.json")); }), route("GET", "/api/history", (_req, res) => { send(res, 200, readJson("history.json")); }), route("POST", "/api/history/snapshots", async (req, res) => { const body = await readBody(req); if (typeof body.date !== "string" || typeof body.marketValue !== "number") { return send(res, 400, { error: "date (string) y marketValue (number) son obligatorios" }); } const history = readJson("history.json"); const withoutSameDate = history.snapshots.filter((s) => s.date !== body.date); const snapshots = [...withoutSameDate, { date: body.date, marketValue: body.marketValue }].sort((a, b) => a.date.localeCompare(b.date), ); const next: History = { snapshots }; writeJson("history.json", next); send(res, 201, next); }), route("DELETE", "/api/history/snapshots/:date", (_req, res, params) => { const history = readJson("history.json"); const snapshots = history.snapshots.filter((s) => s.date !== params.date); const next: History = { snapshots }; writeJson("history.json", next); send(res, 200, next); }), route("GET", "/api/contributions", (_req, res) => { send(res, 200, readJson("contributions.json")); }), route("POST", "/api/contributions/movements", async (req, res) => { const body = await readBody(req); if (typeof body.date !== "string" || typeof body.amount !== "number" || !body.kind) { return send(res, 400, { error: "date, amount y kind son obligatorios" }); } const data = readJson("contributions.json"); const movement: Movement = { id: randomUUID(), date: body.date, kind: body.kind, amount: body.amount, description: body.description ?? undefined, }; data.movements = [...data.movements, movement].sort((a, b) => a.date.localeCompare(b.date)); writeJson("contributions.json", data); send(res, 201, movement); }), route("DELETE", "/api/contributions/movements/:id", (_req, res, params) => { const data = readJson("contributions.json"); data.movements = data.movements.filter((m) => m.id !== params.id); writeJson("contributions.json", data); send(res, 200, data); }), route("POST", "/api/contributions/plans", async (req, res) => { const body = await readBody(req); if (typeof body.amount !== "number" || !body.frequency || typeof body.startDate !== "string") { return send(res, 400, { error: "amount, frequency y startDate son obligatorios" }); } const data = readJson("contributions.json"); const plan: RecurringPlan = { id: randomUUID(), amount: body.amount, frequency: body.frequency, startDate: body.startDate, endDate: body.endDate ?? undefined, active: body.active ?? true, note: body.note ?? undefined, }; data.recurringPlans = [...data.recurringPlans, plan]; writeJson("contributions.json", data); send(res, 201, plan); }), route("PATCH", "/api/contributions/plans/:id", async (req, res, params) => { const body = await readBody(req); const data = readJson("contributions.json"); const idx = data.recurringPlans.findIndex((p) => p.id === params.id); if (idx === -1) return send(res, 404, { error: "Plan no encontrado" }); data.recurringPlans[idx] = { ...data.recurringPlans[idx], ...body, id: params.id }; writeJson("contributions.json", data); send(res, 200, data.recurringPlans[idx]); }), route("DELETE", "/api/contributions/plans/:id", (_req, res, params) => { const data = readJson("contributions.json"); data.recurringPlans = data.recurringPlans.filter((p) => p.id !== params.id); writeJson("contributions.json", data); send(res, 200, data); }), route("GET", "/api/settings", (_req, res) => { send(res, 200, readJson("settings.json")); }), route("PUT", "/api/settings", async (req, res) => { const body = await readBody(req); const current = readJson("settings.json"); const next: Settings = { ...current, ...body }; writeJson("settings.json", next); send(res, 200, next); }), route("GET", "/api/sp500", (_req, res) => { send(res, 200, readJson("sp500.json")); }), ]; const middleware: Connect.NextHandleFunction = (req, res, next) => { if (!req.url?.startsWith("/api/")) return next(); const [pathname] = req.url.split("?"); const method = (req.method ?? "GET").toUpperCase(); for (const r of routes) { if (r.method !== method) continue; const match = r.pattern.exec(pathname); if (!match) continue; const params: Record = {}; r.paramNames.forEach((name, i) => (params[name] = decodeURIComponent(match[i + 1]))); Promise.resolve(r.handler(req, res, params)).catch((err) => { console.error(err); send(res, 500, { error: "Error interno del servidor", detail: String(err?.message ?? err) }); }); return; } send(res, 404, { error: `Ruta no encontrada: ${method} ${pathname}` }); }; return { name: "local-json-api", configureServer(server) { server.middlewares.use(middleware); }, configurePreviewServer(server) { server.middlewares.use(middleware); }, }; }