@beignet/provider-db-drizzle 0.0.21 → 0.0.23

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.
@@ -30,6 +30,7 @@
30
30
  import type { IdempotencyPort } from "@beignet/core/idempotency";
31
31
  import type { OutboxPort } from "@beignet/core/outbox";
32
32
  import {
33
+ type AuditLogPort,
33
34
  type BufferedDomainEventRecorder,
34
35
  createDomainEventRecorder,
35
36
  type EventBusPort,
@@ -52,6 +53,12 @@ import { drizzle, type MySql2Database } from "drizzle-orm/mysql2";
52
53
  import type { Pool, PoolOptions } from "mysql2/promise";
53
54
  import * as mysql from "mysql2/promise";
54
55
  import { z } from "zod";
56
+ import {
57
+ createAuditLogInsertQuery,
58
+ createAuditLogSqlRow,
59
+ type DrizzleAuditLogCoreOptions,
60
+ } from "../shared/audit-core.js";
61
+ import { type DbHealth, dbHealthError, dbHealthOk } from "../shared/health.js";
55
62
  import {
56
63
  createIdempotencyPortCore,
57
64
  formatIdempotencyMutationErrorMessage,
@@ -92,8 +99,18 @@ export interface DbPort<
92
99
  * Use this for advanced operations not covered by Drizzle ORM.
93
100
  */
94
101
  pool: Pool;
102
+
103
+ /**
104
+ * Check whether the underlying database pool can run a cheap read query.
105
+ *
106
+ * Intended for app-owned readiness endpoints. This does not run migrations
107
+ * or mutate schema.
108
+ */
109
+ checkHealth(): Promise<DbHealth>;
95
110
  }
96
111
 
112
+ export type { DbHealth };
113
+
97
114
  /**
98
115
  * Drizzle database or transaction client accepted by repository factories.
99
116
  *
@@ -116,6 +133,15 @@ export type DrizzleMysqlTransaction<
116
133
  ExtractTablesWithRelations<TSchema>
117
134
  >;
118
135
 
136
+ async function checkMysqlHealth(pool: Pool): Promise<DbHealth> {
137
+ try {
138
+ await pool.query("select 1");
139
+ return dbHealthOk("mysql");
140
+ } catch (error) {
141
+ return dbHealthError("mysql", error);
142
+ }
143
+ }
144
+
119
145
  /**
120
146
  * Options for creating a Drizzle MySQL-backed Unit of Work port.
121
147
  */
@@ -186,6 +212,19 @@ export function createDrizzleMysqlUnitOfWork<
186
212
  };
187
213
  }
188
214
 
215
+ /**
216
+ * Options for a Drizzle MySQL-backed audit log port.
217
+ */
218
+ export interface DrizzleMysqlAuditLogOptions
219
+ extends DrizzleAuditLogCoreOptions {
220
+ /**
221
+ * Table that stores audit log entries.
222
+ *
223
+ * Default: "audit_log".
224
+ */
225
+ tableName?: string;
226
+ }
227
+
189
228
  /**
190
229
  * Options for a Drizzle MySQL-backed outbox port.
191
230
  */
@@ -319,6 +358,82 @@ function createMysqlExecutor<TSchema extends Record<string, unknown>>(
319
358
  };
320
359
  }
321
360
 
