@beignet/provider-drizzle-turso 0.0.3 → 0.0.5
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 +41 -0
- package/README.md +289 -47
- package/dist/index.d.ts +55 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +288 -28
- package/dist/index.js.map +1 -1
- package/package.json +30 -5
- package/src/index.ts +431 -36
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# @beignet/provider-drizzle-turso
|
|
2
2
|
|
|
3
|
+
## 0.0.5
|
|
4
|
+
|
|
5
|
+
## 0.0.4
|
|
6
|
+
|
|
7
|
+
### Patch Changes
|
|
8
|
+
|
|
9
|
+
- 8bcb31f: Mark package READMEs with Beignet's experimental alpha status and 0.0.x stability expectations.
|
|
10
|
+
- d137044: Declare `@beignet/core` as a peer dependency with a lockstep version range in
|
|
11
|
+
every integration and provider package instead of a regular `"*"` dependency.
|
|
12
|
+
Installs now always resolve a single shared copy of core, so `instanceof`
|
|
13
|
+
checks such as `isContractError` and upload error identity keep working, and
|
|
14
|
+
mixed Beignet versions fail loudly at install time instead of at runtime.
|
|
15
|
+
|
|
16
|
+
If your package manager does not install peer dependencies automatically, add
|
|
17
|
+
`@beignet/core` to your app alongside these packages. `@beignet/nuqs` now also
|
|
18
|
+
declares `@beignet/react-query` as a peer dependency, and
|
|
19
|
+
`@beignet/provider-storage-s3` now expects you to install
|
|
20
|
+
`@aws-sdk/client-s3` and `@aws-sdk/s3-request-presigner` yourself, matching
|
|
21
|
+
how other providers treat their SDKs.
|
|
22
|
+
|
|
23
|
+
- 780955c: Add Drizzle/Turso-backed durable idempotency storage with setup SQL, root and transaction-client ports, and Unit of Work rollback coverage for idempotency and outbox writes.
|
|
24
|
+
- 89390fe: Harden the Drizzle/Turso outbox and idempotency adapters and widen database URL validation:
|
|
25
|
+
|
|
26
|
+
- `markDelivered(...)` and `markFailed(...)` now trust the driver-reported `rowsAffected` instead of re-reading the row after the update. The re-read raced with legitimate re-claims (a concurrent worker could re-claim a message immediately after `markFailed` returned it to `pending`) and threw spurious `OutboxClaimError`s. The verification read only runs as a fallback when the driver does not report `rowsAffected`.
|
|
27
|
+
- Idempotency `complete(...)` and `fail(...)` no longer silently no-op when no in-progress reservation matches the key and fingerprint. They now throw the new `DrizzleTursoIdempotencyMutationError`, surfacing workflow bugs such as double completion, missing reservations, and fingerprint mismatches. Like `reserve(...)`, a missing `rowsAffected` is treated as success.
|
|
28
|
+
- `TURSO_DB_URL` now accepts every URL scheme `@libsql/client` accepts: `libsql://` and `file:` plus `http://`, `https://`, `ws://`, and `wss://` for local or self-hosted sqld instances.
|
|
29
|
+
|
|
30
|
+
- 69b8c35: Replace the README's broken multiple-databases example. The provider reads one connection from `TURSO_DB_URL`, so registering it twice cannot target two databases; the docs now explain the limitation and show wiring a second database through an app-owned typed provider with its own env prefix.
|
|
31
|
+
- 1a79090: Emit Node-compatible ESM: all relative imports in published packages now carry explicit .js extensions, fixing ERR_MODULE_NOT_FOUND when running the CLI or importing package dist files under plain Node.
|
|
32
|
+
- 44f1192: Move first-party provider diagnostics to package-owned `beignet.provider`
|
|
33
|
+
manifest metadata and have doctor read installed provider package manifests.
|
|
34
|
+
- 2aa77ca: Add static provider metadata and provider wiring diagnostics for generated apps.
|
|
35
|
+
- aa5425c: Clarify Postgres guidance in the provider README with public-facing database adapter language.
|
|
36
|
+
- 03b1743: Document durable workflow provider conventions for retries, backoff, dead-letter state, and unsupported provider semantics.
|
|
37
|
+
- 69b8c35: Require patched dependency versions for security advisories: drizzle-orm
|
|
38
|
+
`^0.45.2` (SQL injection via improperly escaped identifiers, GHSA advisory
|
|
39
|
+
patched in 0.45.2) and nodemailer `^8.0.5` (SMTP command injection fixes).
|
|
40
|
+
Generated apps now scaffold with drizzle-orm `^0.45.2` and drizzle-kit
|
|
41
|
+
`^0.31.10`.
|
|
42
|
+
- 8063d38: Rename the contract front door to `defineContract`/`defineContractGroup`, rename operational commands to tasks (`@beignet/core/tasks`, `defineTasks`, `runTask`, `beignet task run`, `beignet make task`, `server/tasks.ts`, `features/<feature>/tasks/`, `paths.tasks`), and standardize context binding: context-free declarations stay top-level (`defineEvent`), while context-bound definitions come from per-capability factories (`createListeners`, `createJobs`, `createSchedules`, `createNotifications`, `createTasks`) called once in `lib/`. Top-level context-generic `defineListener`, `defineJob`, `defineSchedule`, and `defineNotification` are removed.
|
|
43
|
+
|
|
3
44
|
## 0.0.3
|
|
4
45
|
|
|
5
46
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# @beignet/provider-drizzle-turso
|
|
2
2
|
|
|
3
|
+
> [!CAUTION]
|
|
4
|
+
> Beignet is experimental alpha software. The `0.0.x` package line is for early
|
|
5
|
+
> evaluation, and APIs may change between releases while the framework settles.
|
|
6
|
+
|
|
3
7
|
Drizzle ORM + Turso/libSQL provider for Beignet applications.
|
|
4
8
|
|
|
5
9
|
The provider installs a typed database port backed by Drizzle ORM and Turso's
|
|
@@ -12,11 +16,27 @@ and migration workflow.
|
|
|
12
16
|
- Full TypeScript inference from your Drizzle schema.
|
|
13
17
|
- Works with Turso Cloud or local libSQL.
|
|
14
18
|
- Keeps runtime provider wiring separate from Drizzle CLI configuration.
|
|
19
|
+
- Provides a Unit of Work helper that can build transaction-scoped repositories,
|
|
20
|
+
audit logs, outbox recorders, and other app-owned ports from one Drizzle
|
|
21
|
+
transaction client.
|
|
22
|
+
|
|
23
|
+
## Current scope
|
|
24
|
+
|
|
25
|
+
This package is Beignet's first-party Drizzle provider for libSQL/Turso. It is
|
|
26
|
+
also the reference SQL shape for Beignet apps: keep schema and migrations
|
|
27
|
+
app-owned, accept both a root Drizzle database and transaction client in
|
|
28
|
+
repository factories, and create transaction-scoped app ports inside Unit of
|
|
29
|
+
Work.
|
|
30
|
+
|
|
31
|
+
Beignet does not currently ship a first-party Postgres Drizzle provider. Use
|
|
32
|
+
the same app-owned port pattern with Drizzle's Postgres driver when your app
|
|
33
|
+
needs Postgres. Contracts, use cases, policies, and routes can keep depending
|
|
34
|
+
on ports while the app's infra adapter chooses the database driver.
|
|
15
35
|
|
|
16
36
|
## Install
|
|
17
37
|
|
|
18
38
|
```bash
|
|
19
|
-
bun add @beignet/provider-drizzle-turso drizzle-orm @libsql/client
|
|
39
|
+
bun add @beignet/provider-drizzle-turso @beignet/core drizzle-orm @libsql/client
|
|
20
40
|
```
|
|
21
41
|
|
|
22
42
|
## Setup
|
|
@@ -101,7 +121,7 @@ import { providers } from "./providers";
|
|
|
101
121
|
export const server = await createNextServer({
|
|
102
122
|
ports: appPorts,
|
|
103
123
|
providers,
|
|
104
|
-
|
|
124
|
+
context: ({ ports }) => ({ ports }),
|
|
105
125
|
routes,
|
|
106
126
|
});
|
|
107
127
|
```
|
|
@@ -113,26 +133,66 @@ infrastructure, then wire the repository into `ctx.ports`.
|
|
|
113
133
|
|
|
114
134
|
```ts
|
|
115
135
|
// infra/todos/drizzle-todo-repository.ts
|
|
116
|
-
import {
|
|
117
|
-
import {
|
|
136
|
+
import { Buffer } from "node:buffer";
|
|
137
|
+
import { and, desc, eq, lt, or } from "drizzle-orm";
|
|
138
|
+
import { cursorPageResult } from "@beignet/core/pagination";
|
|
118
139
|
import type { DrizzleTursoDatabase } from "@beignet/provider-drizzle-turso";
|
|
119
140
|
import type { TodoRepository } from "@/features/todos/ports";
|
|
120
141
|
import * as schema from "@/infra/db/schema";
|
|
121
142
|
|
|
143
|
+
type TodoCursor = {
|
|
144
|
+
sortBy: "createdAt";
|
|
145
|
+
sortDirection: "desc";
|
|
146
|
+
sortValue: string;
|
|
147
|
+
id: string;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
function encodeTodoCursor(row: typeof schema.todos.$inferSelect): string {
|
|
151
|
+
return Buffer.from(
|
|
152
|
+
JSON.stringify({
|
|
153
|
+
sortBy: "createdAt",
|
|
154
|
+
sortDirection: "desc",
|
|
155
|
+
sortValue: row.createdAt,
|
|
156
|
+
id: row.id,
|
|
157
|
+
}),
|
|
158
|
+
).toString("base64url");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function decodeTodoCursor(cursor: string): TodoCursor {
|
|
162
|
+
return JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
163
|
+
}
|
|
164
|
+
|
|
122
165
|
export function createTodoRepository(
|
|
123
166
|
db: DrizzleTursoDatabase<typeof schema>,
|
|
124
167
|
): TodoRepository {
|
|
125
168
|
return {
|
|
126
169
|
async list(input) {
|
|
170
|
+
const cursor = input.page.cursor
|
|
171
|
+
? decodeTodoCursor(input.page.cursor)
|
|
172
|
+
: null;
|
|
127
173
|
const rows = await db
|
|
128
174
|
.select()
|
|
129
175
|
.from(schema.todos)
|
|
130
|
-
.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
176
|
+
.where(
|
|
177
|
+
cursor
|
|
178
|
+
? or(
|
|
179
|
+
lt(schema.todos.createdAt, cursor.sortValue),
|
|
180
|
+
and(
|
|
181
|
+
eq(schema.todos.createdAt, cursor.sortValue),
|
|
182
|
+
lt(schema.todos.id, cursor.id),
|
|
183
|
+
),
|
|
184
|
+
)
|
|
185
|
+
: undefined,
|
|
186
|
+
)
|
|
187
|
+
.orderBy(desc(schema.todos.createdAt), desc(schema.todos.id))
|
|
188
|
+
.limit(input.page.limit + 1);
|
|
189
|
+
const pageRows = rows.slice(0, input.page.limit);
|
|
190
|
+
const nextCursor =
|
|
191
|
+
rows.length > input.page.limit && pageRows.length > 0
|
|
192
|
+
? encodeTodoCursor(pageRows[pageRows.length - 1])
|
|
193
|
+
: null;
|
|
194
|
+
|
|
195
|
+
return cursorPageResult(pageRows, input.page, nextCursor);
|
|
136
196
|
},
|
|
137
197
|
};
|
|
138
198
|
}
|
|
@@ -153,14 +213,63 @@ export function createRepositories(db: DrizzleTursoDatabase<typeof schema>) {
|
|
|
153
213
|
}
|
|
154
214
|
```
|
|
155
215
|
|
|
216
|
+
Wire repositories into app ports with a typed app-owned database provider.
|
|
217
|
+
The curried `createProvider<Requires, Context, ServiceInput>()` form types the
|
|
218
|
+
required `db` port, the app context, and `createServiceContext` without casts.
|
|
219
|
+
Register it after `createDrizzleTursoProvider`, which installs the `db` port it
|
|
220
|
+
requires:
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
// infra/db/provider.ts
|
|
224
|
+
import { createProvider } from "@beignet/core/providers";
|
|
225
|
+
import type { DbPort } from "@beignet/provider-drizzle-turso";
|
|
226
|
+
import type { AppContext } from "@/app-context";
|
|
227
|
+
import type { AppPorts } from "@/ports";
|
|
228
|
+
import type { AppServiceContextInput } from "@/server";
|
|
229
|
+
import { createRepositories } from "./repositories";
|
|
230
|
+
import type * as schema from "./schema";
|
|
231
|
+
|
|
232
|
+
export const appDatabaseProvider = createProvider<
|
|
233
|
+
{ db: DbPort<typeof schema> },
|
|
234
|
+
AppContext,
|
|
235
|
+
AppServiceContextInput
|
|
236
|
+
>()({
|
|
237
|
+
name: "app-database",
|
|
238
|
+
async setup({ ports }) {
|
|
239
|
+
const providedPorts: Pick<AppPorts, "todos"> = createRepositories(
|
|
240
|
+
ports.db.db,
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
return { ports: providedPorts };
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Type `ctx.ports` with `InferProviderPorts` so provider-contributed ports,
|
|
249
|
+
including the provider-owned `db` port, stay typed in app code:
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
// app-context.ts
|
|
253
|
+
import type { InferProviderPorts } from "@beignet/core/providers";
|
|
254
|
+
import type { AppPorts } from "@/ports";
|
|
255
|
+
import type { providers } from "@/server/providers";
|
|
256
|
+
|
|
257
|
+
export type AppRuntimePorts = AppPorts & InferProviderPorts<typeof providers>;
|
|
258
|
+
|
|
259
|
+
export type AppContext = {
|
|
260
|
+
requestId: string;
|
|
261
|
+
ports: AppRuntimePorts;
|
|
262
|
+
};
|
|
263
|
+
```
|
|
264
|
+
|
|
156
265
|
```ts
|
|
157
266
|
// features/todos/use-cases/list-todos.ts
|
|
158
|
-
import {
|
|
267
|
+
import { normalizeCursorPage } from "@beignet/core/pagination";
|
|
159
268
|
|
|
160
269
|
export const listTodos = useCase.query("todos.list").run(async ({ ctx }) => {
|
|
161
|
-
const page =
|
|
270
|
+
const page = normalizeCursorPage({}, { defaultLimit: 20, maxLimit: 100 });
|
|
162
271
|
|
|
163
|
-
return ctx.ports.todos.list(page);
|
|
272
|
+
return ctx.ports.todos.list({ page });
|
|
164
273
|
});
|
|
165
274
|
```
|
|
166
275
|
|
|
@@ -233,28 +342,24 @@ export function createDrizzleTodoRepository(
|
|
|
233
342
|
```
|
|
234
343
|
|
|
235
344
|
```ts
|
|
236
|
-
//
|
|
345
|
+
// infra/db/provider.ts
|
|
237
346
|
import { createDrizzleTursoUnitOfWork } from "@beignet/provider-drizzle-turso";
|
|
238
|
-
import { createRepositories } from "
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
...createRepositories(tx),
|
|
252
|
-
events,
|
|
253
|
-
}),
|
|
254
|
-
}),
|
|
255
|
-
},
|
|
256
|
-
};
|
|
347
|
+
import { createRepositories } from "./repositories";
|
|
348
|
+
|
|
349
|
+
// Inside the app database provider's setup({ ports }):
|
|
350
|
+
const providedPorts: Pick<AppPorts, "todos" | "uow"> = {
|
|
351
|
+
...createRepositories(ports.db.db),
|
|
352
|
+
uow: createDrizzleTursoUnitOfWork({
|
|
353
|
+
db: ports.db.db,
|
|
354
|
+
eventBus: ports.eventBus,
|
|
355
|
+
createTransactionPorts: (tx, events) => ({
|
|
356
|
+
...createRepositories(tx),
|
|
357
|
+
events,
|
|
358
|
+
}),
|
|
359
|
+
}),
|
|
257
360
|
};
|
|
361
|
+
|
|
362
|
+
return { ports: providedPorts };
|
|
258
363
|
```
|
|
259
364
|
|
|
260
365
|
Inside a use case, call transaction-scoped repositories through `tx` and record
|
|
@@ -284,15 +389,22 @@ import {
|
|
|
284
389
|
drainOutbox,
|
|
285
390
|
} from "@beignet/core/outbox";
|
|
286
391
|
import {
|
|
392
|
+
createDrizzleTursoIdempotencyPort,
|
|
393
|
+
createDrizzleTursoIdempotencySetupStatements,
|
|
287
394
|
createDrizzleTursoOutboxPort,
|
|
288
395
|
createDrizzleTursoOutboxSetupStatements,
|
|
289
396
|
createDrizzleTursoUnitOfWork,
|
|
290
397
|
} from "@beignet/provider-drizzle-turso";
|
|
291
398
|
```
|
|
292
399
|
|
|
293
|
-
Add Beignet's outbox
|
|
400
|
+
Add Beignet's outbox and idempotency tables to your app-owned migration or
|
|
401
|
+
bootstrap flow:
|
|
294
402
|
|
|
295
403
|
```ts
|
|
404
|
+
for (const statement of createDrizzleTursoIdempotencySetupStatements()) {
|
|
405
|
+
await client.execute(statement);
|
|
406
|
+
}
|
|
407
|
+
|
|
296
408
|
for (const statement of createDrizzleTursoOutboxSetupStatements()) {
|
|
297
409
|
await client.execute(statement);
|
|
298
410
|
}
|
|
@@ -304,18 +416,70 @@ Then expose transaction-scoped outbox-backed event recording:
|
|
|
304
416
|
uow: createDrizzleTursoUnitOfWork({
|
|
305
417
|
db: ports.db.db,
|
|
306
418
|
createTransactionPorts: (tx) => {
|
|
419
|
+
const idempotency = createDrizzleTursoIdempotencyPort(tx);
|
|
307
420
|
const outbox = createDrizzleTursoOutboxPort(tx);
|
|
308
421
|
|
|
309
422
|
return {
|
|
310
423
|
...createRepositories(tx),
|
|
311
424
|
events: createOutboxEventRecorder(outbox),
|
|
425
|
+
idempotency,
|
|
312
426
|
outbox,
|
|
313
427
|
};
|
|
314
428
|
},
|
|
315
429
|
});
|
|
316
430
|
```
|
|
317
431
|
|
|
318
|
-
|
|
432
|
+
Any port that must commit with the business write belongs in
|
|
433
|
+
`createTransactionPorts`: repositories, audit logs, outbox-backed events/jobs,
|
|
434
|
+
feature history repositories, and durable idempotency records. Root ports are
|
|
435
|
+
still useful for reads and background work, but they do not participate in the
|
|
436
|
+
current transaction unless you rebuild them from `tx`.
|
|
437
|
+
|
|
438
|
+
Use `createDrizzleTursoIdempotencyPort(...)` at the root app port for ordinary
|
|
439
|
+
retry-safe commands, and rebuild it from `tx` when the idempotency reservation,
|
|
440
|
+
business write, audit row, outbox records, and replay result must commit
|
|
441
|
+
together.
|
|
442
|
+
|
|
443
|
+
## Durable idempotency
|
|
444
|
+
|
|
445
|
+
Use `createDrizzleTursoIdempotencyPort(...)` when `runIdempotently(...)` needs
|
|
446
|
+
durable storage instead of the in-memory test adapter:
|
|
447
|
+
|
|
448
|
+
```ts
|
|
449
|
+
import { runIdempotently } from "@beignet/core/idempotency";
|
|
450
|
+
import {
|
|
451
|
+
createDrizzleTursoIdempotencyPort,
|
|
452
|
+
createDrizzleTursoIdempotencySetupStatements,
|
|
453
|
+
} from "@beignet/provider-drizzle-turso";
|
|
454
|
+
|
|
455
|
+
for (const statement of createDrizzleTursoIdempotencySetupStatements()) {
|
|
456
|
+
await client.execute(statement);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const idempotency = createDrizzleTursoIdempotencyPort(db);
|
|
460
|
+
|
|
461
|
+
await runIdempotently(idempotency, {
|
|
462
|
+
namespace: "posts.create",
|
|
463
|
+
key: input.idempotencyKey,
|
|
464
|
+
scope: { actorId: ctx.actor.id, tenantId: ctx.tenant.id },
|
|
465
|
+
fingerprint,
|
|
466
|
+
run: () => ctx.ports.posts.create(input),
|
|
467
|
+
});
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
The default table is `idempotency_records`; pass `tableName` to both setup and
|
|
471
|
+
port creation if your app uses a different table name. Expired records are
|
|
472
|
+
removed during reservation, failed in-progress reservations are released, and
|
|
473
|
+
completed matching records replay their stored result.
|
|
474
|
+
|
|
475
|
+
`complete(...)` and `fail(...)` must match an in-progress reservation with the
|
|
476
|
+
provided fingerprint. When the database reports that no row matched, the port
|
|
477
|
+
throws `DrizzleTursoIdempotencyMutationError` instead of silently doing
|
|
478
|
+
nothing, so workflow bugs such as double completion, a missing reservation, or
|
|
479
|
+
a mismatched fingerprint surface immediately. The error exposes `action`,
|
|
480
|
+
`namespace`, `key`, and `scopeKey` for logging and debugging.
|
|
481
|
+
|
|
482
|
+
Drain from a worker, cron route, or schedule:
|
|
319
483
|
|
|
320
484
|
```ts
|
|
321
485
|
await drainOutbox({
|
|
@@ -329,15 +493,31 @@ await drainOutbox({
|
|
|
329
493
|
The default table is `outbox_messages`; pass `tableName` to both setup and port
|
|
330
494
|
creation if your app uses a different table name.
|
|
331
495
|
|
|
496
|
+
The adapter preserves Beignet's durable workflow semantics in SQL:
|
|
497
|
+
|
|
498
|
+
- `attempts` increments when a worker claims a message.
|
|
499
|
+
- `max_attempts` stores the maximum total attempts for the message.
|
|
500
|
+
- `available_at` stores retry timing and backoff decisions.
|
|
501
|
+
- `locked_until` and `claim_token` provide a lease so workers can compete.
|
|
502
|
+
- `deadLettered` is the terminal state for messages that should no longer be
|
|
503
|
+
retried automatically.
|
|
504
|
+
|
|
505
|
+
`drainOutbox(...)` owns retry classification and backoff computation. The
|
|
506
|
+
Drizzle/Turso adapter stores the resulting state and enforces claim ownership.
|
|
507
|
+
|
|
332
508
|
## Configuration
|
|
333
509
|
|
|
334
510
|
The provider reads configuration from environment variables with the `TURSO_` prefix:
|
|
335
511
|
|
|
336
512
|
| Variable | Required | Description |
|
|
337
513
|
|----------|----------|-------------|
|
|
338
|
-
| `TURSO_DB_URL` | Yes | Turso/libSQL database URL (e.g., `libsql://your-db.turso.io
|
|
514
|
+
| `TURSO_DB_URL` | Yes | Turso/libSQL database URL (e.g., `libsql://your-db.turso.io`, `file:local.db`, or `http://127.0.0.1:8080` for local sqld) |
|
|
339
515
|
| `TURSO_DB_AUTH_TOKEN` | No | Turso auth token (required for cloud databases, optional for local) |
|
|
340
516
|
|
|
517
|
+
`TURSO_DB_URL` accepts every URL scheme `@libsql/client` accepts: `libsql://`
|
|
518
|
+
and `file:` plus `http://`, `https://`, `ws://`, and `wss://` for local or
|
|
519
|
+
self-hosted sqld instances.
|
|
520
|
+
|
|
341
521
|
### Example `.env`
|
|
342
522
|
|
|
343
523
|
```env
|
|
@@ -347,30 +527,76 @@ TURSO_DB_AUTH_TOKEN=eyJhbGciOiJFZERTQSIsInR5cCI...
|
|
|
347
527
|
|
|
348
528
|
# For local development
|
|
349
529
|
TURSO_DB_URL=file:local.db
|
|
530
|
+
|
|
531
|
+
# For a local sqld instance
|
|
532
|
+
TURSO_DB_URL=http://127.0.0.1:8080
|
|
350
533
|
```
|
|
351
534
|
|
|
352
535
|
## Advanced usage
|
|
353
536
|
|
|
354
537
|
### Multiple databases
|
|
355
538
|
|
|
356
|
-
|
|
539
|
+
This provider reads one connection from the `TURSO_DB_URL` and
|
|
540
|
+
`TURSO_DB_AUTH_TOKEN` environment variables. Registering it twice, even with
|
|
541
|
+
different `portName` values, reads the same env vars, so both ports would
|
|
542
|
+
connect to the same database. Use `portName` only to rename the single
|
|
543
|
+
provider-owned port.
|
|
544
|
+
|
|
545
|
+
To connect a second database, wire it through an app-owned provider that loads
|
|
546
|
+
its own connection config, following the same typed custom provider pattern
|
|
547
|
+
shown above:
|
|
357
548
|
|
|
358
549
|
```ts
|
|
359
|
-
|
|
360
|
-
import
|
|
361
|
-
import {
|
|
550
|
+
// infra/db/analytics-provider.ts
|
|
551
|
+
import { createClient } from "@libsql/client";
|
|
552
|
+
import { drizzle } from "drizzle-orm/libsql";
|
|
553
|
+
import { createProvider } from "@beignet/core/providers";
|
|
554
|
+
import type { DbPort } from "@beignet/provider-drizzle-turso";
|
|
555
|
+
import { z } from "zod";
|
|
556
|
+
import * as analyticsSchema from "./analytics-schema";
|
|
362
557
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
558
|
+
const AnalyticsDbConfigSchema = z.object({
|
|
559
|
+
DB_URL: z.string().min(1),
|
|
560
|
+
DB_AUTH_TOKEN: z.string().optional(),
|
|
366
561
|
});
|
|
367
562
|
|
|
368
|
-
export const analyticsDbProvider =
|
|
369
|
-
|
|
370
|
-
|
|
563
|
+
export const analyticsDbProvider = createProvider({
|
|
564
|
+
name: "analytics-db",
|
|
565
|
+
|
|
566
|
+
config: {
|
|
567
|
+
schema: AnalyticsDbConfigSchema,
|
|
568
|
+
envPrefix: "ANALYTICS_",
|
|
569
|
+
},
|
|
570
|
+
|
|
571
|
+
async setup({ config }) {
|
|
572
|
+
if (!config) {
|
|
573
|
+
throw new Error(
|
|
574
|
+
"[analyticsDbProvider] Missing config. Set ANALYTICS_DB_URL (and optional ANALYTICS_DB_AUTH_TOKEN).",
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const client = createClient({
|
|
579
|
+
url: config.DB_URL,
|
|
580
|
+
authToken: config.DB_AUTH_TOKEN,
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
const analyticsDb: DbPort<typeof analyticsSchema> = {
|
|
584
|
+
db: drizzle(client, { schema: analyticsSchema }),
|
|
585
|
+
client,
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
return {
|
|
589
|
+
ports: { analyticsDb },
|
|
590
|
+
async stop() {
|
|
591
|
+
await client.close();
|
|
592
|
+
},
|
|
593
|
+
};
|
|
594
|
+
},
|
|
371
595
|
});
|
|
596
|
+
```
|
|
372
597
|
|
|
373
|
-
|
|
598
|
+
```ts
|
|
599
|
+
// ports/index.ts
|
|
374
600
|
export type AppPorts = {
|
|
375
601
|
db: DbPort<typeof mainSchema>;
|
|
376
602
|
analyticsDb: DbPort<typeof analyticsSchema>;
|
|
@@ -490,6 +716,22 @@ Returns SQL setup statements for the app-owned outbox table and indexes. Run
|
|
|
490
716
|
these through your migration/bootstrap flow or translate them into your normal
|
|
491
717
|
Drizzle migrations.
|
|
492
718
|
|
|
719
|
+
### `createDrizzleTursoIdempotencyPort<TSchema>(db, options?)`
|
|
720
|
+
|
|
721
|
+
Factory function to create a SQL-backed `IdempotencyPort` from a root Drizzle
|
|
722
|
+
database or transaction client.
|
|
723
|
+
|
|
724
|
+
**Parameters:**
|
|
725
|
+
- `db` (required): A `DrizzleTursoDatabase<TSchema>` root database or transaction
|
|
726
|
+
- `options.tableName` (optional): Idempotency table name, defaults to `"idempotency_records"`
|
|
727
|
+
- `options.now` (optional): Test clock
|
|
728
|
+
|
|
729
|
+
### `createDrizzleTursoIdempotencySetupStatements(options?)`
|
|
730
|
+
|
|
731
|
+
Returns SQL setup statements for the app-owned idempotency table and indexes.
|
|
732
|
+
Run these through your migration/bootstrap flow or translate them into your
|
|
733
|
+
normal Drizzle migrations.
|
|
734
|
+
|
|
493
735
|
## License
|
|
494
736
|
|
|
495
737
|
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* const todos = createDrizzleTodoRepository(ctx.ports.db.db);
|
|
26
26
|
* ```
|
|
27
27
|
*/
|
|
28
|
+
import { type IdempotencyPort } from "@beignet/core/idempotency";
|
|
28
29
|
import { type OutboxPort } from "@beignet/core/outbox";
|
|
29
30
|
import { type BufferedDomainEventRecorder, type EventBusPort, type UnitOfWorkPort } from "@beignet/core/ports";
|
|
30
31
|
import { type Client } from "@libsql/client";
|
|
@@ -120,6 +121,56 @@ export declare function createDrizzleTursoOutboxSetupStatements(options?: Drizzl
|
|
|
120
121
|
* for eligible messages.
|
|
121
122
|
*/
|
|
122
123
|
export declare function createDrizzleTursoOutboxPort<TSchema extends Record<string, unknown>>(db: DrizzleTursoDatabase<TSchema>, adapterOptions?: DrizzleTursoOutboxOptions): OutboxPort;
|
|
124
|
+
/**
|
|
125
|
+
* Options for a Drizzle/Turso-backed idempotency port.
|
|
126
|
+
*/
|
|
127
|
+
export interface DrizzleTursoIdempotencyOptions {
|
|
128
|
+
/**
|
|
129
|
+
* Table that stores idempotency records.
|
|
130
|
+
*
|
|
131
|
+
* Default: "idempotency_records".
|
|
132
|
+
*/
|
|
133
|
+
tableName?: string;
|
|
134
|
+
/**
|
|
135
|
+
* Clock used by tests and deterministic environments.
|
|
136
|
+
*/
|
|
137
|
+
now?: () => Date;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Error thrown when `complete(...)` or `fail(...)` does not match an
|
|
141
|
+
* in-progress idempotency reservation with the provided fingerprint.
|
|
142
|
+
*
|
|
143
|
+
* This usually signals a workflow bug: the reservation was never made, was
|
|
144
|
+
* already completed or released, expired and was cleaned up, or the caller
|
|
145
|
+
* passed a different fingerprint than the one used to reserve.
|
|
146
|
+
*/
|
|
147
|
+
export declare class DrizzleTursoIdempotencyMutationError extends Error {
|
|
148
|
+
readonly action: "complete" | "fail";
|
|
149
|
+
readonly namespace: string;
|
|
150
|
+
readonly key: string;
|
|
151
|
+
readonly scopeKey: string;
|
|
152
|
+
constructor(args: {
|
|
153
|
+
action: "complete" | "fail";
|
|
154
|
+
namespace: string;
|
|
155
|
+
key: string;
|
|
156
|
+
scopeKey: string;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Create idempotent SQL statements for the Drizzle/Turso idempotency table.
|
|
161
|
+
*
|
|
162
|
+
* Run these during migrations or local setup before using
|
|
163
|
+
* `createDrizzleTursoIdempotencyPort(...)`.
|
|
164
|
+
*/
|
|
165
|
+
export declare function createDrizzleTursoIdempotencySetupStatements(options?: DrizzleTursoIdempotencyOptions): readonly string[];
|
|
166
|
+
/**
|
|
167
|
+
* Create a Beignet idempotency port backed by a Drizzle/Turso database.
|
|
168
|
+
*
|
|
169
|
+
* The port accepts both the root Drizzle database and transaction clients, so
|
|
170
|
+
* applications can expose root idempotency for ordinary retry-safe commands and
|
|
171
|
+
* transaction-scoped idempotency from Unit of Work.
|
|
172
|
+
*/
|
|
173
|
+
export declare function createDrizzleTursoIdempotencyPort<TSchema extends Record<string, unknown>>(db: DrizzleTursoDatabase<TSchema>, adapterOptions?: DrizzleTursoIdempotencyOptions): IdempotencyPort;
|
|
123
174
|
/**
|
|
124
175
|
* Configuration schema for the Drizzle Turso provider.
|
|
125
176
|
* Validates environment variables with TURSO_ prefix.
|
|
@@ -145,7 +196,9 @@ export interface DrizzleTursoProviderOptions<TSchema extends Record<string, unkn
|
|
|
145
196
|
schema: TSchema;
|
|
146
197
|
/**
|
|
147
198
|
* Optional port name. Defaults to "db".
|
|
148
|
-
*
|
|
199
|
+
* Renames the contributed port. The provider always reads its connection
|
|
200
|
+
* from `TURSO_DB_URL`; for a second database, wire an app-owned provider
|
|
201
|
+
* with its own env prefix instead.
|
|
149
202
|
*/
|
|
150
203
|
portName?: PortName;
|
|
151
204
|
}
|
|
@@ -173,6 +226,6 @@ export interface DrizzleTursoProviderOptions<TSchema extends Record<string, unkn
|
|
|
173
226
|
export declare function createDrizzleTursoProvider<TSchema extends Record<string, unknown>, PortName extends string = "db">(options: DrizzleTursoProviderOptions<TSchema, PortName>): import("@beignet/core/providers").ServiceProvider<unknown, z.ZodObject<{
|
|
174
227
|
DB_URL: z.ZodString;
|
|
175
228
|
DB_AUTH_TOKEN: z.ZodOptional<z.ZodString>;
|
|
176
|
-
}, z.core.$strip>, Record<PortName, DbPort<TSchema
|
|
229
|
+
}, z.core.$strip>, Record<PortName, DbPort<TSchema>>, unknown, void>;
|
|
177
230
|
export {};
|
|
178
231
|
//# sourceMappingURL=index.d.ts.map
|
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,EAWL,KAAK,UAAU,EAEhB,MAAM,sBAAsB,CAAC;AAC9B,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,EAAE,KAAK,0BAA0B,EAAO,MAAM,aAAa,CAAC;AACnE,OAAO,EAAW,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAClE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AACpE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AACvE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;GAKG;AACH,MAAM,WAAW,MAAM,CACrB,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAEjE;;;OAGG;IACH,EAAE,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC;IAE5B;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,MAAM,oBAAoB,CAC9B,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAE/D,cAAc,CAAC,OAAO,CAAC,GACvB,iBAAiB,CAAC,OAAO,EAAE,0BAA0B,CAAC,OAAO,CAAC,CAAC,CAAC;AAEpE;;GAEG;AACH,MAAM,MAAM,uBAAuB,CACjC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAC/D,iBAAiB,CAAC,OAAO,EAAE,0BAA0B,CAAC,OAAO,CAAC,CAAC,CAAC;AAEpE;;GAEG;AACH,MAAM,WAAW,6BAA6B,CAC5C,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvC,OAAO;IAEP;;OAEG;IACH,EAAE,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC;IAE5B;;;;;OAKG;IACH,sBAAsB,EAAE,CACtB,EAAE,EAAE,uBAAuB,CAAC,OAAO,CAAC,EACpC,MAAM,EAAE,2BAA2B,KAChC,OAAO,CAAC;IAEb;;OAEG;IACH,QAAQ,CAAC,EAAE,YAAY,CAAC;IAExB;;OAEG;IACH,iBAAiB,CAAC,EAAE,uBAAuB,CAAC;CAC7C;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvC,OAAO,EAEP,OAAO,EAAE,6BAA6B,CAAC,OAAO,EAAE,OAAO,CAAC,GACvD,cAAc,CAAC,OAAO,CAAC,CAsBzB;AAED;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|