@beignet/provider-drizzle-turso 0.0.1 → 0.0.3

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # @beignet/provider-drizzle-turso
2
2
 
3
+ ## 0.0.3
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [3160184]
8
+ - Updated dependencies [254ef6d]
9
+ - Updated dependencies [4cb1784]
10
+ - Updated dependencies [8bd9085]
11
+ - @beignet/core@0.0.3
12
+
13
+ ## 0.0.2
14
+
15
+ ### Patch Changes
16
+
17
+ - 90b29ad: Add pagination primitives and generate list responses with `items` and `page`.
18
+ - 07fa19c: Add durable outbox primitives for transactionally recording events and jobs, plus a Drizzle/Turso outbox adapter.
19
+ - Updated dependencies [90b29ad]
20
+ - Updated dependencies [07fa19c]
21
+ - Updated dependencies [08bae67]
22
+ - Updated dependencies [730a818]
23
+ - Updated dependencies [a79f60c]
24
+ - @beignet/core@0.0.2
25
+
3
26
  ## 0.0.1
4
27
 
5
28
  - Initial Beignet release under the `@beignet` npm scope.
package/README.md CHANGED
@@ -113,7 +113,8 @@ infrastructure, then wire the repository into `ctx.ports`.
113
113
 
114
114
  ```ts
115
115
  // infra/todos/drizzle-todo-repository.ts
116
- import { desc } from "drizzle-orm";
116
+ import { count, desc } from "drizzle-orm";
117
+ import { offsetPageResult } from "@beignet/core/pagination";
117
118
  import type { DrizzleTursoDatabase } from "@beignet/provider-drizzle-turso";
118
119
  import type { TodoRepository } from "@/features/todos/ports";
119
120
  import * as schema from "@/infra/db/schema";
@@ -123,12 +124,15 @@ export function createTodoRepository(
123
124
  ): TodoRepository {
124
125
  return {
125
126
  async list(input) {
126
- return db
127
+ const rows = await db
127
128
  .select()
128
129
  .from(schema.todos)
129
130
  .orderBy(desc(schema.todos.createdAt))
130
131
  .limit(input.limit)
131
132
  .offset(input.offset);
133
+ const [{ total }] = await db.select({ total: count() }).from(schema.todos);
134
+
135
+ return offsetPageResult(rows, input, total);
132
136
  },
133
137
  };
134
138
  }
@@ -151,8 +155,12 @@ export function createRepositories(db: DrizzleTursoDatabase<typeof schema>) {
151
155
 
152
156
  ```ts
153
157
  // features/todos/use-cases/list-todos.ts
158
+ import { normalizeOffsetPage } from "@beignet/core/pagination";
159
+
154
160
  export const listTodos = useCase.query("todos.list").run(async ({ ctx }) => {
155
- return ctx.ports.todos.list({ limit: 20, offset: 0 });
161
+ const page = normalizeOffsetPage({}, { defaultLimit: 20, maxLimit: 100 });
162
+
163
+ return ctx.ports.todos.list(page);
156
164
  });
157
165
  ```
158
166
 
@@ -265,6 +273,62 @@ throws, events are not flushed. If event publishing fails after commit,
265
273
  `transaction(...)` rejects but the database transaction is already committed;
266
274
  use an outbox when events or jobs need durable delivery guarantees.
267
275
 
276
+ ## Durable outbox
277
+
278
+ Use `createDrizzleTursoOutboxPort(...)` when events or jobs must be recorded in
279
+ the same database transaction as the business write, then drained after commit:
280
+
281
+ ```ts
282
+ import {
283
+ createOutboxEventRecorder,
284
+ drainOutbox,
285
+ } from "@beignet/core/outbox";
286
+ import {
287
+ createDrizzleTursoOutboxPort,
288
+ createDrizzleTursoOutboxSetupStatements,
289
+ createDrizzleTursoUnitOfWork,
290
+ } from "@beignet/provider-drizzle-turso";
291
+ ```
292
+
293
+ Add Beignet's outbox table to your app-owned migration or bootstrap flow:
294
+
295
+ ```ts
296
+ for (const statement of createDrizzleTursoOutboxSetupStatements()) {
297
+ await client.execute(statement);
298
+ }
299
+ ```
300
+
301
+ Then expose transaction-scoped outbox-backed event recording:
302
+
303
+ ```ts
304
+ uow: createDrizzleTursoUnitOfWork({
305
+ db: ports.db.db,
306
+ createTransactionPorts: (tx) => {
307
+ const outbox = createDrizzleTursoOutboxPort(tx);
308
+
309
+ return {
310
+ ...createRepositories(tx),
311
+ events: createOutboxEventRecorder(outbox),
312
+ outbox,
313
+ };
314
+ },
315
+ });
316
+ ```
317
+
318
+ Drain from a worker, cron route, or scheduled task:
319
+
320
+ ```ts
321
+ await drainOutbox({
322
+ outbox: ports.outbox,
323
+ registry: outboxRegistry,
324
+ eventBus: ports.eventBus,
325
+ jobs: ports.jobs,
326
+ });
327
+ ```
328
+
329
+ The default table is `outbox_messages`; pass `tableName` to both setup and port
330
+ creation if your app uses a different table name.
331
+
268
332
  ## Configuration
269
333
 
270
334
  The provider reads configuration from environment variables with the `TURSO_` prefix:
@@ -410,6 +474,22 @@ Factory function to create a transaction-backed `UnitOfWorkPort`.
410
474
  **Returns:** A `UnitOfWorkPort<TxPorts>` that runs work inside
411
475
  `db.transaction(...)`.
412
476
 
477
+ ### `createDrizzleTursoOutboxPort<TSchema>(db, options?)`
478
+
479
+ Factory function to create a SQL-backed `OutboxPort` from a root Drizzle
480
+ database or transaction client.
481
+
482
+ **Parameters:**
483
+ - `db` (required): A `DrizzleTursoDatabase<TSchema>` root database or transaction
484
+ - `options.tableName` (optional): Outbox table name, defaults to `"outbox_messages"`
485
+ - `options.now` (optional): Test clock
486
+
487
+ ### `createDrizzleTursoOutboxSetupStatements(options?)`
488
+
489
+ Returns SQL setup statements for the app-owned outbox table and indexes. Run
490
+ these through your migration/bootstrap flow or translate them into your normal
491
+ Drizzle migrations.
492
+
413
493
  ## License
414
494
 
415
495
  MIT
package/dist/index.d.ts CHANGED
@@ -25,9 +25,10 @@
25
25
  * const todos = createDrizzleTodoRepository(ctx.ports.db.db);
26
26
  * ```