361
+ /**
362
+ * Create idempotent SQL statements for the Drizzle MySQL audit log table.
363
+ *
364
+ * Run these during migrations or local setup before using
365
+ * `createDrizzleMysqlAuditLogPort(...)`.
366
+ *
367
+ * Indexes are declared inline because MySQL has no
368
+ * `CREATE INDEX IF NOT EXISTS`; the whole shape stays idempotent under
369
+ * `CREATE TABLE IF NOT EXISTS`.
370
+ */
371
+ export function createDrizzleMysqlAuditLogSetupStatements(
372
+ options: DrizzleMysqlAuditLogOptions = {},
373
+ ): readonly string[] {
374
+ const tableName = options.tableName ?? "audit_log";
375
+ const table = quoteIdentifier(tableName, "`");
376
+ const indexPrefix = tableName.replaceAll(/[^A-Za-z0-9_]/g, "_");
377
+
378
+ return [
379
+ `CREATE TABLE IF NOT EXISTS ${table} (
380
+ id varchar(255) NOT NULL PRIMARY KEY,
381
+ action varchar(255) NOT NULL,
382
+ actor_type varchar(32) NOT NULL,
383
+ actor_id varchar(255),
384
+ actor_display_name varchar(255),
385
+ tenant_id varchar(255),
386
+ tenant_slug varchar(255),
387
+ resource_type varchar(255),
388
+ resource_id varchar(255),
389
+ resource_name varchar(255),
390
+ outcome varchar(16) NOT NULL DEFAULT 'success',
391
+ request_id varchar(255),
392
+ trace_id varchar(255),
393
+ message longtext,
394
+ metadata longtext,
395
+ actor_metadata longtext,
396
+ tenant_metadata longtext,
397
+ resource_metadata longtext,
398
+ occurred_at varchar(32) NOT NULL,
399
+ INDEX ${quoteIdentifier(`${indexPrefix}_action_idx`, "`")} (action),
400
+ INDEX ${quoteIdentifier(`${indexPrefix}_actor_idx`, "`")} (actor_type, actor_id),
401
+ INDEX ${quoteIdentifier(`${indexPrefix}_occurred_at_idx`, "`")} (occurred_at),
402
+ INDEX ${quoteIdentifier(`${indexPrefix}_request_idx`, "`")} (request_id),
403
+ INDEX ${quoteIdentifier(`${indexPrefix}_resource_idx`, "`")} (resource_type, resource_id),
404
+ INDEX ${quoteIdentifier(`${indexPrefix}_tenant_idx`, "`")} (tenant_id)
405
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_bin`,
406
+ ];
407
+ }
408
+
409
+ /**
410
+ * Create a Beignet audit log port backed by a Drizzle MySQL database.
411
+ *
412
+ * The port accepts both the root Drizzle database and transaction clients, so
413
+ * applications can rebuild audit logging inside Unit of Work transaction ports.
414
+ */
415
+ export function createDrizzleMysqlAuditLogPort<
416
+ TSchema extends Record<string, unknown>,
417
+ >(
418
+ db: DrizzleMysqlDatabase<TSchema>,
419
+ adapterOptions: DrizzleMysqlAuditLogOptions = {},
420
+ ): AuditLogPort {
421
+ const table = sql.raw(
422
+ quoteIdentifier(adapterOptions.tableName ?? "audit_log", "`"),
423
+ );
424
+
425
+ return {
426
+ async record(input) {
427
+ await db.execute(
428
+ createAuditLogInsertQuery(
429
+ table,
430
+ createAuditLogSqlRow(input, adapterOptions),
431
+ ),
432
+ );
433
+ },
434
+ };
435
+ }
436
+
322
437
  /**
323
438
  * Create idempotent SQL statements for the Drizzle MySQL outbox table.
324
439
  *
@@ -612,7 +727,12 @@ export function createDrizzleMysqlProvider<
612
727
  : drizzle(pool, { schema, mode });
613
728
 
614
729
  // 3. Build a typed DbPort
615
- const dbPort: DbPort<TSchema> = { drizzle: db, db, pool };
730
+ const dbPort: DbPort<TSchema> = {
731
+ drizzle: db,
732
+ db,
733
+ pool,
734
+ checkHealth: () => checkMysqlHealth(pool),
735
+ };
616
736
 
617
737
  return {
618
738
  ports: { [portName]: dbPort } as Record<PortName, DbPort<TSchema>>,
@@ -30,6 +30,7 @@
30
30
  import type { IdempotencyPort } from "@beignet/core/idempotency";
31
31
  import type { OutboxPort } from "@beignet/core/outbox";
32
32
  import {
33
+ type AuditLogPort,
33
34
  type BufferedDomainEventRecorder,
34
35
  createDomainEventRecorder,
35
36
  type EventBusPort,
@@ -50,6 +51,12 @@ import {
50
51
  } from "drizzle-orm/pg-core";
51
52
  import pg, { type Pool, type PoolConfig } from "pg";
52
53
  import { z } from "zod";
54
+ import {
55
+ createAuditLogInsertQuery,
56
+ createAuditLogSqlRow,
57
+ type DrizzleAuditLogCoreOptions,
58
+ } from "../shared/audit-core.js";
59
+ import { type DbHealth, dbHealthError, dbHealthOk } from "../shared/health.js";
53
60
  import {
54
61
  createIdempotencyPortCore,
55
62
  formatIdempotencyMutationErrorMessage,
@@ -90,8 +97,18 @@ export interface DbPort<
90
97
  * Use this for advanced operations not covered by Drizzle ORM.
91
98
  */
