@usebetterdev/audit-cli 0.4.0-beta.4 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,345 @@
1
+ // src/sql-executor.ts
2
+ import { Kysely, sql } from "kysely";
3
+ import { parseDuration } from "@usebetterdev/audit-core";
4
+ function importModule(name) {
5
+ return import(
6
+ /* webpackIgnore: true */
7
+ name
8
+ );
9
+ }
10
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
11
+ var VALID_OPERATIONS = /* @__PURE__ */ new Set([
12
+ "INSERT",
13
+ "UPDATE",
14
+ "DELETE"
15
+ ]);
16
+ var VALID_SEVERITIES = /* @__PURE__ */ new Set([
17
+ "low",
18
+ "medium",
19
+ "high",
20
+ "critical"
21
+ ]);
22
+ var MAX_LIMIT = 1e3;
23
+ async function createDialect(databaseUrl, dialectName) {
24
+ if (dialectName === "postgres") {
25
+ let pgModule;
26
+ try {
27
+ pgModule = await importModule("pg");
28
+ } catch {
29
+ throw new Error(
30
+ "Missing dependency: pg. Install it with:\n npm install pg"
31
+ );
32
+ }
33
+ const { PostgresDialect } = await import("kysely");
34
+ const pgDefault = pgModule["default"];
35
+ const PoolClass = pgDefault?.["Pool"] ?? pgModule["Pool"];
36
+ if (typeof PoolClass !== "function") {
37
+ throw new Error(
38
+ "Incompatible pg version: 'Pool' export not found. Ensure pg >=8.0.0 is installed."
39
+ );
40
+ }
41
+ return new PostgresDialect({
42
+ pool: new PoolClass({ connectionString: databaseUrl })
43
+ });
44
+ }
45
+ if (dialectName === "mysql") {
46
+ let mysql2Module;
47
+ try {
48
+ mysql2Module = await importModule("mysql2");
49
+ } catch {
50
+ throw new Error(
51
+ "Missing dependency: mysql2. Install it with:\n npm install mysql2"
52
+ );
53
+ }
54
+ const { MysqlDialect } = await import("kysely");
55
+ const mysql2Default = mysql2Module["default"];
56
+ const createPool = mysql2Default?.["createPool"] ?? mysql2Module["createPool"];
57
+ if (typeof createPool !== "function") {
58
+ throw new Error(
59
+ "Incompatible mysql2 version: 'createPool' export not found. Ensure mysql2 >=3.0.0 is installed."
60
+ );
61
+ }
62
+ return new MysqlDialect({
63
+ pool: createPool(databaseUrl)
64
+ });
65
+ }
66
+ let betterSqlite3Module;
67
+ try {
68
+ betterSqlite3Module = await importModule("better-sqlite3");
69
+ } catch {
70
+ throw new Error(
71
+ "Missing dependency: better-sqlite3. Install it with:\n npm install better-sqlite3"
72
+ );
73
+ }
74
+ const { SqliteDialect } = await import("kysely");
75
+ const betterSqlite3Default = betterSqlite3Module["default"];
76
+ const DatabaseClass = betterSqlite3Default ?? betterSqlite3Module["Database"];
77
+ if (typeof DatabaseClass !== "function") {
78
+ throw new Error(
79
+ "Incompatible better-sqlite3 version: default export not found. Ensure better-sqlite3 >=11.0.0 is installed."
80
+ );
81
+ }
82
+ const filePath = databaseUrl.startsWith("file:") ? databaseUrl.slice(5) : databaseUrl;
83
+ return new SqliteDialect({
84
+ database: new DatabaseClass(filePath)
85
+ });
86
+ }
87
+ async function createKyselyInstance(databaseUrl, dialectName) {
88
+ const dialect = await createDialect(databaseUrl, dialectName);
89
+ return new Kysely({ dialect });
90
+ }
91
+ function resolveTimeFilter(filter) {
92
+ if ("date" in filter && filter.date !== void 0) {
93
+ return filter.date;
94
+ }
95
+ if ("duration" in filter && filter.duration !== void 0) {
96
+ return parseDuration(filter.duration);
97
+ }
98
+ throw new Error("TimeFilter must have either date or duration");
99
+ }
100
+ function escapeLikePattern(input) {
101
+ return input.replace(/[%_\\]/g, "\\$&");
102
+ }
103
+ function encodeCursor(timestamp, id) {
104
+ const payload = JSON.stringify({ t: timestamp.toISOString(), i: id });
105
+ return btoa(payload);
106
+ }
107
+ function decodeCursor(cursor) {
108
+ let parsed;
109
+ try {
110
+ parsed = JSON.parse(atob(cursor));
111
+ } catch {
112
+ throw new Error("Invalid cursor: failed to decode");
113
+ }
114
+ if (typeof parsed !== "object" || parsed === null || !("t" in parsed) || !("i" in parsed)) {
115
+ throw new Error("Invalid cursor: missing required fields");
116
+ }
117
+ const { t, i } = parsed;
118
+ if (typeof t !== "string" || typeof i !== "string") {
119
+ throw new Error("Invalid cursor: fields must be strings");
120
+ }
121
+ const timestamp = new Date(t);
122
+ if (isNaN(timestamp.getTime())) {
123
+ throw new Error("Invalid cursor: invalid timestamp");
124
+ }
125
+ if (!UUID_PATTERN.test(i)) {
126
+ throw new Error("Invalid cursor: id must be a valid UUID");
127
+ }
128
+ return { timestamp, id: i };
129
+ }
130
+ function parseJsonColumn(value, column, rowId) {
131
+ if (typeof value !== "string") {
132
+ return value;
133
+ }
134
+ try {
135
+ return JSON.parse(value);
136
+ } catch (cause) {
137
+ throw new Error(
138
+ `Failed to parse "${column}" as JSON for audit log "${rowId}": ${cause instanceof Error ? cause.message : String(cause)}`
139
+ );
140
+ }
141
+ }
142
+ function isAuditOperation(value) {
143
+ return VALID_OPERATIONS.has(value);
144
+ }
145
+ function isAuditSeverity(value) {
146
+ return VALID_SEVERITIES.has(value);
147
+ }
148
+ function mapRowToAuditLog(row) {
149
+ const rowId = String(row["id"]);
150
+ const operation = String(row["operation"]);
151
+ if (!isAuditOperation(operation)) {
152
+ throw new Error(
153
+ `Invalid audit operation: "${operation}". Expected one of: INSERT, UPDATE, DELETE`
154
+ );
155
+ }
156
+ const rawTimestamp = row["timestamp"];
157
+ const timestamp = rawTimestamp instanceof Date ? rawTimestamp : new Date(String(rawTimestamp));
158
+ const log = {
159
+ id: rowId,
160
+ timestamp,
161
+ tableName: String(row["table_name"]),
162
+ operation,
163
+ recordId: String(row["record_id"])
164
+ };
165
+ const actorId = row["actor_id"];
166
+ if (actorId !== null && actorId !== void 0) {
167
+ log.actorId = String(actorId);
168
+ }
169
+ const beforeData = row["before_data"];
170
+ if (beforeData !== null && beforeData !== void 0) {
171
+ log.beforeData = parseJsonColumn(beforeData, "before_data", rowId);
172
+ }
173
+ const afterData = row["after_data"];
174
+ if (afterData !== null && afterData !== void 0) {
175
+ log.afterData = parseJsonColumn(afterData, "after_data", rowId);
176
+ }
177
+ const diff = row["diff"];
178
+ if (diff !== null && diff !== void 0) {
179
+ log.diff = parseJsonColumn(diff, "diff", rowId);
180
+ }
181
+ const label = row["label"];
182
+ if (label !== null && label !== void 0) {
183
+ log.label = String(label);
184
+ }
185
+ const description = row["description"];
186
+ if (description !== null && description !== void 0) {
187
+ log.description = String(description);
188
+ }
189
+ const severity = row["severity"];
190
+ if (severity !== null && severity !== void 0) {
191
+ const severityStr = String(severity);
192
+ if (!isAuditSeverity(severityStr)) {
193
+ throw new Error(
194
+ `Invalid audit severity: "${severityStr}". Expected one of: low, medium, high, critical`
195
+ );
196
+ }
197
+ log.severity = severityStr;
198
+ }
199
+ const compliance = row["compliance"];
200
+ if (compliance !== null && compliance !== void 0) {
201
+ log.compliance = parseJsonColumn(compliance, "compliance", rowId);
202
+ }
203
+ const notify = row["notify"];
204
+ if (notify !== null && notify !== void 0) {
205
+ log.notify = Boolean(notify);
206
+ }
207
+ const reason = row["reason"];
208
+ if (reason !== null && reason !== void 0) {
209
+ log.reason = String(reason);
210
+ }
211
+ const metadata = row["metadata"];
212
+ if (metadata !== null && metadata !== void 0) {
213
+ log.metadata = parseJsonColumn(metadata, "metadata", rowId);
214
+ }
215
+ const redactedFields = row["redacted_fields"];
216
+ if (redactedFields !== null && redactedFields !== void 0) {
217
+ log.redactedFields = parseJsonColumn(redactedFields, "redacted_fields", rowId);
218
+ }
219
+ return log;
220
+ }
221
+ function createSqlExecutor(db, dialectName) {
222
+ return async (spec) => {
223
+ const sortOrder = spec.sortOrder ?? "desc";
224
+ const limit = Math.min(spec.limit ?? 50, MAX_LIMIT);
225
+ const fetchLimit = limit + 1;
226
+ let query = db.selectFrom("audit_logs").selectAll();
227
+ const { filters } = spec;
228
+ if (filters.resource !== void 0) {
229
+ query = query.where("table_name", "=", filters.resource.tableName);
230
+ if (filters.resource.recordId !== void 0) {
231
+ query = query.where("record_id", "=", filters.resource.recordId);
232
+ }
233
+ }
234
+ if (filters.actorIds !== void 0 && filters.actorIds.length > 0) {
235
+ if (filters.actorIds.length === 1) {
236
+ query = query.where("actor_id", "=", filters.actorIds[0]);
237
+ } else {
238
+ query = query.where("actor_id", "in", filters.actorIds);
239
+ }
240
+ }
241
+ if (filters.severities !== void 0 && filters.severities.length > 0) {
242
+ if (filters.severities.length === 1) {
243
+ query = query.where("severity", "=", filters.severities[0]);
244
+ } else {
245
+ query = query.where("severity", "in", filters.severities);
246
+ }
247
+ }
248
+ if (filters.operations !== void 0 && filters.operations.length > 0) {
249
+ if (filters.operations.length === 1) {
250
+ query = query.where("operation", "=", filters.operations[0]);
251
+ } else {
252
+ query = query.where("operation", "in", filters.operations);
253
+ }
254
+ }
255
+ if (filters.since !== void 0) {
256
+ const date = resolveTimeFilter(filters.since);
257
+ query = query.where("timestamp", ">=", date);
258
+ }
259
+ if (filters.until !== void 0) {
260
+ const date = resolveTimeFilter(filters.until);
261
+ query = query.where("timestamp", "<=", date);
262
+ }
263
+ if (filters.searchText !== void 0 && filters.searchText.length > 0) {
264
+ const escaped = escapeLikePattern(filters.searchText);
265
+ const pattern = `%${escaped}%`;
266
+ if (dialectName === "postgres") {
267
+ query = query.where(
268
+ (eb) => eb.or([
269
+ eb("label", "ilike", pattern),
270
+ eb("description", "ilike", pattern)
271
+ ])
272
+ );
273
+ } else {
274
+ query = query.where(
275
+ (eb) => eb.or([
276
+ eb("label", "like", pattern),
277
+ eb("description", "like", pattern)
278
+ ])
279
+ );
280
+ }
281
+ }
282
+ if (filters.compliance !== void 0 && filters.compliance.length > 0) {
283
+ const tagsJson = JSON.stringify(filters.compliance);
284
+ if (dialectName === "postgres") {
285
+ query = query.where(
286
+ sql`"compliance" @> ${tagsJson}::jsonb`
287
+ );
288
+ } else if (dialectName === "mysql") {
289
+ query = query.where(
290
+ sql`JSON_CONTAINS(compliance, ${tagsJson})`
291
+ );
292
+ } else {
293
+ for (const tag of filters.compliance) {
294
+ query = query.where(
295
+ sql`EXISTS (SELECT 1 FROM json_each("compliance") WHERE value = ${tag})`
296
+ );
297
+ }
298
+ }
299
+ }
300
+ if (spec.cursor !== void 0) {
301
+ const decoded = decodeCursor(spec.cursor);
302
+ if (sortOrder === "desc") {
303
+ query = query.where(
304
+ (eb) => eb.or([
305
+ eb("timestamp", "<", decoded.timestamp),
306
+ eb.and([
307
+ eb("timestamp", "=", decoded.timestamp),
308
+ eb("id", "<", decoded.id)
309
+ ])
310
+ ])
311
+ );
312
+ } else {
313
+ query = query.where(
314
+ (eb) => eb.or([
315
+ eb("timestamp", ">", decoded.timestamp),
316
+ eb.and([
317
+ eb("timestamp", "=", decoded.timestamp),
318
+ eb("id", ">", decoded.id)
319
+ ])
320
+ ])
321
+ );
322
+ }
323
+ }
324
+ query = query.orderBy("timestamp", sortOrder).orderBy("id", sortOrder).limit(fetchLimit);
325
+ const rows = await query.execute();
326
+ const hasNextPage = rows.length > limit;
327
+ const resultRows = hasNextPage ? rows.slice(0, -1) : rows;
328
+ const entries = resultRows.map((row) => mapRowToAuditLog(row));
329
+ const lastRow = resultRows[resultRows.length - 1];
330
+ if (hasNextPage && lastRow !== void 0) {
331
+ const lastTimestamp = lastRow.timestamp instanceof Date ? lastRow.timestamp : new Date(String(lastRow.timestamp));
332
+ return {
333
+ entries,
334
+ nextCursor: encodeCursor(lastTimestamp, String(lastRow.id))
335
+ };
336
+ }
337
+ return { entries };
338
+ };
339
+ }
340
+
341
+ export {
342
+ createKyselyInstance,
343
+ createSqlExecutor
344
+ };
345
+ //# sourceMappingURL=chunk-7GSN73TA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/sql-executor.ts"],"sourcesContent":["/**\n * Kysely-based query executor for the CLI export command.\n *\n * Builds and executes queries against the `audit_logs` table using Kysely\n * with multi-dialect support (Postgres, MySQL, SQLite). No ORM adapter required.\n */\n\nimport { Kysely, sql } from \"kysely\";\nimport type { Dialect, SqlBool } from \"kysely\";\nimport { parseDuration } from \"@usebetterdev/audit-core\";\nimport type {\n AuditLog,\n AuditOperation,\n AuditSeverity,\n AuditQuerySpec,\n AuditQueryResult,\n QueryExecutor,\n TimeFilter,\n} from \"@usebetterdev/audit-core\";\nimport type { DatabaseDialect } from \"./detect-adapter.js\";\n\n/**\n * Dynamic module import helper. Uses a variable to prevent TypeScript from\n * resolving the module at compile time — these are optional peer deps that\n * may not be installed.\n */\nfunction importModule(name: string): Promise<Record<string, unknown>> {\n return import(/* webpackIgnore: true */ name) as Promise<Record<string, unknown>>;\n}\n\ninterface AuditLogsTable {\n id: string;\n timestamp: Date;\n table_name: string;\n operation: string;\n record_id: string;\n actor_id: string | null;\n before_data: unknown | null;\n after_data: unknown | null;\n diff: unknown | null;\n label: string | null;\n description: string | null;\n severity: string | null;\n compliance: unknown | null;\n notify: boolean | null;\n reason: string | null;\n metadata: unknown | null;\n redacted_fields: unknown | null;\n}\n\ninterface Database {\n audit_logs: AuditLogsTable;\n}\n\nconst UUID_PATTERN =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\nconst VALID_OPERATIONS: ReadonlySet<string> = new Set([\n \"INSERT\",\n \"UPDATE\",\n \"DELETE\",\n]);\n\nconst VALID_SEVERITIES: ReadonlySet<string> = new Set([\n \"low\",\n \"medium\",\n \"high\",\n \"critical\",\n]);\n\nconst MAX_LIMIT = 1000;\n\n/**\n * Dynamically imports the appropriate driver and creates a Kysely Dialect.\n *\n * This is a deliberate exception to the static-import convention — CLI commands\n * cannot statically depend on database drivers that may not be installed.\n */\nasync function createDialect(\n databaseUrl: string,\n dialectName: DatabaseDialect,\n): Promise<Dialect> {\n if (dialectName === \"postgres\") {\n let pgModule: Record<string, unknown>;\n try {\n pgModule = await importModule(\"pg\");\n } catch {\n throw new Error(\n \"Missing dependency: pg. Install it with:\\n npm install pg\",\n );\n }\n\n const { PostgresDialect } = await import(\"kysely\");\n\n // pg exports Pool on `default.Pool` (CJS → ESM interop) or directly\n const pgDefault = pgModule[\"default\"] as Record<string, unknown> | undefined;\n const PoolClass = (pgDefault?.[\"Pool\"] ?? pgModule[\"Pool\"]) as\n | (new (config: { connectionString: string }) => unknown)\n | undefined;\n\n if (typeof PoolClass !== \"function\") {\n throw new Error(\n \"Incompatible pg version: 'Pool' export not found. Ensure pg >=8.0.0 is installed.\",\n );\n }\n\n return new PostgresDialect({\n pool: new PoolClass({ connectionString: databaseUrl }) as never,\n });\n }\n\n if (dialectName === \"mysql\") {\n let mysql2Module: Record<string, unknown>;\n try {\n mysql2Module = await importModule(\"mysql2\");\n } catch {\n throw new Error(\n \"Missing dependency: mysql2. Install it with:\\n npm install mysql2\",\n );\n }\n\n const { MysqlDialect } = await import(\"kysely\");\n\n const mysql2Default = mysql2Module[\"default\"] as Record<string, unknown> | undefined;\n const createPool = (mysql2Default?.[\"createPool\"] ?? mysql2Module[\"createPool\"]) as\n | ((uri: string) => unknown)\n | undefined;\n\n if (typeof createPool !== \"function\") {\n throw new Error(\n \"Incompatible mysql2 version: 'createPool' export not found. Ensure mysql2 >=3.0.0 is installed.\",\n );\n }\n\n return new MysqlDialect({\n pool: createPool(databaseUrl) as never,\n });\n }\n\n // sqlite\n let betterSqlite3Module: Record<string, unknown>;\n try {\n betterSqlite3Module = await importModule(\"better-sqlite3\");\n } catch {\n throw new Error(\n \"Missing dependency: better-sqlite3. Install it with:\\n npm install better-sqlite3\",\n );\n }\n\n const { SqliteDialect } = await import(\"kysely\");\n\n const betterSqlite3Default = betterSqlite3Module[\"default\"] as\n | (new (path: string) => unknown)\n | undefined;\n const DatabaseClass = betterSqlite3Default ?? betterSqlite3Module[\"Database\"] as\n | (new (path: string) => unknown)\n | undefined;\n\n if (typeof DatabaseClass !== \"function\") {\n throw new Error(\n \"Incompatible better-sqlite3 version: default export not found. Ensure better-sqlite3 >=11.0.0 is installed.\",\n );\n }\n\n // Strip `file:` prefix if present\n const filePath = databaseUrl.startsWith(\"file:\")\n ? databaseUrl.slice(5)\n : databaseUrl;\n\n return new SqliteDialect({\n database: new DatabaseClass(filePath) as never,\n });\n}\n\n/**\n * Creates a Kysely instance configured for the given database URL and dialect.\n */\nexport async function createKyselyInstance(\n databaseUrl: string,\n dialectName: DatabaseDialect,\n): Promise<Kysely<Database>> {\n const dialect = await createDialect(databaseUrl, dialectName);\n return new Kysely<Database>({ dialect });\n}\n\n/**\n * Resolves a `TimeFilter` to an absolute `Date`.\n * Absolute dates are returned as-is; duration strings are parsed relative to now.\n */\nfunction resolveTimeFilter(filter: TimeFilter): Date {\n if (\"date\" in filter && filter.date !== undefined) {\n return filter.date;\n }\n if (\"duration\" in filter && filter.duration !== undefined) {\n return parseDuration(filter.duration);\n }\n throw new Error(\"TimeFilter must have either date or duration\");\n}\n\n/**\n * Escapes LIKE/ILIKE wildcard characters so they match literally.\n * Backslash is the default escape character.\n */\nfunction escapeLikePattern(input: string): string {\n return input.replace(/[%_\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Encodes a cursor from a timestamp and id.\n * Format: base64(JSON.stringify({ t: iso_string, i: id }))\n */\nfunction encodeCursor(timestamp: Date, id: string): string {\n const payload = JSON.stringify({ t: timestamp.toISOString(), i: id });\n return btoa(payload);\n}\n\n/**\n * Decodes an opaque cursor string back to timestamp + id.\n * Throws on invalid input.\n */\nfunction decodeCursor(cursor: string): { timestamp: Date; id: string } {\n let parsed: unknown;\n try {\n parsed = JSON.parse(atob(cursor));\n } catch {\n throw new Error(\"Invalid cursor: failed to decode\");\n }\n\n if (\n typeof parsed !== \"object\" ||\n parsed === null ||\n !(\"t\" in parsed) ||\n !(\"i\" in parsed)\n ) {\n throw new Error(\"Invalid cursor: missing required fields\");\n }\n\n const { t, i } = parsed as { t: unknown; i: unknown };\n\n if (typeof t !== \"string\" || typeof i !== \"string\") {\n throw new Error(\"Invalid cursor: fields must be strings\");\n }\n\n const timestamp = new Date(t);\n if (isNaN(timestamp.getTime())) {\n throw new Error(\"Invalid cursor: invalid timestamp\");\n }\n\n if (!UUID_PATTERN.test(i)) {\n throw new Error(\"Invalid cursor: id must be a valid UUID\");\n }\n\n return { timestamp, id: i };\n}\n\n/**\n * Parses a JSON string, re-throwing with the column name and row ID for context.\n * Non-string values are returned as-is (drivers like pg auto-parse JSONB).\n */\nfunction parseJsonColumn(value: unknown, column: string, rowId: string): unknown {\n if (typeof value !== \"string\") {\n return value;\n }\n try {\n return JSON.parse(value);\n } catch (cause) {\n throw new Error(\n `Failed to parse \"${column}\" as JSON for audit log \"${rowId}\": ${cause instanceof Error ? cause.message : String(cause)}`,\n );\n }\n}\n\nfunction isAuditOperation(value: string): value is AuditOperation {\n return VALID_OPERATIONS.has(value);\n}\n\nfunction isAuditSeverity(value: string): value is AuditSeverity {\n return VALID_SEVERITIES.has(value);\n}\n\n/**\n * Converts a snake_case DB row to a camelCase `AuditLog`.\n * Only includes optional fields when non-null.\n */\nfunction mapRowToAuditLog(row: Record<string, unknown>): AuditLog {\n const rowId = String(row[\"id\"]);\n const operation = String(row[\"operation\"]);\n if (!isAuditOperation(operation)) {\n throw new Error(\n `Invalid audit operation: \"${operation}\". Expected one of: INSERT, UPDATE, DELETE`,\n );\n }\n\n const rawTimestamp = row[\"timestamp\"];\n const timestamp = rawTimestamp instanceof Date\n ? rawTimestamp\n : new Date(String(rawTimestamp));\n\n const log: AuditLog = {\n id: rowId,\n timestamp,\n tableName: String(row[\"table_name\"]),\n operation,\n recordId: String(row[\"record_id\"]),\n };\n\n const actorId = row[\"actor_id\"];\n if (actorId !== null && actorId !== undefined) {\n log.actorId = String(actorId);\n }\n\n const beforeData = row[\"before_data\"];\n if (beforeData !== null && beforeData !== undefined) {\n log.beforeData = parseJsonColumn(beforeData, \"before_data\", rowId) as Record<string, unknown>;\n }\n\n const afterData = row[\"after_data\"];\n if (afterData !== null && afterData !== undefined) {\n log.afterData = parseJsonColumn(afterData, \"after_data\", rowId) as Record<string, unknown>;\n }\n\n const diff = row[\"diff\"];\n if (diff !== null && diff !== undefined) {\n log.diff = parseJsonColumn(diff, \"diff\", rowId) as { changedFields: string[] };\n }\n\n const label = row[\"label\"];\n if (label !== null && label !== undefined) {\n log.label = String(label);\n }\n\n const description = row[\"description\"];\n if (description !== null && description !== undefined) {\n log.description = String(description);\n }\n\n const severity = row[\"severity\"];\n if (severity !== null && severity !== undefined) {\n const severityStr = String(severity);\n if (!isAuditSeverity(severityStr)) {\n throw new Error(\n `Invalid audit severity: \"${severityStr}\". Expected one of: low, medium, high, critical`,\n );\n }\n log.severity = severityStr;\n }\n\n const compliance = row[\"compliance\"];\n if (compliance !== null && compliance !== undefined) {\n log.compliance = parseJsonColumn(compliance, \"compliance\", rowId) as string[];\n }\n\n const notify = row[\"notify\"];\n if (notify !== null && notify !== undefined) {\n log.notify = Boolean(notify);\n }\n\n const reason = row[\"reason\"];\n if (reason !== null && reason !== undefined) {\n log.reason = String(reason);\n }\n\n const metadata = row[\"metadata\"];\n if (metadata !== null && metadata !== undefined) {\n log.metadata = parseJsonColumn(metadata, \"metadata\", rowId) as Record<string, unknown>;\n }\n\n const redactedFields = row[\"redacted_fields\"];\n if (redactedFields !== null && redactedFields !== undefined) {\n log.redactedFields = parseJsonColumn(redactedFields, \"redacted_fields\", rowId) as string[];\n }\n\n return log;\n}\n\n/**\n * Creates a `QueryExecutor` backed by a Kysely database instance.\n *\n * Translates `AuditQuerySpec` filters into Kysely query builder calls\n * and handles cursor-based pagination.\n */\nexport function createSqlExecutor(\n db: Kysely<Database>,\n dialectName: DatabaseDialect,\n): QueryExecutor {\n return async (spec: AuditQuerySpec): Promise<AuditQueryResult> => {\n const sortOrder = spec.sortOrder ?? \"desc\";\n const limit = Math.min(spec.limit ?? 50, MAX_LIMIT);\n const fetchLimit = limit + 1;\n\n let query = db.selectFrom(\"audit_logs\").selectAll();\n\n const { filters } = spec;\n\n // resource filter\n if (filters.resource !== undefined) {\n query = query.where(\"table_name\", \"=\", filters.resource.tableName);\n if (filters.resource.recordId !== undefined) {\n query = query.where(\"record_id\", \"=\", filters.resource.recordId);\n }\n }\n\n // actorIds\n if (filters.actorIds !== undefined && filters.actorIds.length > 0) {\n if (filters.actorIds.length === 1) {\n query = query.where(\"actor_id\", \"=\", filters.actorIds[0]!);\n } else {\n query = query.where(\"actor_id\", \"in\", filters.actorIds);\n }\n }\n\n // severities\n if (filters.severities !== undefined && filters.severities.length > 0) {\n if (filters.severities.length === 1) {\n query = query.where(\"severity\", \"=\", filters.severities[0]!);\n } else {\n query = query.where(\"severity\", \"in\", filters.severities);\n }\n }\n\n // operations\n if (filters.operations !== undefined && filters.operations.length > 0) {\n if (filters.operations.length === 1) {\n query = query.where(\"operation\", \"=\", filters.operations[0]!);\n } else {\n query = query.where(\"operation\", \"in\", filters.operations);\n }\n }\n\n // since\n if (filters.since !== undefined) {\n const date = resolveTimeFilter(filters.since);\n query = query.where(\"timestamp\", \">=\", date);\n }\n\n // until\n if (filters.until !== undefined) {\n const date = resolveTimeFilter(filters.until);\n query = query.where(\"timestamp\", \"<=\", date);\n }\n\n // searchText — dialect-specific LIKE/ILIKE\n if (filters.searchText !== undefined && filters.searchText.length > 0) {\n const escaped = escapeLikePattern(filters.searchText);\n const pattern = `%${escaped}%`;\n\n if (dialectName === \"postgres\") {\n query = query.where((eb) =>\n eb.or([\n eb(\"label\", \"ilike\", pattern),\n eb(\"description\", \"ilike\", pattern),\n ]),\n );\n } else {\n // MySQL is case-insensitive by default; SQLite LIKE is case-insensitive for ASCII\n query = query.where((eb) =>\n eb.or([\n eb(\"label\", \"like\", pattern),\n eb(\"description\", \"like\", pattern),\n ]),\n );\n }\n }\n\n // compliance — dialect-specific JSON containment\n if (filters.compliance !== undefined && filters.compliance.length > 0) {\n const tagsJson = JSON.stringify(filters.compliance);\n\n if (dialectName === \"postgres\") {\n query = query.where(\n sql<SqlBool>`\"compliance\" @> ${tagsJson}::jsonb`,\n );\n } else if (dialectName === \"mysql\") {\n query = query.where(\n sql<SqlBool>`JSON_CONTAINS(compliance, ${tagsJson})`,\n );\n } else {\n // SQLite: no native JSON containment — use json_each to check all tags\n for (const tag of filters.compliance) {\n query = query.where(\n sql<SqlBool>`EXISTS (SELECT 1 FROM json_each(\"compliance\") WHERE value = ${tag})`,\n );\n }\n }\n }\n\n // Cursor pagination\n if (spec.cursor !== undefined) {\n const decoded = decodeCursor(spec.cursor);\n if (sortOrder === \"desc\") {\n query = query.where((eb) =>\n eb.or([\n eb(\"timestamp\", \"<\", decoded.timestamp),\n eb.and([\n eb(\"timestamp\", \"=\", decoded.timestamp),\n eb(\"id\", \"<\", decoded.id),\n ]),\n ]),\n );\n } else {\n query = query.where((eb) =>\n eb.or([\n eb(\"timestamp\", \">\", decoded.timestamp),\n eb.and([\n eb(\"timestamp\", \"=\", decoded.timestamp),\n eb(\"id\", \">\", decoded.id),\n ]),\n ]),\n );\n }\n }\n\n // Order + limit\n query = query\n .orderBy(\"timestamp\", sortOrder)\n .orderBy(\"id\", sortOrder)\n .limit(fetchLimit);\n\n const rows = await query.execute();\n\n const hasNextPage = rows.length > limit;\n const resultRows = hasNextPage ? rows.slice(0, -1) : rows;\n const entries = resultRows.map((row) => mapRowToAuditLog(row as Record<string, unknown>));\n const lastRow = resultRows[resultRows.length - 1];\n\n if (hasNextPage && lastRow !== undefined) {\n const lastTimestamp = lastRow.timestamp instanceof Date\n ? lastRow.timestamp\n : new Date(String(lastRow.timestamp));\n return {\n entries,\n nextCursor: encodeCursor(lastTimestamp, String(lastRow.id)),\n };\n }\n\n return { entries };\n };\n}\n\n// Re-export for testing\nexport {\n mapRowToAuditLog as _mapRowToAuditLog,\n encodeCursor as _encodeCursor,\n decodeCursor as _decodeCursor,\n escapeLikePattern as _escapeLikePattern,\n};\n\nexport type { Database };\n"],"mappings":"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","names":[]}
@@ -0,0 +1,10 @@
1
+ // src/stats.ts
2
+ async function stats(options = {}) {
3
+ console.log("[better-audit] stats \u2014 not yet implemented");
4
+ void options;
5
+ }
6
+
7
+ export {
8
+ stats
9
+ };
10
+ //# sourceMappingURL=chunk-AGFBL646.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/stats.ts"],"sourcesContent":["/**\n * `better-audit stats` — Show audit log statistics.\n *\n * Outputs:\n * - Total log count\n * - Events per day (last 7 / 30 days)\n * - Top actors (by event count)\n * - Top tables (by event count)\n * - Severity distribution\n *\n * TODO: Connect to DB and run aggregation queries.\n */\n\nexport interface StatsOptions {\n config?: string;\n since?: string;\n}\n\nexport async function stats(options: StatsOptions = {}): Promise<void> {\n console.log(\"[better-audit] stats — not yet implemented\");\n void options;\n}\n"],"mappings":";AAkBA,eAAsB,MAAM,UAAwB,CAAC,GAAkB;AACrE,UAAQ,IAAI,iDAA4C;AACxD,OAAK;AACP;","names":[]}
@@ -0,0 +1,77 @@
1
+ // src/utils.ts
2
+ function formatTimestamp(date) {
3
+ const y = date.getFullYear();
4
+ const m = String(date.getMonth() + 1).padStart(2, "0");
5
+ const d = String(date.getDate()).padStart(2, "0");
6
+ const h = String(date.getHours()).padStart(2, "0");
7
+ const min = String(date.getMinutes()).padStart(2, "0");
8
+ const sec = String(date.getSeconds()).padStart(2, "0");
9
+ return `${y}${m}${d}${h}${min}${sec}`;
10
+ }
11
+
12
+ // src/detect-adapter.ts
13
+ import { existsSync, readFileSync } from "fs";
14
+ import { join } from "path";
15
+ function detectAdapter(cwd) {
16
+ const pkgPath = join(cwd, "package.json");
17
+ if (!existsSync(pkgPath)) {
18
+ return void 0;
19
+ }
20
+ try {
21
+ const content = readFileSync(pkgPath, "utf-8");
22
+ const parsed = JSON.parse(content);
23
+ if (typeof parsed !== "object" || parsed === null) {
24
+ return void 0;
25
+ }
26
+ const allDeps = {};
27
+ if ("dependencies" in parsed && typeof parsed.dependencies === "object" && parsed.dependencies !== null) {
28
+ Object.assign(allDeps, parsed.dependencies);
29
+ }
30
+ if ("devDependencies" in parsed && typeof parsed.devDependencies === "object" && parsed.devDependencies !== null) {
31
+ Object.assign(allDeps, parsed.devDependencies);
32
+ }
33
+ if ("drizzle-orm" in allDeps) {
34
+ return "drizzle";
35
+ }
36
+ if ("@prisma/client" in allDeps) {
37
+ return "prisma";
38
+ }
39
+ } catch {
40
+ }
41
+ return void 0;
42
+ }
43
+ function detectDialect(databaseUrl) {
44
+ if (databaseUrl === void 0) {
45
+ return "postgres";
46
+ }
47
+ const lower = databaseUrl.toLowerCase();
48
+ if (lower.startsWith("postgres://") || lower.startsWith("postgresql://")) {
49
+ return "postgres";
50
+ }
51
+ if (lower.startsWith("mysql://")) {
52
+ return "mysql";
53
+ }
54
+ if (lower.startsWith("file:") || lower.endsWith(".db") || lower.endsWith(".sqlite") || lower.endsWith(".sqlite3")) {
55
+ return "sqlite";
56
+ }
57
+ return "postgres";
58
+ }
59
+ function findMigrationDirectory(cwd, adapter) {
60
+ if (adapter === "drizzle") {
61
+ const supabasePath = join(cwd, "supabase", "migrations");
62
+ if (existsSync(supabasePath)) {
63
+ return supabasePath;
64
+ }
65
+ return join(cwd, "drizzle");
66
+ }
67
+ const timestamp = formatTimestamp(/* @__PURE__ */ new Date());
68
+ return join(cwd, "prisma", "migrations", `${timestamp}_add_audit_logs`);
69
+ }
70
+
71
+ export {
72
+ formatTimestamp,
73
+ detectAdapter,
74
+ detectDialect,
75
+ findMigrationDirectory
76
+ };
77
+ //# sourceMappingURL=chunk-HDO5P6X7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils.ts","../src/detect-adapter.ts"],"sourcesContent":["/** Format a date as YYYYMMDDHHMMSS for migration file/directory names. */\nexport function formatTimestamp(date: Date): string {\n const y = date.getFullYear();\n const m = String(date.getMonth() + 1).padStart(2, \"0\");\n const d = String(date.getDate()).padStart(2, \"0\");\n const h = String(date.getHours()).padStart(2, \"0\");\n const min = String(date.getMinutes()).padStart(2, \"0\");\n const sec = String(date.getSeconds()).padStart(2, \"0\");\n return `${y}${m}${d}${h}${min}${sec}`;\n}\n","/**\n * Auto-detection utilities for ORM adapter, database dialect, and migration directories.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { formatTimestamp } from \"./utils.js\";\n\nexport type DatabaseDialect = \"postgres\" | \"mysql\" | \"sqlite\";\n\nexport type AdapterType = \"drizzle\" | \"prisma\";\n\n/**\n * Detect the ORM adapter from `package.json` dependencies.\n * Prefers drizzle when both are present.\n * Returns `undefined` if neither is found.\n */\nexport function detectAdapter(cwd: string): AdapterType | undefined {\n const pkgPath = join(cwd, \"package.json\");\n if (!existsSync(pkgPath)) {\n return undefined;\n }\n\n try {\n const content = readFileSync(pkgPath, \"utf-8\");\n const parsed: unknown = JSON.parse(content);\n if (typeof parsed !== \"object\" || parsed === null) {\n return undefined;\n }\n\n const allDeps: Record<string, unknown> = {};\n if (\"dependencies\" in parsed && typeof parsed.dependencies === \"object\" && parsed.dependencies !== null) {\n Object.assign(allDeps, parsed.dependencies);\n }\n if (\"devDependencies\" in parsed && typeof parsed.devDependencies === \"object\" && parsed.devDependencies !== null) {\n Object.assign(allDeps, parsed.devDependencies);\n }\n\n if (\"drizzle-orm\" in allDeps) {\n return \"drizzle\";\n }\n if (\"@prisma/client\" in allDeps) {\n return \"prisma\";\n }\n } catch {\n // Invalid JSON or read error — skip detection\n }\n\n return undefined;\n}\n\n/**\n * Detect the database dialect from a connection URL.\n * Falls back to `\"postgres\"` if the URL is missing or unrecognizable.\n */\nexport function detectDialect(databaseUrl?: string): DatabaseDialect {\n if (databaseUrl === undefined) {\n return \"postgres\";\n }\n\n const lower = databaseUrl.toLowerCase();\n\n if (lower.startsWith(\"postgres://\") || lower.startsWith(\"postgresql://\")) {\n return \"postgres\";\n }\n if (lower.startsWith(\"mysql://\")) {\n return \"mysql\";\n }\n if (lower.startsWith(\"file:\") || lower.endsWith(\".db\") || lower.endsWith(\".sqlite\") || lower.endsWith(\".sqlite3\")) {\n return \"sqlite\";\n }\n\n return \"postgres\";\n}\n\n/**\n * Determine the migration output directory based on the detected adapter.\n *\n * - **Drizzle**: `./drizzle/` (or `./supabase/migrations/` if that directory exists)\n * - **Prisma**: `./prisma/migrations/<timestamp_add_audit_logs>/migration.sql`\n */\nexport function findMigrationDirectory(\n cwd: string,\n adapter: AdapterType,\n): string {\n if (adapter === \"drizzle\") {\n const supabasePath = join(cwd, \"supabase\", \"migrations\");\n if (existsSync(supabasePath)) {\n return supabasePath;\n }\n return join(cwd, \"drizzle\");\n }\n\n // prisma\n const timestamp = formatTimestamp(new Date());\n return join(cwd, \"prisma\", \"migrations\", `${timestamp}_add_audit_logs`);\n}\n\n"],"mappings":";AACO,SAAS,gBAAgB,MAAoB;AAClD,QAAM,IAAI,KAAK,YAAY;AAC3B,QAAM,IAAI,OAAO,KAAK,SAAS,IAAI,CAAC,EAAE,SAAS,GAAG,GAAG;AACrD,QAAM,IAAI,OAAO,KAAK,QAAQ,CAAC,EAAE,SAAS,GAAG,GAAG;AAChD,QAAM,IAAI,OAAO,KAAK,SAAS,CAAC,EAAE,SAAS,GAAG,GAAG;AACjD,QAAM,MAAM,OAAO,KAAK,WAAW,CAAC,EAAE,SAAS,GAAG,GAAG;AACrD,QAAM,MAAM,OAAO,KAAK,WAAW,CAAC,EAAE,SAAS,GAAG,GAAG;AACrD,SAAO,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,GAAG,GAAG;AACrC;;;ACLA,SAAS,YAAY,oBAAoB;AACzC,SAAS,YAAY;AAYd,SAAS,cAAc,KAAsC;AAClE,QAAM,UAAU,KAAK,KAAK,cAAc;AACxC,MAAI,CAAC,WAAW,OAAO,GAAG;AACxB,WAAO;AAAA,EACT;AAEA,MAAI;AACF,UAAM,UAAU,aAAa,SAAS,OAAO;AAC7C,UAAM,SAAkB,KAAK,MAAM,OAAO;AAC1C,QAAI,OAAO,WAAW,YAAY,WAAW,MAAM;AACjD,aAAO;AAAA,IACT;AAEA,UAAM,UAAmC,CAAC;AAC1C,QAAI,kBAAkB,UAAU,OAAO,OAAO,iBAAiB,YAAY,OAAO,iBAAiB,MAAM;AACvG,aAAO,OAAO,SAAS,OAAO,YAAY;AAAA,IAC5C;AACA,QAAI,qBAAqB,UAAU,OAAO,OAAO,oBAAoB,YAAY,OAAO,oBAAoB,MAAM;AAChH,aAAO,OAAO,SAAS,OAAO,eAAe;AAAA,IAC/C;AAEA,QAAI,iBAAiB,SAAS;AAC5B,aAAO;AAAA,IACT;AACA,QAAI,oBAAoB,SAAS;AAC/B,aAAO;AAAA,IACT;AAAA,EACF,QAAQ;AAAA,EAER;AAEA,SAAO;AACT;AAMO,SAAS,cAAc,aAAuC;AACnE,MAAI,gBAAgB,QAAW;AAC7B,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,YAAY,YAAY;AAEtC,MAAI,MAAM,WAAW,aAAa,KAAK,MAAM,WAAW,eAAe,GAAG;AACxE,WAAO;AAAA,EACT;AACA,MAAI,MAAM,WAAW,UAAU,GAAG;AAChC,WAAO;AAAA,EACT;AACA,MAAI,MAAM,WAAW,OAAO,KAAK,MAAM,SAAS,KAAK,KAAK,MAAM,SAAS,SAAS,KAAK,MAAM,SAAS,UAAU,GAAG;AACjH,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAQO,SAAS,uBACd,KACA,SACQ;AACR,MAAI,YAAY,WAAW;AACzB,UAAM,eAAe,KAAK,KAAK,YAAY,YAAY;AACvD,QAAI,WAAW,YAAY,GAAG;AAC5B,aAAO;AAAA,IACT;AACA,WAAO,KAAK,KAAK,SAAS;AAAA,EAC5B;AAGA,QAAM,YAAY,gBAAgB,oBAAI,KAAK,CAAC;AAC5C,SAAO,KAAK,KAAK,UAAU,cAAc,GAAG,SAAS,iBAAiB;AACxE;","names":[]}