1import type { NextFunction, Request, Response } from "express";
2import { v4 as uuid } from "uuid";
3import { API_PREFIX, MAX_CSV_IMPORT_BODY_BYTES } from "../constants.js";
4
5
23
24const IMPORT_PATH = new RegExp(`^/${API_PREFIX.replace(/\//g, "\\/")}/databases/[^/]+/import/csv$`);
25
26function envelope(req: Request, res: Response, code: string, status: number, message: string): void {
27 const requestId = (req.headers["x-request-id"] as string | undefined) ?? uuid();
28 req.id = requestId;
29 res.setHeader("x-request-id", requestId);
30 res.status(status).json({ error: { code, message, requestId } });
31}
32
33export function csvImportBodyParser(req: Request, res: Response, next: NextFunction): void {
34 if (req.method !== "POST" || !IMPORT_PATH.test(req.path)) {
35 next();
36 return;
37 }
38
39 const declared = Number(req.headers["content-length"] ?? 0);
40 if (declared > MAX_CSV_IMPORT_BODY_BYTES) {
41 req.resume();
42 envelope(req, res, "PAYLOAD_TOO_LARGE", 413, `CSV import body exceeds the ${MAX_CSV_IMPORT_BODY_BYTES} byte limit.`);
43 return;
44 }
45 const chunks: Buffer[] = [];
46 let size = 0;
47 let done = false;
48 const finish = (err?: unknown): void => {
49 if (done) return;
50 done = true;
51 if (err) {
52 next(err);
53 return;
54 }
55 const raw = Buffer.concat(chunks);
56 req.rawBody = raw;
57 try {
58 req.body = raw.length > 0 ? JSON.parse(raw.toString("utf8")) : {};
59 } catch {
60 envelope(req, res, "VALIDATION_ERROR", 400, "Request body must be valid JSON.");
61 return;
62 }
63 (req as unknown as Record<string, unknown>)._body = true;
64 next();
65 };
66 req.on("data", (chunk: Buffer) => {
67 size += chunk.length;
68 if (size > MAX_CSV_IMPORT_BODY_BYTES) {
69 done = true;
70 req.resume();
71 envelope(req, res, "PAYLOAD_TOO_LARGE", 413, `CSV import body exceeds the ${MAX_CSV_IMPORT_BODY_BYTES} byte limit.`);
72 return;
73 }
74 chunks.push(chunk);
75 });
76 req.on("end", () => finish());
77 req.on("error", (err) => finish(err));
78}
79