92
99
  pool: Pool;
100
+
101
+ /**
102
+ * Check whether the underlying database pool can run a cheap read query.
103
+ *
104
+ * Intended for app-owned readiness endpoints. This does not run migrations
105
+ * or mutate schema.
106
+ */
107
+ checkHealth(): Promise<DbHealth>;
93
108
  }
94
109
 
110
+ export type { DbHealth };
111
+
95
112
  /**
96
113
  * Drizzle database or transaction client accepted by repository factories.
97
114
  *
@@ -114,6 +131,15 @@ export type DrizzlePostgresTransaction<
114
131
  ExtractTablesWithRelations<TSchema>
115
132
  >;
116
133
 
134
+ async function checkPostgresHealth(pool: Pool): Promise<DbHealth> {
135
+ try {
136
+ await pool.query("select 1");
137
+ return dbHealthOk("postgres");
138
+ } catch (error) {
139
+ return dbHealthError("postgres", error);
140
+ }
141
+ }
142
+
117
143
  /**
118
144
  * Options for creating a Drizzle Postgres-backed Unit of Work port.
119
145
  */
@@ -181,6 +207,91 @@ export function createDrizzlePostgresUnitOfWork<
181
207
  };
182
208
  }
183
209
 
210
+ /**
211
+ * Options for a Drizzle Postgres-backed audit log port.
212
+ */
213
+ export interface DrizzlePostgresAuditLogOptions
214
+ extends DrizzleAuditLogCoreOptions {
215
+ /**
216
+ * Table that stores audit log entries.
217
+ *
218
+ * Default: "audit_log".
219
+ */
220
+ tableName?: string;
221
+ }
222
+
223
+ /**
224
+ * Create idempotent SQL statements for the Drizzle Postgres audit log table.
225
+ *
226
+ * Run these during migrations or local setup before using
227
+ * `createDrizzlePostgresAuditLogPort(...)`.
228
+ */
229
+ export function createDrizzlePostgresAuditLogSetupStatements(
230
+ options: DrizzlePostgresAuditLogOptions = {},
231
+ ): readonly string[] {
232
+ const tableName = options.tableName ?? "audit_log";
233
+ const table = quoteIdentifier(tableName, '"');
234
+ const indexPrefix = tableName.replaceAll(/[^A-Za-z0-9_]/g, "_");
235
+
236
+ return [
237
+ `CREATE TABLE IF NOT EXISTS ${table} (
238
+ id text PRIMARY KEY NOT NULL,
239
+ action text NOT NULL,
240
+ actor_type text NOT NULL,
241
+ actor_id text,
242
+ actor_display_name text,
243
+ tenant_id text,
244
+ tenant_slug text,
245
+ resource_type text,
246
+ resource_id text,
247
+ resource_name text,
248
+ outcome text DEFAULT 'success' NOT NULL,
249
+ request_id text,
250
+ trace_id text,
251
+ message text,
252
+ metadata text,
253
+ actor_metadata text,
254
+ tenant_metadata text,
255
+ resource_metadata text,
256
+ occurred_at text NOT NULL
257
+ )`,
258
+ `CREATE INDEX IF NOT EXISTS ${quoteIdentifier(`${indexPrefix}_action_idx`, '"')} ON ${table} (action)`,
259
+ `CREATE INDEX IF NOT EXISTS ${quoteIdentifier(`${indexPrefix}_actor_idx`, '"')} ON ${table} (actor_type, actor_id)`,
260
+ `CREATE INDEX IF NOT EXISTS ${quoteIdentifier(`${indexPrefix}_occurred_at_idx`, '"')} ON ${table} (occurred_at)`,
261
+ `CREATE INDEX IF NOT EXISTS ${quoteIdentifier(`${indexPrefix}_request_idx`, '"')} ON ${table} (request_id)`,
262
+ `CREATE INDEX IF NOT EXISTS ${quoteIdentifier(`${indexPrefix}_resource_idx`, '"')} ON ${table} (resource_type, resource_id)`,
263
+ `CREATE INDEX IF NOT EXISTS ${quoteIdentifier(`${indexPrefix}_tenant_idx`, '"')} ON ${table} (tenant_id)`,
264
+ ];
265
+ }
266
+
267
+ /**
268
+ * Create a Beignet audit log port backed by a Drizzle Postgres database.
269
+ *
270
+ * The port accepts both the root Drizzle database and transaction clients, so
271
+ * applications can rebuild audit logging inside Unit of Work transaction ports.
272
+ */
273
+ export function createDrizzlePostgresAuditLogPort<
274
+ TSchema extends Record<string, unknown>,
275
+ >(
276
+ db: DrizzlePostgresDatabase<TSchema>,
277
+ adapterOptions: DrizzlePostgresAuditLogOptions = {},
278
+ ): AuditLogPort {
279
+ const table = sql.raw(
280
+ quoteIdentifier(adapterOptions.tableName ?? "audit_log", '"'),
281
+ );
282
+
283
+ return {
284
+ async record(input) {
285
+ await db.execute(
286
+ createAuditLogInsertQuery(
287
+ table,
288
+ createAuditLogSqlRow(input, adapterOptions),
289
+ ),
290
+ );
291
+ },
292
+ };
293
+ }
294
+
184
295
  /**
185
296
  * Options for a Drizzle Postgres-backed outbox port.
186
297
  */