27
27
  */
28
+ import { type OutboxPort } from "@beignet/core/outbox";
28
29
  import { type BufferedDomainEventRecorder, type EventBusPort, type UnitOfWorkPort } from "@beignet/core/ports";
29
30
  import { type Client } from "@libsql/client";
30
- import type { ExtractTablesWithRelations } from "drizzle-orm";
31
+ import { type ExtractTablesWithRelations } from "drizzle-orm";
31
32
  import { type LibSQLDatabase } from "drizzle-orm/libsql";
32
33
  import type { LibSQLTransaction } from "drizzle-orm/libsql/session";
33
34
  import type { SQLiteTransactionConfig } from "drizzle-orm/sqlite-core";
@@ -58,6 +59,9 @@ export type DrizzleTursoDatabase<TSchema extends Record<string, unknown> = Recor
58
59
  * Drizzle transaction client passed to Unit of Work repository factories.
59
60
  */
60
61
  export type DrizzleTursoTransaction<TSchema extends Record<string, unknown> = Record<string, unknown>> = LibSQLTransaction<TSchema, ExtractTablesWithRelations<TSchema>>;
62
+ /**
63
+ * Options for creating a Drizzle/Turso-backed Unit of Work port.
64
+ */
61
65
  export interface DrizzleTursoUnitOfWorkOptions<TSchema extends Record<string, unknown>, TxPorts> {
62
66
  /**
63
67
  * The root Drizzle database instance from `ctx.ports.db.db`.
@@ -87,6 +91,35 @@ export interface DrizzleTursoUnitOfWorkOptions<TSchema extends Record<string, un
87
91
  * decides which transaction-scoped ports to create.
88
92
  */
89
93
  export declare function createDrizzleTursoUnitOfWork<TSchema extends Record<string, unknown>, TxPorts>(options: DrizzleTursoUnitOfWorkOptions<TSchema, TxPorts>): UnitOfWorkPort<TxPorts>;
94
+ /**
95
+ * Options for a Drizzle/Turso-backed outbox port.
96
+ */
97
+ export interface DrizzleTursoOutboxOptions {
98
+ /**
99
+ * Table that stores outbox messages.
100
+ *
101
+ * Default: "outbox_messages".
102
+ */
103
+ tableName?: string;
104
+ /**
105
+ * Clock used by tests and deterministic environments.
106
+ */
107
+ now?: () => Date;
108
+ }
109
+ /**
110
+ * Create idempotent SQL statements for the Drizzle/Turso outbox table.
111
+ *
112
+ * Run these during migrations or local setup before using
113
+ * `createDrizzleTursoOutboxPort(...)`.
114
+ */
115
+ export declare function createDrizzleTursoOutboxSetupStatements(options?: DrizzleTursoOutboxOptions): readonly string[];
116
+ /**
117
+ * Create a Beignet outbox port backed by a Drizzle/Turso database.
118
+ *
119
+ * Claiming uses a claim-token lease so concurrent workers can safely compete
120
+ * for eligible messages.
121
+ */
122
+ export declare function createDrizzleTursoOutboxPort<TSchema extends Record<string, unknown>>(db: DrizzleTursoDatabase<TSchema>, adapterOptions?: DrizzleTursoOutboxOptions): OutboxPort;
90
123
  /**
91
124
  * Configuration schema for the Drizzle Turso provider.
92
125
  * Validates environment variables with TURSO_ prefix.
@@ -96,7 +129,7 @@ declare const TursoConfigSchema: z.ZodObject<{
96
129
  DB_AUTH_TOKEN: z.ZodOptional<z.ZodString>;
97
130
  }, z.core.$strip>;
98
131
  /**
99
- * Inferred configuration type for Turso provider.
132
+ * Turso provider config loaded from `TURSO_*` env vars.
100
133
  */
101
134
  export type TursoConfig = z.infer<typeof TursoConfigSchema>;
102
135
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -25,9 +25,11 @@
25
25
  * const todos = createDrizzleTodoRepository(ctx.ports.db.db);
26
26
  * ```
27
27
  */
28
+ import { createOutboxMessage, DEFAULT_OUTBOX_LEASE_MS, OutboxClaimError, serializeOutboxError, } from "@beignet/core/outbox";
28
29
  import { createDomainEventRecorder, } from "@beignet/core/ports";
29
30
  import { createProvider, createProviderInstrumentation, } from "@beignet/core/providers";
30
31
  import { createClient } from "@libsql/client";
32
+ import { sql } from "drizzle-orm";
31
33
  import { drizzle } from "drizzle-orm/libsql";
32
34
  import { z } from "zod";
33
35
  /**
@@ -52,6 +54,276 @@ export function createDrizzleTursoUnitOfWork(options) {
52
54
  },
53
55
  };
54
56
  }
57
+ function assertSafeIdentifier(name) {
58
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
59
+ throw new Error(`Unsafe SQL identifier "${name}".`);
60
+ }
61
+ }
62
+ function quoteIdentifier(name) {
63
+ assertSafeIdentifier(name);
64
+ return `"${name.replaceAll('"', '""')}"`;
65
+ }
66
+ function outboxTable(options = {}) {
67
+ const tableName = options.tableName ?? "outbox_messages";
68
+ return sql.raw(quoteIdentifier(tableName));
69
+ }
70
+ function nowFrom(options) {
71
+ return options.now?.() ?? new Date();
72
+ }
73
+ function parseNullableDate(value) {
74
+ return value ? new Date(value) : null;
75
+ }
76
+ function parseLastError(value) {
77
+ return value ? JSON.parse(value) : null;
78
+ }
79
+ function rowToMessage(row) {
80
+ return {
81
+ id: row.id,
82
+ kind: row.kind,
83
+ name: row.name,
84
+ payload: JSON.parse(row.payload_json),
85
+ status: row.status,
86
+ attempts: row.attempts,
87
+ maxAttempts: row.max_attempts,
88
+ availableAt: new Date(row.available_at),
89
+ claimedAt: parseNullableDate(row.claimed_at),
90
+ lockedUntil: parseNullableDate(row.locked_until),
91
+ claimToken: row.claim_token,
92
+ deliveredAt: parseNullableDate(row.delivered_at),
93
+ lastError: parseLastError(row.last_error_json),
94
+ createdAt: new Date(row.created_at),
95
+ updatedAt: new Date(row.updated_at),
96
+ };
97
+ }
98
+ function rowToClaimedMessage(row) {
99
+ const message = rowToMessage(row);
100
+ if (message.status !== "claimed" ||
101
+ !message.claimToken ||
102
+ !message.claimedAt ||
103
+ !message.lockedUntil) {
104
+ throw new OutboxClaimError({
105
+ id: message.id,
106
+ message: `Outbox message "${message.id}" is not claimed.`,
107
+ });
108
+ }
109
+ return {
110
+ ...message,
111
+ status: "claimed",
112
+ claimToken: message.claimToken,
113
+ claimedAt: message.claimedAt,
114
+ lockedUntil: message.lockedUntil,
115
+ };
116
+ }
117
+ function createClaimToken() {
118
+ return crypto.randomUUID();
119
+ }
120
+ async function getOutboxRow(db, tableName, id) {
121
+ return db.get(sql `select * from ${tableName} where id = ${id}`);
122
+ }
123
+ function assertClaimMutationSucceeded(input, result) {
124
+ const rowsAffected = result.rowsAffected;
125
+ if (typeof rowsAffected === "number" && rowsAffected <= 0) {
126
+ throw new OutboxClaimError({
127
+ id: input.id,
128
+ message: `Outbox message "${input.id}" is not claimed by this worker.`,
129
+ });
130
+ }
131
+ }
132
+ function assertClaimedRowSucceeded(row, input) {
133
+ if (!row || row.status !== input.expectedStatus || row.claim_token !== null) {
134
+ throw new OutboxClaimError({
135
+ id: input.id,
136
+ message: `Outbox message "${input.id}" is not claimed by this worker.`,
137
+ });
138
+ }
139
+ }
140
+ /**
141
+ * Create idempotent SQL statements for the Drizzle/Turso outbox table.
142
+ *
143
+ * Run these during migrations or local setup before using
144
+ * `createDrizzleTursoOutboxPort(...)`.
145
+ */
146
+ export function createDrizzleTursoOutboxSetupStatements(options = {}) {
147
+ const tableName = options.tableName ?? "outbox_messages";
148
+ const table = quoteIdentifier(tableName);
149
+ const indexPrefix = tableName.replaceAll(/[^A-Za-z0-9_]/g, "_");
150
+ return [
151
+ `CREATE TABLE IF NOT EXISTS ${table} (
152
+ id text PRIMARY KEY NOT NULL,
153
+ kind text NOT NULL,
154
+ name text NOT NULL,
155
+ payload_json text NOT NULL,
156
+ status text NOT NULL,
157
+ attempts integer DEFAULT 0 NOT NULL,
158
+ max_attempts integer DEFAULT 3 NOT NULL,
159
+ available_at text NOT NULL,
160
+ claimed_at text,
161
+ locked_until text,
162
+ claim_token text,
163
+ delivered_at text,
164
+ last_error_json text,
165
+ created_at text NOT NULL,
166
+ updated_at text NOT NULL
167
+ )`,
168
+ `CREATE INDEX IF NOT EXISTS ${quoteIdentifier(`${indexPrefix}_available_idx`)} ON ${table} (status, available_at)`,
169
+ `CREATE INDEX IF NOT EXISTS ${quoteIdentifier(`${indexPrefix}_locked_idx`)} ON ${table} (status, locked_until)`,
170
+ ];
171
+ }
172
+ /**
173
+ * Create a Beignet outbox port backed by a Drizzle/Turso database.
174
+ *
175
+ * Claiming uses a claim-token lease so concurrent workers can safely compete
176
+ * for eligible messages.
177
+ */
178
+ export function createDrizzleTursoOutboxPort(db, adapterOptions = {}) {
179
+ const table = outboxTable(adapterOptions);
180
+ async function claimWith(tx, input) {
181
+ const nowIso = input.now.toISOString();
182
+ const lockedUntilIso = new Date(input.now.getTime() + input.leaseMs).toISOString();
183
+ const rows = await tx.all(sql `select * from ${table}
184
+ where (
185
+ (status = 'pending' and available_at <= ${nowIso})
186
+ or (status = 'claimed' and locked_until is not null and locked_until <= ${nowIso})
187
+ )
188
+ order by available_at asc, created_at asc
189
+ limit ${input.limit}`);
190
+ const claimed = [];
191
+ for (const row of rows) {
192
+ const claimToken = createClaimToken();
193
+ const updateResult = await tx.run(sql `update ${table}
194
+ set
195
+ status = 'claimed',
196
+ attempts = attempts + 1,
197
+ claimed_at = ${nowIso},
198
+ locked_until = ${lockedUntilIso},
199
+ claim_token = ${claimToken},
200
+ updated_at = ${nowIso}
201
+ where id = ${row.id}
202
+ and (
203
+ (status = 'pending' and available_at <= ${nowIso})
204
+ or (status = 'claimed' and locked_until is not null and locked_until <= ${nowIso})
205
+ )`);
206
+ const rowsAffected = updateResult
207
+ .rowsAffected;
208
+ if (typeof rowsAffected === "number" && rowsAffected <= 0) {
209
+ continue;
210
+ }
211
+ const claimedRow = await tx.get(sql `select * from ${table} where id = ${row.id} and claim_token = ${claimToken}`);
212
+ if (claimedRow) {
213
+ claimed.push(rowToClaimedMessage(claimedRow));
214
+ }
215
+ }
216
+ return claimed;
217
+ }
218
+ return {
219
+ async enqueue(input) {
220
+ const message = createOutboxMessage(input, {
221
+ now: nowFrom(adapterOptions),
222
+ });
223
+ const nowIso = message.createdAt.toISOString();
224
+ await db.run(sql `insert into ${table} (
225
+ id,
226
+ kind,
227
+ name,
228
+ payload_json,
229
+ status,
230
+ attempts,
231
+ max_attempts,
232
+ available_at,
233
+ claimed_at,
234
+ locked_until,
235
+ claim_token,
236
+ delivered_at,
237
+ last_error_json,
238
+ created_at,
239
+ updated_at
240
+ ) values (
241
+ ${message.id},
242
+ ${message.kind},
243
+ ${message.name},
244
+ ${JSON.stringify(message.payload)},
245
+ ${message.status},
246
+ ${message.attempts},
247
+ ${message.maxAttempts},
248
+ ${message.availableAt.toISOString()},
249
+ null,
250
+ null,
251
+ null,
252
+ null,
253
+ null,
254
+ ${nowIso},
255
+ ${nowIso}
256
+ )`);
257
+ return message;
258
+ },
259
+ async claimBatch(input) {
260
+ const limit = input.limit;
261
+ if (!Number.isInteger(limit) || limit <= 0) {
262
+ throw new Error("limit must be a positive integer");
263
+ }
264
+ const now = input.now ?? nowFrom(adapterOptions);
265
+ const leaseMs = input.leaseMs ?? DEFAULT_OUTBOX_LEASE_MS;
266
+ if (!Number.isInteger(leaseMs) || leaseMs <= 0) {
267
+ throw new Error("leaseMs must be a positive integer");
268
+ }
269
+ const claimInput = { limit, now, leaseMs };
270
+ if ("transaction" in db && typeof db.transaction === "function") {
271
+ return db.transaction((tx) => claimWith(tx, claimInput));
272
+ }
273
+ return claimWith(db, claimInput);
274
+ },
275
+ async markDelivered(input) {
276
+ const nowIso = (input.now ?? nowFrom(adapterOptions)).toISOString();
277
+ const result = await db.run(sql `update ${table}
278
+ set
279
+ status = 'delivered',
280
+ delivered_at = ${nowIso},
281
+ claimed_at = null,
282
+ locked_until = null,
283
+ claim_token = null,
284
+ updated_at = ${nowIso}
285
+ where id = ${input.id}
286
+ and status = 'claimed'
287
+ and claim_token = ${input.claimToken}`);
288
+ assertClaimMutationSucceeded({
289
+ id: input.id,
290
+ expectedStatus: "delivered",
291
+ }, result);
292
+ assertClaimedRowSucceeded(await getOutboxRow(db, table, input.id), {
293
+ id: input.id,
294
+ expectedStatus: "delivered",
295
+ });
296
+ },
297
+ async markFailed(input) {
298
+ const now = input.now ?? nowFrom(adapterOptions);
299
+ const nowIso = now.toISOString();
300
+ const status = input.deadLetter
301
+ ? "deadLettered"
302
+ : "pending";
303
+ const availableAt = input.retryAt ?? now;
304
+ const result = await db.run(sql `update ${table}
305
+ set
306
+ status = ${status},
307
+ available_at = ${availableAt.toISOString()},
308
+ claimed_at = null,
309
+ locked_until = null,
310
+ claim_token = null,
311
+ last_error_json = ${JSON.stringify(serializeOutboxError(input.error))},
312
+ updated_at = ${nowIso}
313
+ where id = ${input.id}
314
+ and status = 'claimed'
315
+ and claim_token = ${input.claimToken}`);
316
+ assertClaimMutationSucceeded({
317
+ id: input.id,
318
+ expectedStatus: status,
319
+ }, result);
320
+ assertClaimedRowSucceeded(await getOutboxRow(db, table, input.id), {
321
+ id: input.id,
322
+ expectedStatus: status,
323
+ });
324
+ },
325
+ };
326
+ }
55
327
  /**
56
328
  * Configuration schema for the Drizzle Turso provider.
57
329
  * Validates environment variables with TURSO_ prefix.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beignet/provider-drizzle-turso",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "type": "module",
5
5
  "description": "Drizzle ORM + Turso/libSQL provider for Beignet - adds typed DbPort using drizzle-orm",
6
6
  "main": "./dist/index.js",
package/src/index.ts CHANGED
@@ -26,6 +26,20 @@
26
26
  * ```
27
27
  */
