@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 +23 -0
- package/README.md +83 -3
- package/dist/index.d.ts +35 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +272 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +391 -2
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
132
|
+
* Turso provider config loaded from `TURSO_*` env vars.
|
|
100
133
|
*/
|
|
101
134
|
export type TursoConfig = z.infer<typeof TursoConfigSchema>;
|
|
102
135
|
/**
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EACL,KAAK,2BAA2B,EAEhC,KAAK,YAAY,EAEjB,KAAK,cAAc,EACpB,MAAM,qBAAqB,CAAC;AAK7B,OAAO,EAAE,KAAK,MAAM,EAAgB,MAAM,gBAAgB,CAAC;AAC3D,OAAO,KAAK,
|
|
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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAEL,mBAAmB,EACnB,uBAAuB,EACvB,gBAAgB,EAQhB,oBAAoB,GACrB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAEL,yBAAyB,GAI1B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,cAAc,EACd,6BAA6B,GAC9B,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAe,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC3D,OAAO,EAAmC,GAAG,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,EAAE,OAAO,EAAuB,MAAM,oBAAoB,CAAC;AAGlE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA0ExB;;;;;;GAMG;AACH,MAAM,UAAU,4BAA4B,CAI1C,OAAwD;IAExD,OAAO;QACL,KAAK,CAAC,WAAW,CACf,IAAyC;YAEzC,MAAM,MAAM,GAAG,yBAAyB,EAAE,CAAC;YAE3C,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,EAAE,CAAC,WAAW,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;gBACvD,MAAM,OAAO,GAAG,OAAO,CAAC,sBAAsB,CAC5C,EAAsC,EACtC,MAAM,CACP,CAAC;gBACF,OAAO,IAAI,CAAC,OAAO,CAAC,CAAC;YACvB,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,CAAC;YAE9B,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;gBACrB,MAAM,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;YACvC,CAAC;YAED,OAAO,MAAM,CAAC;QAChB,CAAC;KACF,CAAC;AACJ,CAAC;AAqCD,SAAS,oBAAoB,CAAC,IAAY;IACxC,IAAI,CAAC,0BAA0B,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3C,MAAM,IAAI,KAAK,CAAC,0BAA0B,IAAI,IAAI,CAAC,CAAC;IACtD,CAAC;AACH,CAAC;AAED,SAAS,eAAe,CAAC,IAAY;IACnC,oBAAoB,CAAC,IAAI,CAAC,CAAC;IAC3B,OAAO,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC;AAC3C,CAAC;AAED,SAAS,WAAW,CAAC,UAAqC,EAAE;IAC1D,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,iBAAiB,CAAC;IACzD,OAAO,GAAG,CAAC,GAAG,CAAC,eAAe,CAAC,SAAS,CAAC,CAAC,CAAC;AAC7C,CAAC;AAED,SAAS,OAAO,CAAC,OAAkC;IACjD,OAAO,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,IAAI,IAAI,EAAE,CAAC;AACvC,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAoB;IAC7C,OAAO,KAAK,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACxC,CAAC;AAED,SAAS,cAAc,CAAC,KAAoB;IAC1C,OAAO,KAAK,CAAC,CAAC,CAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAqB,CAAC,CAAC,CAAC,IAAI,CAAC;AAC/D,CAAC;AAED,SAAS,YAAY,CAAC,GAA0B;IAC9C,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,YAAY,CAAoB;QACxD,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,WAAW,EAAE,GAAG,CAAC,YAAY;QAC7B,WAAW,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC;QACvC,SAAS,EAAE,iBAAiB,CAAC,GAAG,CAAC,UAAU,CAAC;QAC5C,WAAW,EAAE,iBAAiB,CAAC,GAAG,CAAC,YAAY,CAAC;QAChD,UAAU,EAAE,GAAG,CAAC,WAAW;QAC3B,WAAW,EAAE,iBAAiB,CAAC,GAAG,CAAC,YAAY,CAAC;QAChD,SAAS,EAAE,cAAc,CAAC,GAAG,CAAC,eAAe,CAAC;QAC9C,SAAS,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC;QACnC,SAAS,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC;KACpC,CAAC;AACJ,CAAC;AAED,SAAS,mBAAmB,CAAC,GAA0B;IACrD,MAAM,OAAO,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IAClC,IACE,OAAO,CAAC,MAAM,KAAK,SAAS;QAC5B,CAAC,OAAO,CAAC,UAAU;QACnB,CAAC,OAAO,CAAC,SAAS;QAClB,CAAC,OAAO,CAAC,WAAW,EACpB,CAAC;QACD,MAAM,IAAI,gBAAgB,CAAC;YACzB,EAAE,EAAE,OAAO,CAAC,EAAE;YACd,OAAO,EAAE,mBAAmB,OAAO,CAAC,EAAE,mBAAmB;SAC1D,CAAC,CAAC;IACL,CAAC;IAED,OAAO;QACL,GAAG,OAAO;QACV,MAAM,EAAE,SAAS;QACjB,UAAU,EAAE,OAAO,CAAC,UAAU;QAC9B,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,WAAW,EAAE,OAAO,CAAC,WAAW;KACjC,CAAC;AACJ,CAAC;AAED,SAAS,gBAAgB;IACvB,OAAO,MAAM,CAAC,UAAU,EAAE,CAAC;AAC7B,CAAC;AAED,KAAK,UAAU,YAAY,CACzB,EAAiC,EACjC,SAAyC,EACzC,EAAU;IAEV,OAAO,EAAE,CAAC,GAAG,CACX,GAAG,CAAA,iBAAiB,SAAS,eAAe,EAAE,EAAE,CACjD,CAAC;AACJ,CAAC;AAED,SAAS,4BAA4B,CACnC,KAA0D,EAC1D,MAAe;IAEf,MAAM,YAAY,GAAI,MAAqC,CAAC,YAAY,CAAC;IACzE,IAAI,OAAO,YAAY,KAAK,QAAQ,IAAI,YAAY,IAAI,CAAC,EAAE,CAAC;QAC1D,MAAM,IAAI,gBAAgB,CAAC;YACzB,EAAE,EAAE,KAAK,CAAC,EAAE;YACZ,OAAO,EAAE,mBAAmB,KAAK,CAAC,EAAE,kCAAkC;SACvE,CAAC,CAAC;IACL,CAAC;AACH,CAAC;AAED,SAAS,yBAAyB,CAChC,GAAsC,EACtC,KAA0D;IAE1D,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,KAAK,KAAK,CAAC,cAAc,IAAI,GAAG,CAAC,WAAW,KAAK,IAAI,EAAE,CAAC;QAC5E,MAAM,IAAI,gBAAgB,CAAC;YACzB,EAAE,EAAE,KAAK,CAAC,EAAE;YACZ,OAAO,EAAE,mBAAmB,KAAK,CAAC,EAAE,kCAAkC;SACvE,CAAC,CAAC;IACL,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,uCAAuC,CACrD,UAAqC,EAAE;IAEvC,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,iBAAiB,CAAC;IACzD,MAAM,KAAK,GAAG,eAAe,CAAC,SAAS,CAAC,CAAC;IACzC,MAAM,WAAW,GAAG,SAAS,CAAC,UAAU,CAAC,gBAAgB,EAAE,GAAG,CAAC,CAAC;IAEhE,OAAO;QACL,8BAA8B,KAAK;;;;;;;;;;;;;;;;MAgBjC;QACF,8BAA8B,eAAe,CAAC,GAAG,WAAW,gBAAgB,CAAC,OAAO,KAAK,yBAAyB;QAClH,8BAA8B,eAAe,CAAC,GAAG,WAAW,aAAa,CAAC,OAAO,KAAK,yBAAyB;KAChH,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,4BAA4B,CAG1C,EAAiC,EACjC,iBAA4C,EAAE;IAE9C,MAAM,KAAK,GAAG,WAAW,CAAC,cAAc,CAAC,CAAC;IAE1C,KAAK,UAAU,SAAS,CACtB,EAAiC,EACjC,KAAoD;QAEpD,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;QACvC,MAAM,cAAc,GAAG,IAAI,IAAI,CAC7B,KAAK,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,KAAK,CAAC,OAAO,CACpC,CAAC,WAAW,EAAE,CAAC;QAChB,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,GAAG,CAAwB,GAAG,CAAA,iBAAiB,KAAK;;kDAE5B,MAAM;kFAC0B,MAAM;;;cAG1E,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;QACzB,MAAM,OAAO,GAA2B,EAAE,CAAC;QAE3C,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,MAAM,UAAU,GAAG,gBAAgB,EAAE,CAAC;YACtC,MAAM,YAAY,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,GAAG,CAAA,UAAU,KAAK;;;;yBAIjC,MAAM;2BACJ,cAAc;0BACf,UAAU;yBACX,MAAM;qBACV,GAAG,CAAC,EAAE;;sDAE2B,MAAM;sFAC0B,MAAM;YAChF,CAAC,CAAC;YACR,MAAM,YAAY,GAAI,YAA2C;iBAC9D,YAAY,CAAC;YAChB,IAAI,OAAO,YAAY,KAAK,QAAQ,IAAI,YAAY,IAAI,CAAC,EAAE,CAAC;gBAC1D,SAAS;YACX,CAAC;YAED,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC,GAAG,CAC7B,GAAG,CAAA,iBAAiB,KAAK,eAAe,GAAG,CAAC,EAAE,sBAAsB,UAAU,EAAE,CACjF,CAAC;YACF,IAAI,UAAU,EAAE,CAAC;gBACf,OAAO,CAAC,IAAI,CAAC,mBAAmB,CAAC,UAAU,CAAC,CAAC,CAAC;YAChD,CAAC;QACH,CAAC;QAED,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,OAAO;QACL,KAAK,CAAC,OAAO,CAAC,KAAyB;YACrC,MAAM,OAAO,GAAG,mBAAmB,CAAC,KAAK,EAAE;gBACzC,GAAG,EAAE,OAAO,CAAC,cAAc,CAAC;aAC7B,CAAC,CAAC;YACH,MAAM,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,WAAW,EAAE,CAAC;YAE/C,MAAM,EAAE,CAAC,GAAG,CAAC,GAAG,CAAA,eAAe,KAAK;;;;;;;;;;;;;;;;;UAiBhC,OAAO,CAAC,EAAE;UACV,OAAO,CAAC,IAAI;UACZ,OAAO,CAAC,IAAI;UACZ,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC;UAC/B,OAAO,CAAC,MAAM;UACd,OAAO,CAAC,QAAQ;UAChB,OAAO,CAAC,WAAW;UACnB,OAAO,CAAC,WAAW,CAAC,WAAW,EAAE;;;;;;UAMjC,MAAM;UACN,MAAM;QACR,CAAC,CAAC;YAEJ,OAAO,OAAO,CAAC;QACjB,CAAC;QAED,KAAK,CAAC,UAAU,CAAC,KAAK;YACpB,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;YAC1B,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;gBAC3C,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;YACtD,CAAC;YACD,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,IAAI,OAAO,CAAC,cAAc,CAAC,CAAC;YACjD,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,IAAI,uBAAuB,CAAC;YACzD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,OAAO,IAAI,CAAC,EAAE,CAAC;gBAC/C,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;YACxD,CAAC;YACD,MAAM,UAAU,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;YAE3C,IAAI,aAAa,IAAI,EAAE,IAAI,OAAO,EAAE,CAAC,WAAW,KAAK,UAAU,EAAE,CAAC;gBAChE,OAAO,EAAE,CAAC,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,CAC3B,SAAS,CAAC,EAAmC,EAAE,UAAU,CAAC,CAC3D,CAAC;YACJ,CAAC;YAED,OAAO,SAAS,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;QACnC,CAAC;QAED,KAAK,CAAC,aAAa,CAAC,KAAK;YACvB,MAAM,MAAM,GAAG,CAAC,KAAK,CAAC,GAAG,IAAI,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;YACpE,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,GAAG,CAAA,UAAU,KAAK;;;2BAGzB,MAAM;;;;yBAIR,MAAM;qBACV,KAAK,CAAC,EAAE;;8BAEC,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC;YAE5C,4BAA4B,CAC1B;gBACE,EAAE,EAAE,KAAK,CAAC,EAAE;gBACZ,cAAc,EAAE,WAAW;aAC5B,EACD,MAAM,CACP,CAAC;YACF,yBAAyB,CAAC,MAAM,YAAY,CAAC,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC,EAAE;gBACjE,EAAE,EAAE,KAAK,CAAC,EAAE;gBACZ,cAAc,EAAE,WAAW;aAC5B,CAAC,CAAC;QACL,CAAC;QAED,KAAK,CAAC,UAAU,CAAC,KAAK;YACpB,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,IAAI,OAAO,CAAC,cAAc,CAAC,CAAC;YACjD,MAAM,MAAM,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC;YACjC,MAAM,MAAM,GAAwB,KAAK,CAAC,UAAU;gBAClD,CAAC,CAAC,cAAc;gBAChB,CAAC,CAAC,SAAS,CAAC;YACd,MAAM,WAAW,GAAG,KAAK,CAAC,OAAO,IAAI,GAAG,CAAC;YAEzC,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,GAAG,CAAA,UAAU,KAAK;;qBAE/B,MAAM;2BACA,WAAW,CAAC,WAAW,EAAE;;;;8BAItB,IAAI,CAAC,SAAS,CAAC,oBAAoB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;yBACtD,MAAM;qBACV,KAAK,CAAC,EAAE;;8BAEC,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC;YAE5C,4BAA4B,CAC1B;gBACE,EAAE,EAAE,KAAK,CAAC,EAAE;gBACZ,cAAc,EAAE,MAAM;aACvB,EACD,MAAM,CACP,CAAC;YACF,yBAAyB,CAAC,MAAM,YAAY,CAAC,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC,EAAE;gBACjE,EAAE,EAAE,KAAK,CAAC,EAAE;gBACZ,cAAc,EAAE,MAAM;aACvB,CAAC,CAAC;QACL,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC;IACjC;;;OAGG;IACH,MAAM,EAAE,CAAC;SACN,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,CAAC;SACN,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,WAAW,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE;QACvE,OAAO,EACL,6GAA6G;KAChH,CAAC;IAEJ;;;OAGG;IACH,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACrC,CAAC,CAAC;AAOH,SAAS,cAAc,CAAC,KAAa;IACnC,MAAM,UAAU,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACrD,OAAO,UAAU,CAAC,MAAM,GAAG,GAAG;QAC5B,CAAC,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,KAAK;QAClC,CAAC,CAAC,UAAU,CAAC;AACjB,CAAC;AAwBD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,0BAA0B,CAGxC,OAAuD;IACvD,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAG,IAAI,EAAE,GAAG,OAAO,CAAC;IAE5C,OAAO,cAAc,CAAC;QACpB,IAAI,EAAE,eAAe;QAErB,MAAM,EAAE;YACN,MAAM,EAAE,iBAAiB;YACzB,SAAS,EAAE,QAAQ;SACpB;QAED,KAAK,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE;YAC3B,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,MAAM,IAAI,KAAK,CACb,6FAA6F,CAC9F,CAAC;YACJ,CAAC;YAED,yBAAyB;YACzB,MAAM,MAAM,GAAW,YAAY,CAAC;gBAClC,GAAG,EAAE,MAAM,CAAC,MAAM;gBAClB,SAAS,EAAE,MAAM,CAAC,aAAa;aAChC,CAAC,CAAC;YAEH,MAAM,eAAe,GAAG,6BAA6B,CAAC,KAAK,EAAE;gBAC3D,YAAY,EAAE,eAAe;gBAC7B,OAAO,EAAE,IAAI;aACd,CAAC,CAAC;YAEH,MAAM,MAAM,GAAG,eAAe,CAAC,SAAS,EAAE;gBACxC,CAAC,CAAC;oBACE,QAAQ,CAAC,KAAa,EAAE,MAAiB;wBACvC,eAAe,CAAC,MAAM,CAAC;4BACrB,IAAI,EAAE,UAAU;4BAChB,KAAK,EAAE,gBAAgB;4BACvB,OAAO,EAAE,cAAc,CAAC,KAAK,CAAC;4BAC9B,OAAO,EAAE;gCACP,KAAK;gCACL,MAAM,EAAE,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE;gCAC7C,WAAW,EAAE,MAAM,CAAC,MAAM;gCAC1B,QAAQ;6BACT;yBACF,CAAC,CAAC;oBACL,CAAC;iBACF;gBACH,CAAC,CAAC,SAAS,CAAC;YAEd,mCAAmC;YACnC,MAAM,EAAE,GAA4B,MAAM;gBACxC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;gBACrC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;YAEhC,0BAA0B;YAC1B,MAAM,MAAM,GAAoB,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC;YAE/C,OAAO;gBACL,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAuC;gBAClE,KAAK,CAAC,IAAI;oBACR,2CAA2C;oBAC3C,IAAI,CAAC;wBACH,MAAM,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;oBAC9B,CAAC;oBAAC,OAAO,KAAK,EAAE,CAAC;wBACf,yDAAyD;wBACzD,OAAO,CAAC,KAAK,CACX,2DAA2D,EAC3D,KAAK,CACN,CAAC;oBACJ,CAAC;gBACH,CAAC;aACF,CAAC;QACJ,CAAC;KACF,CAAC,CAAC;AACL,CAAC"}
|
package/package.json
CHANGED
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
|
|
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
|
-
*
|
|
566
|
+
* Turso provider config loaded from `TURSO_*` env vars.
|
|
178
567
|
*/
|
|
179
568
|
export type TursoConfig = z.infer<typeof TursoConfigSchema>;
|
|
180
569
|
|