1
17
18export const AUDIT_CSV_HEADER =
19 "id,created_at,actor_type,actor_id,actor_name,workspace_id,workspace_name,resource_type,resource_id,action,ip_address,user_agent,metadata";
20
21export interface AuditCsvRow {
22 id: string;
23 createdAt: Date;
24 actorType: string;
25 actorId: string;
26 workspaceId: string | null;
27 resourceType: string;
28 resourceId: string;
29 action: string;
30 ipAddress: string | null;
31 userAgent: string | null;
32 metadata: unknown;
33}
34
35function quote(value: string | null | undefined, always = false): string {
36 if (value == null) return always ? '""' : "";
37 const s = String(value);
38 if (always || /[",\n\r]/.test(s)) return `"${s.replace(/"/g, '""')}"`;
39 return s;
40}
41
42/** Resolve actor names for USER rows (one bulk lookup; `-` otherwise). */
43export function resolveActorNames(
44 rows: AuditCsvRow[],
45 userNames: Map<string, string>
46): string[] {
47 return rows.map((r) =>
48 r.actorType === "USER" ? (userNames.get(r.actorId) ?? "-") : "-"
49 );
50}
51
52/** Render one row (no header). The worker's streaming export calls this per
53 * batch; `renderAuditCsv` is the sync path's header+rows convenience. */
54export function auditCsvLine(
55 r: AuditCsvRow,
56 actorName: string,
57 workspaceNames: Map<string, string>
58): string {
59 return [
60 quote(r.id),
61 quote(r.createdAt.toISOString()),
62 quote(r.actorType),
63 quote(r.actorId),
64 quote(actorName),
65 quote(r.workspaceId),
66 quote(r.workspaceId ? (workspaceNames.get(r.workspaceId) ?? "") : ""),
67 quote(r.resourceType),
68 quote(r.resourceId),
69 quote(r.action),
70 quote(r.ipAddress),
71 quote(r.userAgent, true),
72 quote(JSON.stringify(r.metadata ?? {}), true)
73 ].join(",");
74}
75
76
77export function renderAuditCsv(
78 rows: AuditCsvRow[],
79 userNames: Map<string, string>,
80 workspaceNames: Map<string, string>
81): string {
82 const names = resolveActorNames(rows, userNames);
83 const lines = [AUDIT_CSV_HEADER];
84 rows.forEach((r, i) => lines.push(auditCsvLine(r, names[i] ?? "-", workspaceNames)));
85 return lines.join("\n") + "\n";
86}
87