28
28
 
29
+ import {
30
+ type ClaimedOutboxMessage,
31
+ createOutboxMessage,
32
+ DEFAULT_OUTBOX_LEASE_MS,
33
+ OutboxClaimError,
34
+ type OutboxEnqueueInput,
35
+ type OutboxErrorInfo,
36
+ type OutboxJsonValue,
37
+ type OutboxMessage,
38
+ type OutboxMessageKind,
39
+ type OutboxMessageStatus,
40
+ type OutboxPort,
41
+ serializeOutboxError,
42
+ } from "@beignet/core/outbox";
29
43
  import {
30
44
  type BufferedDomainEventRecorder,
31
45
  createDomainEventRecorder,
@@ -38,7 +52,7 @@ import {
38
52
  createProviderInstrumentation,
39
53
  } from "@beignet/core/providers";
40
54
  import { type Client, createClient } from "@libsql/client";
41
- import type { ExtractTablesWithRelations } from "drizzle-orm";
55
+ import { type ExtractTablesWithRelations, sql } from "drizzle-orm";
42
56
  import { drizzle, type LibSQLDatabase } from "drizzle-orm/libsql";
43
57
  import type { LibSQLTransaction } from "drizzle-orm/libsql/session";
44
58
  import type { SQLiteTransactionConfig } from "drizzle-orm/sqlite-core";
@@ -82,6 +96,9 @@ export type DrizzleTursoTransaction<
82
96
  TSchema extends Record<string, unknown> = Record<string, unknown>,
83
97
  > = LibSQLTransaction<TSchema, ExtractTablesWithRelations<TSchema>>;
84
98
 
99
+ /**
100
+ * Options for creating a Drizzle/Turso-backed Unit of Work port.
101
+ */
85
102
  export interface DrizzleTursoUnitOfWorkOptions<
86
103
  TSchema extends Record<string, unknown>,
87
104
  TxPorts,
@@ -149,6 +166,378 @@ export function createDrizzleTursoUnitOfWork<
149
166
  };