@@ -557,7 +668,12 @@ export function createDrizzlePostgresProvider<
557
668
  : drizzle(pool, { schema });
558
669
 
559
670
  // 3. Build a typed DbPort
560
- const dbPort: DbPort<TSchema> = { drizzle: db, db, pool };
671
+ const dbPort: DbPort<TSchema> = {
672
+ drizzle: db,
673
+ db,
674
+ pool,
675
+ checkHealth: () => checkPostgresHealth(pool),
676
+ };
561
677
 
562
678
  return {
563
679
  ports: { [portName]: dbPort } as Record<PortName, DbPort<TSchema>>,
@@ -0,0 +1,134 @@
1
+ import {
2
+ type ActivityActorType,
3
+ type AuditLogEntry,
4
+ type AuditLogEntryInput,
5
+ type AuditLogOptions,
6
+ normalizeAuditLogEntry,
7
+ redactAuditLogEntry,
8
+ } from "@beignet/core/ports";
9
+ import { type SQL, sql } from "drizzle-orm";
10
+
11
+ export interface DrizzleAuditLogCoreOptions extends AuditLogOptions {
12
+ /**
13
+ * Optional ID factory for deterministic tests or runtimes without
14
+ * `globalThis.crypto.randomUUID()`.
15
+ */
16
+ createId?: () => string;
17
+ }
18
+
19
+ export type AuditLogSqlRow = {
20
+ id: string;
21
+ action: string;
22
+ actorType: ActivityActorType;
23
+ actorId: string | null;
24
+ actorDisplayName: string | null;
25
+ tenantId: string | null;
26
+ tenantSlug: string | null;
27
+ resourceType: string | null;
28
+ resourceId: string | null;
29
+ resourceName: string | null;
30
+ outcome: AuditLogEntry["outcome"];
31
+ requestId: string | null;
32
+ traceId: string | null;
33
+ message: string | null;
34
+ metadata: string | null;
35
+ actorMetadata: string | null;
36
+ tenantMetadata: string | null;
37
+ resourceMetadata: string | null;
38
+ occurredAt: string;
39
+ };
40
+
41
+ function defaultCreateAuditLogId(): string {
42
+ if (typeof globalThis.crypto?.randomUUID !== "function") {
43
+ throw new Error(
44
+ "Drizzle audit log adapters require globalThis.crypto.randomUUID(). Pass createId when this runtime does not provide it.",
45
+ );
46
+ }
47
+ return globalThis.crypto.randomUUID();
48
+ }
49
+
50
+ function serialize(value: unknown): string | null {
51
+ return value === undefined ? null : JSON.stringify(value);
52
+ }
53
+
54
+ export function createAuditLogSqlRow(
55
+ input: AuditLogEntryInput,
56
+ options: DrizzleAuditLogCoreOptions = {},
57
+ ): AuditLogSqlRow {
58
+ const normalized = normalizeAuditLogEntry(input);
59
+ const entry = options.redact
60
+ ? options.redact(redactAuditLogEntry(normalized))
61
+ : redactAuditLogEntry(normalized);
62
+ const createId = options.createId ?? defaultCreateAuditLogId;
63
+
64
+ return {
65
+ id: createId(),
66
+ action: entry.action,
67
+ actorType: entry.actor.type,
68
+ actorId: entry.actor.id ?? null,
69
+ actorDisplayName: entry.actor.displayName ?? null,
70
+ tenantId: entry.tenant?.id ?? null,
71
+ tenantSlug: entry.tenant?.slug ?? null,
72
+ resourceType: entry.resource?.type ?? null,
73
+ resourceId: entry.resource?.id ?? null,
74
+ resourceName: entry.resource?.name ?? null,
75
+ outcome: entry.outcome,
76
+ requestId: entry.requestId ?? null,
77
+ traceId: entry.traceId ?? null,
78
+ message: entry.message ?? null,
79
+ metadata: serialize(entry.metadata),
80
+ actorMetadata: serialize(entry.actor.metadata),
81
+ tenantMetadata: serialize(entry.tenant?.metadata),
82
+ resourceMetadata: serialize(entry.resource?.metadata),
83
+ occurredAt: entry.occurredAt.toISOString(),
84
+ };
85
+ }
86
+
87
+ export function createAuditLogInsertQuery(
88
+ table: SQL,
89
+ row: AuditLogSqlRow,
90
+ ): SQL {
91
+ return sql`
92
+ insert into ${table} (
93
+ id,
94
+ action,
95
+ actor_type,
96
+ actor_id,
97
+ actor_display_name,
98
+ tenant_id,
99
+ tenant_slug,
100
+ resource_type,
101
+ resource_id,
102
+ resource_name,
103
+ outcome,
104
+ request_id,
105
+ trace_id,
106
+ message,
107
+ metadata,
108
+ actor_metadata,
109
+ tenant_metadata,
110
+ resource_metadata,
111
+ occurred_at
112
+ ) values (
113
+ ${row.id},
114
+ ${row.action},
115
+ ${row.actorType},
116
+ ${row.actorId},
117
+ ${row.actorDisplayName},
118
+ ${row.tenantId},
119
+ ${row.tenantSlug},
120
+ ${row.resourceType},
121
+ ${row.resourceId},
122
+ ${row.resourceName},
123
+ ${row.outcome},
124
+ ${row.requestId},
125
+ ${row.traceId},
126
+ ${row.message},
127
+ ${row.metadata},
128
+ ${row.actorMetadata},
129
+ ${row.tenantMetadata},
130
+ ${row.resourceMetadata},
131
+ ${row.occurredAt}
132
+ )
133
+ `;
134
+ }
@@ -0,0 +1,25 @@
1
+ export interface DbHealth {
2
+ ok: boolean;
3
+ message?: string;
4
+ metadata: {
5
+ adapter: "sqlite" | "postgres" | "mysql";
6
+ };
7
+ }
8
+
9
+ export function dbHealthOk(adapter: DbHealth["metadata"]["adapter"]): DbHealth {
10
+ return {
11
+ ok: true,
12
+ metadata: { adapter },
13
+ };
14
+ }
15
+
16
+ export function dbHealthError(
17
+ adapter: DbHealth["metadata"]["adapter"],
18
+ error: unknown,
19
+ ): DbHealth {
20
+ return {
21
+ ok: false,
22
+ message: error instanceof Error ? error.message : String(error),
23
+ metadata: { adapter },
24
+ };
25
+ }
@@ -32,6 +32,7 @@
32
32
  import type { IdempotencyPort } from "@beignet/core/idempotency";
33
33
  import type { OutboxPort } from "@beignet/core/outbox";
34
34
  import {
35
+ type AuditLogPort,
35
36
  type BufferedDomainEventRecorder,
36
37
  createDomainEventRecorder,
37
38
  type EventBusPort,
@@ -48,6 +49,12 @@ import { drizzle, type LibSQLDatabase } from "drizzle-orm/libsql";
48
49
  import type { LibSQLTransaction } from "drizzle-orm/libsql/session";
49
50
  import type { SQLiteTransactionConfig } from "drizzle-orm/sqlite-core";
50
51
  import { z } from "zod";
52
+ import {
53
+ createAuditLogInsertQuery,
54
+ createAuditLogSqlRow,
55
+ type DrizzleAuditLogCoreOptions,
56
+ } from "../shared/audit-core.js";
57
+ import { type DbHealth, dbHealthError, dbHealthOk } from "../shared/health.js";
51
58
  import {
52
59
  createIdempotencyPortCore,
53
60
  formatIdempotencyMutationErrorMessage,
@@ -88,8 +95,18 @@ export interface DbPort<
88
95
  * Use this for advanced operations not covered by Drizzle ORM.
89
96
  */
90
97
  client: Client;
98
+
99
+ /**
100
+ * Check whether the underlying database client can run a cheap read query.
101
+ *
102
+ * Intended for app-owned readiness endpoints. This does not run migrations
103
+ * or mutate schema.
104
+ */
105
+ checkHealth(): Promise<DbHealth>;
91
106
  }
92
107
 
108
+ export type { DbHealth };
109
+
93
110
  /**
94
111
  * Drizzle database or transaction client accepted by repository factories.
95
112
  */
@@ -106,6 +123,15 @@ export type DrizzleSqliteTransaction<
106
123
  TSchema extends Record<string, unknown> = Record<string, unknown>,
107
124
  > = LibSQLTransaction<TSchema, ExtractTablesWithRelations<TSchema>>;
108
125
 
126
+ async function checkSqliteHealth(client: Client): Promise<DbHealth> {
127
+ try {
128
+ await client.execute("select 1");
129
+ return dbHealthOk("sqlite");
130
+ } catch (error) {
131
+ return dbHealthError("sqlite", error);
132
+ }
133
+ }
134
+
109
135
  /**
110
136
  * Options for creating a Drizzle SQLite-backed Unit of Work port.
111
137
  */
@@ -176,6 +202,91 @@ export function createDrizzleSqliteUnitOfWork<
176
202
  };
177
203
  }