150
167
  }
151
168
 
169
+ /**
170
+ * Options for a Drizzle/Turso-backed outbox port.
171
+ */
172
+ export interface DrizzleTursoOutboxOptions {
173
+ /**
174
+ * Table that stores outbox messages.
175
+ *
176
+ * Default: "outbox_messages".
177
+ */
178
+ tableName?: string;
179
+
180
+ /**
181
+ * Clock used by tests and deterministic environments.
182
+ */
183
+ now?: () => Date;
184
+ }
185
+
186
+ interface DrizzleTursoOutboxRow {
187
+ id: string;
188
+ kind: OutboxMessageKind;
189
+ name: string;
190
+ payload_json: string;
191
+ status: OutboxMessageStatus;
192
+ attempts: number;
193
+ max_attempts: number;
194
+ available_at: string;
195
+ claimed_at: string | null;
196
+ locked_until: string | null;
197
+ claim_token: string | null;
198
+ delivered_at: string | null;
199
+ last_error_json: string | null;
200
+ created_at: string;
201
+ updated_at: string;
202
+ }
203
+
204
+ function assertSafeIdentifier(name: string): void {
205
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
206
+ throw new Error(`Unsafe SQL identifier "${name}".`);
207
+ }
208
+ }
209
+
210
+ function quoteIdentifier(name: string): string {
211
+ assertSafeIdentifier(name);
212
+ return `"${name.replaceAll('"', '""')}"`;
213
+ }
214
+
215
+ function outboxTable(options: DrizzleTursoOutboxOptions = {}) {
216
+ const tableName = options.tableName ?? "outbox_messages";
217
+ return sql.raw(quoteIdentifier(tableName));
218
+ }
219
+
220
+ function nowFrom(options: DrizzleTursoOutboxOptions): Date {
221
+ return options.now?.() ?? new Date();
222
+ }
223
+
224
+ function parseNullableDate(value: string | null): Date | null {
225
+ return value ? new Date(value) : null;
226
+ }
227
+
228
+ function parseLastError(value: string | null): OutboxErrorInfo | null {
229
+ return value ? (JSON.parse(value) as OutboxErrorInfo) : null;
230
+ }
231
+
232
+ function rowToMessage(row: DrizzleTursoOutboxRow): OutboxMessage {
233
+ return {
234
+ id: row.id,
235
+ kind: row.kind,
236
+ name: row.name,
237
+ payload: JSON.parse(row.payload_json) as OutboxJsonValue,
238
+ status: row.status,
239
+ attempts: row.attempts,
240
+ maxAttempts: row.max_attempts,
241
+ availableAt: new Date(row.available_at),
242
+ claimedAt: parseNullableDate(row.claimed_at),
243
+ lockedUntil: parseNullableDate(row.locked_until),
244
+ claimToken: row.claim_token,
245
+ deliveredAt: parseNullableDate(row.delivered_at),
246
+ lastError: parseLastError(row.last_error_json),
247
+ createdAt: new Date(row.created_at),
248
+ updatedAt: new Date(row.updated_at),
249
+ };
250
+ }
251
+
252
+ function rowToClaimedMessage(row: DrizzleTursoOutboxRow): ClaimedOutboxMessage {
253
+ const message = rowToMessage(row);
254
+ if (
255
+ message.status !== "claimed" ||
256
+ !message.claimToken ||
257
+ !message.claimedAt ||
258
+ !message.lockedUntil
259
+ ) {
260
+ throw new OutboxClaimError({
261
+ id: message.id,
262
+ message: `Outbox message "${message.id}" is not claimed.`,
263
+ });
264
+ }
265
+
266
+ return {
267
+ ...message,
268
+ status: "claimed",
269
+ claimToken: message.claimToken,
270
+ claimedAt: message.claimedAt,
271
+ lockedUntil: message.lockedUntil,
272
+ };
273
+ }
274
+
275
+ function createClaimToken(): string {
276
+ return crypto.randomUUID();
277
+ }
278
+
279
+ async function getOutboxRow<TSchema extends Record<string, unknown>>(
280
+ db: DrizzleTursoDatabase<TSchema>,
281
+ tableName: ReturnType<typeof outboxTable>,
282
+ id: string,
283
+ ): Promise<DrizzleTursoOutboxRow | undefined> {
284
+ return db.get<DrizzleTursoOutboxRow>(
285
+ sql`select * from ${tableName} where id = ${id}`,
286
+ );
287
+ }
288
+
289
+ function assertClaimMutationSucceeded(
290
+ input: { id: string; expectedStatus: OutboxMessageStatus },
291
+ result: unknown,
292
+ ): void {
293
+ const rowsAffected = (result as { rowsAffected?: unknown }).rowsAffected;
294
+ if (typeof rowsAffected === "number" && rowsAffected <= 0) {
295
+ throw new OutboxClaimError({
296
+ id: input.id,
297
+ message: `Outbox message "${input.id}" is not claimed by this worker.`,
298
+ });
299
+ }
300
+ }
301
+
302
+ function assertClaimedRowSucceeded(
303
+ row: DrizzleTursoOutboxRow | undefined,
304
+ input: { id: string; expectedStatus: OutboxMessageStatus },
305
+ ): void {
306
+ if (!row || row.status !== input.expectedStatus || row.claim_token !== null) {
307
+ throw new OutboxClaimError({
308
+ id: input.id,
309
+ message: `Outbox message "${input.id}" is not claimed by this worker.`,
310
+ });
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Create idempotent SQL statements for the Drizzle/Turso outbox table.
316
+ *
317
+ * Run these during migrations or local setup before using
318
+ * `createDrizzleTursoOutboxPort(...)`.
319
+ */
320
+ export function createDrizzleTursoOutboxSetupStatements(
321
+ options: DrizzleTursoOutboxOptions = {},
322
+ ): readonly string[] {
323
+ const tableName = options.tableName ?? "outbox_messages";
324
+ const table = quoteIdentifier(tableName);
325
+ const indexPrefix = tableName.replaceAll(/[^A-Za-z0-9_]/g, "_");
326
+
327
+ return [
328
+ `CREATE TABLE IF NOT EXISTS ${table} (
329
+ id text PRIMARY KEY NOT NULL,
330
+ kind text NOT NULL,
331
+ name text NOT NULL,
332
+ payload_json text NOT NULL,
333
+ status text NOT NULL,
334
+ attempts integer DEFAULT 0 NOT NULL,
335
+ max_attempts integer DEFAULT 3 NOT NULL,
336
+ available_at text NOT NULL,
337
+ claimed_at text,
338
+ locked_until text,
339
+ claim_token text,
340
+ delivered_at text,
341
+ last_error_json text,
342
+ created_at text NOT NULL,
343
+ updated_at text NOT NULL
344
+ )`,
345
+ `CREATE INDEX IF NOT EXISTS ${quoteIdentifier(`${indexPrefix}_available_idx`)} ON ${table} (status, available_at)`,
346
+ `CREATE INDEX IF NOT EXISTS ${quoteIdentifier(`${indexPrefix}_locked_idx`)} ON ${table} (status, locked_until)`,
347
+ ];
348
+ }
349
+
350
+ /**
351
+ * Create a Beignet outbox port backed by a Drizzle/Turso database.
352
+ *
353
+ * Claiming uses a claim-token lease so concurrent workers can safely compete
354
+ * for eligible messages.
355
+ */
356
+ export function createDrizzleTursoOutboxPort<
357
+ TSchema extends Record<string, unknown>,
358
+ >(
359
+ db: DrizzleTursoDatabase<TSchema>,
360
+ adapterOptions: DrizzleTursoOutboxOptions = {},
361
+ ): OutboxPort {
362
+ const table = outboxTable(adapterOptions);
363
+
364
+ async function claimWith(
365
+ tx: DrizzleTursoDatabase<TSchema>,
366
+ input: { limit: number; now: Date; leaseMs: number },
367
+ ): Promise<ClaimedOutboxMessage[]> {
368
+ const nowIso = input.now.toISOString();
369
+ const lockedUntilIso = new Date(
370
+ input.now.getTime() + input.leaseMs,
371
+ ).toISOString();
372
+ const rows = await tx.all<DrizzleTursoOutboxRow>(sql`select * from ${table}
373
+ where (
374
+ (status = 'pending' and available_at <= ${nowIso})
375
+ or (status = 'claimed' and locked_until is not null and locked_until <= ${nowIso})
376
+ )
377
+ order by available_at asc, created_at asc
378
+ limit ${input.limit}`);
379
+ const claimed: ClaimedOutboxMessage[] = [];
380
+
381
+ for (const row of rows) {
382
+ const claimToken = createClaimToken();
383
+ const updateResult = await tx.run(sql`update ${table}
384
+ set
385
+ status = 'claimed',
386
+ attempts = attempts + 1,
387
+ claimed_at = ${nowIso},
388
+ locked_until = ${lockedUntilIso},
389
+ claim_token = ${claimToken},
390
+ updated_at = ${nowIso}
391
+ where id = ${row.id}
392
+ and (
393
+ (status = 'pending' and available_at <= ${nowIso})
394
+ or (status = 'claimed' and locked_until is not null and locked_until <= ${nowIso})
395
+ )`);
396
+ const rowsAffected = (updateResult as { rowsAffected?: unknown })
397
+ .rowsAffected;
398
+ if (typeof rowsAffected === "number" && rowsAffected <= 0) {
399
+ continue;
400
+ }
401
+
402
+ const claimedRow = await tx.get<DrizzleTursoOutboxRow>(
403
+ sql`select * from ${table} where id = ${row.id} and claim_token = ${claimToken}`,
404
+ );
405
+ if (claimedRow) {
406
+ claimed.push(rowToClaimedMessage(claimedRow));
407
+ }
408
+ }
409
+
410
+ return claimed;
411
+ }
412
+
413
+ return {
414
+ async enqueue(input: OutboxEnqueueInput): Promise<OutboxMessage> {
415
+ const message = createOutboxMessage(input, {
416
+ now: nowFrom(adapterOptions),
417
+ });
418
+ const nowIso = message.createdAt.toISOString();
419
+
420
+ await db.run(sql`insert into ${table} (
421
+ id,
422
+ kind,
423
+ name,
424
+ payload_json,
425
+ status,
426
+ attempts,
427
+ max_attempts,
428
+ available_at,
429
+ claimed_at,
430
+ locked_until,
431
+ claim_token,
432
+ delivered_at,
433
+ last_error_json,
434
+ created_at,
435
+ updated_at
436
+ ) values (
437
+ ${message.id},
438
+ ${message.kind},
439
+ ${message.name},
440
+ ${JSON.stringify(message.payload)},
441
+ ${message.status},
442
+ ${message.attempts},
443
+ ${message.maxAttempts},
444
+ ${message.availableAt.toISOString()},
445
+ null,
446
+ null,
447
+ null,
448
+ null,
449
+ null,
450
+ ${nowIso},
451
+ ${nowIso}
452
+ )`);
453
+
454
+ return message;
455
+ },
456
+
457
+ async claimBatch(input) {
458
+ const limit = input.limit;
459
+ if (!Number.isInteger(limit) || limit <= 0) {
460
+ throw new Error("limit must be a positive integer");
461
+ }
462
+ const now = input.now ?? nowFrom(adapterOptions);
463
+ const leaseMs = input.leaseMs ?? DEFAULT_OUTBOX_LEASE_MS;
464
+ if (!Number.isInteger(leaseMs) || leaseMs <= 0) {
465
+ throw new Error("leaseMs must be a positive integer");
466
+ }
467
+ const claimInput = { limit, now, leaseMs };
468
+
469
+ if ("transaction" in db && typeof db.transaction === "function") {
470
+ return db.transaction((tx) =>
471
+ claimWith(tx as DrizzleTursoDatabase<TSchema>, claimInput),
472
+ );
473
+ }
474
+
475
+ return claimWith(db, claimInput);
476
+ },
477
+
478
+ async markDelivered(input) {
479
+ const nowIso = (input.now ?? nowFrom(adapterOptions)).toISOString();
480
+ const result = await db.run(sql`update ${table}
481
+ set
482
+ status = 'delivered',
483
+ delivered_at = ${nowIso},
484
+ claimed_at = null,
485
+ locked_until = null,
486
+ claim_token = null,
487
+ updated_at = ${nowIso}
488
+ where id = ${input.id}
489
+ and status = 'claimed'
490
+ and claim_token = ${input.claimToken}`);
491
+
492
+ assertClaimMutationSucceeded(
493
+ {
494
+ id: input.id,
495
+ expectedStatus: "delivered",
496
+ },
497
+ result,
498
+ );
499
+ assertClaimedRowSucceeded(await getOutboxRow(db, table, input.id), {
500
+ id: input.id,
501
+ expectedStatus: "delivered",
502
+ });
503
+ },
504
+
505
+ async markFailed(input) {
506
+ const now = input.now ?? nowFrom(adapterOptions);
507
+ const nowIso = now.toISOString();
508
+ const status: OutboxMessageStatus = input.deadLetter
509
+ ? "deadLettered"
510
+ : "pending";
511
+ const availableAt = input.retryAt ?? now;
512
+
513
+ const result = await db.run(sql`update ${table}
514
+ set
515
+ status = ${status},
516
+ available_at = ${availableAt.toISOString()},
517
+ claimed_at = null,
518
+ locked_until = null,
519
+ claim_token = null,
520
+ last_error_json = ${JSON.stringify(serializeOutboxError(input.error))},
521
+ updated_at = ${nowIso}
522
+ where id = ${input.id}
523
+ and status = 'claimed'
524
+ and claim_token = ${input.claimToken}`);
525
+
526
+ assertClaimMutationSucceeded(
527
+ {
528
+ id: input.id,
529
+ expectedStatus: status,
530
+ },
531
+ result,
532
+ );
533
+ assertClaimedRowSucceeded(await getOutboxRow(db, table, input.id), {
534
+ id: input.id,
535
+ expectedStatus: status,
536
+ });
537
+ },
538
+ };
539
+ }
540
+
152
541
  /**
153
542
  * Configuration schema for the Drizzle Turso provider.
154
543
  * Validates environment variables with TURSO_ prefix.
@@ -174,7 +563,7 @@ const TursoConfigSchema = z.object({
174
563
  });
175
564
 
176
565
  /**
177
- * Inferred configuration type for Turso provider.
566
+ * Turso provider config loaded from `TURSO_*` env vars.
178
567
  */
179
568
  export type TursoConfig = z.infer<typeof TursoConfigSchema>;
180
569