178
204
 
205
+ /**
206
+ * Options for a Drizzle SQLite-backed audit log port.
207
+ */
208
+ export interface DrizzleSqliteAuditLogOptions
209
+ extends DrizzleAuditLogCoreOptions {
210
+ /**
211
+ * Table that stores audit log entries.
212
+ *
213
+ * Default: "audit_log".
214
+ */
215
+ tableName?: string;
216
+ }
217
+
218
+ /**
219
+ * Create idempotent SQL statements for the Drizzle SQLite audit log table.
220
+ *
221
+ * Run these during migrations or local setup before using
222
+ * `createDrizzleSqliteAuditLogPort(...)`.
223
+ */
224
+ export function createDrizzleSqliteAuditLogSetupStatements(
225
+ options: DrizzleSqliteAuditLogOptions = {},
226
+ ): readonly string[] {
227
+ const tableName = options.tableName ?? "audit_log";
228
+ const table = quoteIdentifier(tableName, '"');
229
+ const indexPrefix = tableName.replaceAll(/[^A-Za-z0-9_]/g, "_");
230
+
231
+ return [
232
+ `CREATE TABLE IF NOT EXISTS ${table} (
233
+ id text PRIMARY KEY NOT NULL,
234
+ action text NOT NULL,
235
+ actor_type text NOT NULL,
236
+ actor_id text,
237
+ actor_display_name text,
238
+ tenant_id text,
239
+ tenant_slug text,
240
+ resource_type text,
241
+ resource_id text,
242
+ resource_name text,
243
+ outcome text DEFAULT 'success' NOT NULL,
244
+ request_id text,
245
+ trace_id text,
246
+ message text,
247
+ metadata text,
248
+ actor_metadata text,
249
+ tenant_metadata text,
250
+ resource_metadata text,
251
+ occurred_at text NOT NULL
252
+ )`,
253
+ `CREATE INDEX IF NOT EXISTS ${quoteIdentifier(`${indexPrefix}_action_idx`, '"')} ON ${table} (action)`,
254
+ `CREATE INDEX IF NOT EXISTS ${quoteIdentifier(`${indexPrefix}_actor_idx`, '"')} ON ${table} (actor_type, actor_id)`,
255
+ `CREATE INDEX IF NOT EXISTS ${quoteIdentifier(`${indexPrefix}_occurred_at_idx`, '"')} ON ${table} (occurred_at)`,
256
+ `CREATE INDEX IF NOT EXISTS ${quoteIdentifier(`${indexPrefix}_request_idx`, '"')} ON ${table} (request_id)`,
257
+ `CREATE INDEX IF NOT EXISTS ${quoteIdentifier(`${indexPrefix}_resource_idx`, '"')} ON ${table} (resource_type, resource_id)`,
258
+ `CREATE INDEX IF NOT EXISTS ${quoteIdentifier(`${indexPrefix}_tenant_idx`, '"')} ON ${table} (tenant_id)`,
259
+ ];
260
+ }
261
+
262
+ /**
263
+ * Create a Beignet audit log port backed by a Drizzle SQLite database.
264
+ *
265
+ * The port accepts both the root Drizzle database and transaction clients, so
266
+ * applications can rebuild audit logging inside Unit of Work transaction ports.
267
+ */
268
+ export function createDrizzleSqliteAuditLogPort<
269
+ TSchema extends Record<string, unknown>,
270
+ >(
271
+ db: DrizzleSqliteDatabase<TSchema>,
272
+ adapterOptions: DrizzleSqliteAuditLogOptions = {},
273
+ ): AuditLogPort {
274
+ const table = sql.raw(
275
+ quoteIdentifier(adapterOptions.tableName ?? "audit_log", '"'),
276
+ );
277
+
278
+ return {
279
+ async record(input) {
280
+ await db.run(
281
+ createAuditLogInsertQuery(
282
+ table,
283
+ createAuditLogSqlRow(input, adapterOptions),
284
+ ),
285
+ );
286
+ },
287
+ };
288
+ }
289
+
179
290
  /**
180
291
  * Options for a Drizzle SQLite-backed outbox port.
181
292
  */
@@ -532,7 +643,12 @@ export function createDrizzleSqliteProvider<
532
643
  : drizzle(client, { schema });
533
644
 
534
645
  // 3. Build a typed DbPort
535
- const dbPort: DbPort<TSchema> = { drizzle: db, db, client };
646
+ const dbPort: DbPort<TSchema> = {
647
+ drizzle: db,
648
+ db,
649
+ client,
650
+ checkHealth: () => checkSqliteHealth(client),
651
+ };
536
652
 
537
653
  return {
538
654
  ports: { [portName]: dbPort } as Record<PortName, DbPort<TSchema>>,