@firtoz/drizzle-durable-sqlite 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # @firtoz/drizzle-durable-sqlite
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`b714ebb`](https://github.com/firtoz/fullstack-toolkit/commit/b714ebbb62ec0e3c3aa56c4105e7499fac11d1e5) Thanks [@firtoz](https://github.com/firtoz)! - Extract shared SQLite TanStack sync backend (`createSqliteTableSyncBackend`, IR→Drizzle helpers, `SQLOperation` types) into `@firtoz/drizzle-utils`. Add `@firtoz/drizzle-durable-sqlite` for Durable Object SQLite collections (`durableSqliteCollectionOptions`). Refactor `@firtoz/drizzle-sqlite-wasm` to use the shared backend with `driverMode: "async"`. `durableSqliteCollectionOptions` accepts optional `readyPromise` (defaults to immediate readiness). README documents the class-field Hono pattern, `app.fetch(request, env)` for bindings, optional `on-demand` + `preload` vs eager + `onFirstReady`, and `honoDoFetcherWithName` without a separate exported app type. Restore JSDoc on `DrizzleSqliteCollectionConfig` (`debug`, `checkpoint`, `interceptor`) for editor tooltips. Align `createStandaloneCollection` generics with `InsertToSelectSchema` from `@firtoz/drizzle-utils`.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [[`b714ebb`](https://github.com/firtoz/fullstack-toolkit/commit/b714ebbb62ec0e3c3aa56c4105e7499fac11d1e5)]:
12
+ - @firtoz/drizzle-utils@1.1.0
13
+
14
+ ## 0.1.0
15
+
16
+ Initial release: TanStack DB collection options for Drizzle on Cloudflare Durable Object SQLite.
package/README.md ADDED
@@ -0,0 +1,211 @@
1
+ # @firtoz/drizzle-durable-sqlite
2
+
3
+ TanStack DB collection configuration for **Drizzle ORM** on **Cloudflare Durable Object SQLite** (`drizzle-orm/durable-sqlite`). This mirrors [`@firtoz/drizzle-sqlite-wasm`](../drizzle-sqlite-wasm) for the browser (SQLite WASM + workers), but targets Workers/DOs only—no React provider, no OPFS, no Web Workers.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ bun add @firtoz/drizzle-durable-sqlite @firtoz/drizzle-utils drizzle-orm @tanstack/db
9
+ bun add -d drizzle-kit @cloudflare/workers-types
10
+ ```
11
+
12
+ **Optional** (only for the [Hono + Zod example](#tanstack-db-collection-and-crud-from-a-durable-object) and a typed [`honoDoFetcher`](https://github.com/firtoz/fullstack-toolkit/tree/main/packages/hono-fetcher) client):
13
+
14
+ ```bash
15
+ bun add hono zod @hono/zod-validator @firtoz/hono-fetcher
16
+ ```
17
+
18
+ ## Drizzle Kit
19
+
20
+ Use the durable-sqlite driver so generated migrations match DO storage:
21
+
22
+ ```typescript
23
+ import { defineConfig } from "drizzle-kit";
24
+
25
+ export default defineConfig({
26
+ schema: "./src/schema.ts",
27
+ out: "./drizzle",
28
+ dialect: "sqlite",
29
+ driver: "durable-sqlite",
30
+ });
31
+ ```
32
+
33
+ ## Wrangler
34
+
35
+ - Import SQL as text for the migrator ([Drizzle DO docs](https://orm.drizzle.team/docs/connect-cloudflare-do)).
36
+ - Use `new_sqlite_classes` for SQLite-backed Durable Objects.
37
+
38
+ ```jsonc
39
+ {
40
+ "rules": [
41
+ { "type": "Text", "globs": ["**/*.sql"], "fallthrough": true }
42
+ ],
43
+ "migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyDurableObject"] }]
44
+ }
45
+ ```
46
+
47
+ ## Durable Object initialization
48
+
49
+ **Recommended for generic DOs:** run migrations inside `ctx.blockConcurrencyWhile` so the schema is ready before `fetch` or alarms. Example:
50
+
51
+ ```typescript
52
+ import { DurableObject } from "cloudflare:workers";
53
+ import { drizzle } from "drizzle-orm/durable-sqlite";
54
+ import { migrate } from "drizzle-orm/durable-sqlite/migrator";
55
+ import migrations from "../drizzle/migrations.js";
56
+ import * as schema from "./schema";
57
+
58
+ export class MyDurableObject extends DurableObject {
59
+ private db!: ReturnType<typeof drizzle<typeof schema>>;
60
+
61
+ constructor(ctx: DurableObjectState, env: Env) {
62
+ super(ctx, env);
63
+ this.ctx.blockConcurrencyWhile(async () => {
64
+ const db = drizzle(ctx.storage, { schema });
65
+ migrate(db, migrations);
66
+ this.db = db;
67
+ });
68
+ }
69
+ }
70
+ ```
71
+
72
+ [`@firtoz/chat-agent-drizzle`](../chat-agent-drizzle) uses a different pattern: `ChatAgentBase` calls a synchronous `dbInitialize()` hook (see `DrizzleChatAgent`). Use `blockConcurrencyWhile` when you are not on that agent base class.
73
+
74
+ ## TanStack DB collection and CRUD from a Durable Object
75
+
76
+ Use `durableSqliteCollectionOptions` with tables built via `syncableTable` from `@firtoz/drizzle-utils` (same as WASM). The DO SQLite driver is **sync**; the shared backend uses synchronous transactions and `.all()` / `.run()` for mutations (see `@firtoz/drizzle-utils` `createSqliteTableSyncBackend` with `driverMode: "sync"`).
77
+
78
+ `tableName` must be the **property name** on your Drizzle schema object (e.g. `export const schema = { todosTable }` → `tableName: "todosTable"`), not the SQLite table name string.
79
+
80
+ If something else must finish before sync runs (e.g. a migration promise), pass `readyPromise`. When omitted, the collection treats storage as ready immediately (same as `Promise.resolve()`).
81
+
82
+ Example `schema.ts`:
83
+
84
+ ```typescript
85
+ import { syncableTable } from "@firtoz/drizzle-utils";
86
+ import { text } from "drizzle-orm/sqlite-core";
87
+
88
+ export const todosTable = syncableTable("todos", {
89
+ title: text("title").notNull(),
90
+ });
91
+
92
+ export const schema = { todosTable };
93
+ ```
94
+
95
+ Example Durable Object: migrate in `blockConcurrencyWhile`, create the TanStack collection, then define a [Hono](https://hono.dev/) app as a **class field** (chained `.get(…)`, `.post(…)`, …) and forward `fetch` to it—the same shape as the [Durable Object snippet in `@firtoz/hono-fetcher`](../hono-fetcher/README.md).
96
+
97
+ Route handlers only run when a request is handled; Workers finish your constructor’s `blockConcurrencyWhile` work **before** the first `fetch`, so `this.todos` is always assigned before any handler runs. Pass **`this.env`** into `app.fetch` so `c.env` matches `Bindings: Env` (middleware, secrets, etc.). Use [`zValidator`](https://github.com/honojs/middleware/tree/main/packages/zod-validator) so JSON bodies and `:id` params are validated; [`honoDoFetcherWithName`](../hono-fetcher/README.md) can infer request/response types from that app without a separate exported `App` type.
98
+
99
+ For `syncMode: "on-demand"`, `await collection.preload()` inside `blockConcurrencyWhile` if you want rows loaded before serving. For `syncMode: "eager"`, you can wait for the first sync with `await new Promise<void>((resolve) => collection.onFirstReady(() => resolve()))` after `preload()`.
100
+
101
+ A minimal vitest + DO setup lives in [`tests/drizzle-durable-sqlite-test`](https://github.com/firtoz/fullstack-toolkit/tree/main/tests/drizzle-durable-sqlite-test).
102
+
103
+ ```typescript
104
+ import { DurableObject } from "cloudflare:workers";
105
+ import { zValidator } from "@hono/zod-validator";
106
+ import { createCollection } from "@tanstack/db";
107
+ import type { DrizzleSqliteDODatabase } from "drizzle-orm/durable-sqlite";
108
+ import { drizzle } from "drizzle-orm/durable-sqlite";
109
+ import { migrate } from "drizzle-orm/durable-sqlite/migrator";
110
+ import { Hono } from "hono";
111
+ import { z } from "zod";
112
+ import { durableSqliteCollectionOptions } from "@firtoz/drizzle-durable-sqlite";
113
+ import migrations from "../drizzle/migrations.js";
114
+ import * as schema from "./schema";
115
+
116
+ type TodosCollection = ReturnType<typeof createCollection>;
117
+
118
+ export class TodosDurableObject extends DurableObject<Env> {
119
+ private db!: DrizzleSqliteDODatabase<typeof schema>;
120
+ private todos!: TodosCollection;
121
+ app = new Hono<{ Bindings: Env }>()
122
+ .get("/todos", (c) => c.json({ todos: this.todos.toArray }))
123
+ .post(
124
+ "/todos",
125
+ zValidator("json", z.object({ title: z.string() })),
126
+ async (c) => {
127
+ const { title } = c.req.valid("json");
128
+ const tx = this.todos.insert([{ title }]);
129
+ await tx.isPersisted.promise;
130
+ return c.json({ ok: true as const }, 201);
131
+ },
132
+ )
133
+ .patch(
134
+ "/todos/:id",
135
+ zValidator("param", z.object({ id: z.string() })),
136
+ zValidator(
137
+ "json",
138
+ z.object({ title: z.string().optional() }),
139
+ ),
140
+ async (c) => {
141
+ const { id } = c.req.valid("param");
142
+ const { title } = c.req.valid("json");
143
+ const tx = this.todos.update(id, (draft) => {
144
+ if (title !== undefined) draft.title = title;
145
+ });
146
+ await tx.isPersisted.promise;
147
+ return c.json(this.todos.state.get(id) ?? null);
148
+ },
149
+ )
150
+ .delete(
151
+ "/todos/:id",
152
+ zValidator("param", z.object({ id: z.string() })),
153
+ async (c) => {
154
+ const { id } = c.req.valid("param");
155
+ const tx = this.todos.delete([id]);
156
+ await tx.isPersisted.promise;
157
+ return new Response(null, { status: 204 });
158
+ },
159
+ )
160
+ .notFound((c) => c.text("Not found", 404));
161
+
162
+ constructor(ctx: DurableObjectState, env: Env) {
163
+ super(ctx, env);
164
+
165
+ this.ctx.blockConcurrencyWhile(async () => {
166
+ const db = drizzle(ctx.storage, { schema });
167
+ migrate(db, migrations);
168
+ this.db = db;
169
+
170
+ const todos = createCollection(
171
+ durableSqliteCollectionOptions({
172
+ drizzle: db,
173
+ tableName: "todosTable",
174
+ syncMode: "eager",
175
+ }),
176
+ );
177
+ this.todos = todos;
178
+ todos.preload(); // Preload to ensure the data's in the collection from storage.
179
+ await new Promise<void>((resolve) => todos.onFirstReady(() => resolve()));
180
+ });
181
+ }
182
+
183
+ fetch(request: Request) {
184
+ return this.app.fetch(request, this.env);
185
+ }
186
+ }
187
+ ```
188
+
189
+ **Typed client from your worker** (same idea as [Durable Objects in `@firtoz/hono-fetcher`](../hono-fetcher/README.md)):
190
+
191
+ ```typescript
192
+ import { honoDoFetcherWithName } from "@firtoz/hono-fetcher";
193
+
194
+ const api = honoDoFetcherWithName(env.TODOS, "my-list");
195
+
196
+ await api.post({
197
+ url: "/todos",
198
+ body: { title: "Buy milk" },
199
+ });
200
+
201
+ const list = await api.get({ url: "/todos" });
202
+ const data = await list.json();
203
+ ```
204
+
205
+ `body` / response inference follows your `zValidator` + `c.json` shapes; use `params: { id: "…" }` on `/todos/:id` routes. If inference ever fails to pick up the DO’s `app` type, pass an explicit generic, e.g. `honoDoFetcherWithName<InstanceType<typeof TodosDurableObject>["app"]>(…)`.
206
+
207
+ Adjust paths (`../drizzle/migrations.js`, `./schema`) and `Env` to match your worker. To return the created row from `POST`, read from `this.todos.state` / `toArray` after `isPersisted` and return `c.json(…)` with a shape that matches what you want the fetcher to infer.
208
+
209
+ ## License
210
+
211
+ MIT
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "@firtoz/drizzle-durable-sqlite",
3
+ "version": "0.2.0",
4
+ "description": "TanStack DB collections backed by Drizzle on Cloudflare Durable Object SQLite",
5
+ "main": "./src/index.ts",
6
+ "module": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "type": "module",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./src/index.ts",
12
+ "import": "./src/index.ts",
13
+ "require": "./src/index.ts"
14
+ },
15
+ "./durableSqliteCollectionOptions": {
16
+ "types": "./src/durable-sqlite-collection.ts",
17
+ "import": "./src/durable-sqlite-collection.ts",
18
+ "require": "./src/durable-sqlite-collection.ts"
19
+ },
20
+ "./*": {
21
+ "types": "./src/*.ts",
22
+ "import": "./src/*.ts",
23
+ "require": "./src/*.ts"
24
+ }
25
+ },
26
+ "files": [
27
+ "src/**/*.ts",
28
+ "!src/**/*.test.ts",
29
+ "README.md",
30
+ "CHANGELOG.md"
31
+ ],
32
+ "scripts": {
33
+ "typecheck": "tsc --noEmit -p ./tsconfig.json",
34
+ "lint": "biome check --write src",
35
+ "lint:ci": "biome ci src",
36
+ "format": "biome format src --write"
37
+ },
38
+ "keywords": [
39
+ "typescript",
40
+ "cloudflare",
41
+ "durable-objects",
42
+ "sqlite",
43
+ "drizzle",
44
+ "tanstack-db"
45
+ ],
46
+ "author": "Firtina Ozbalikchi <firtoz@github.com>",
47
+ "license": "MIT",
48
+ "homepage": "https://github.com/firtoz/fullstack-toolkit#readme",
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "https://github.com/firtoz/fullstack-toolkit.git",
52
+ "directory": "packages/drizzle-durable-sqlite"
53
+ },
54
+ "bugs": {
55
+ "url": "https://github.com/firtoz/fullstack-toolkit/issues"
56
+ },
57
+ "engines": {
58
+ "node": ">=18.0.0"
59
+ },
60
+ "publishConfig": {
61
+ "access": "public"
62
+ },
63
+ "peerDependencies": {
64
+ "@cloudflare/workers-types": "^4.20260317.1",
65
+ "@tanstack/db": "^0.5.33",
66
+ "drizzle-orm": "^0.45.1",
67
+ "drizzle-valibot": ">=0.4.0",
68
+ "valibot": ">=1.3.1"
69
+ },
70
+ "dependencies": {
71
+ "@firtoz/db-helpers": "^2.0.0",
72
+ "@firtoz/drizzle-utils": "^1.1.0"
73
+ },
74
+ "devDependencies": {
75
+ "@cloudflare/workers-types": "^4.20260317.1",
76
+ "@tanstack/db": "^0.5.33",
77
+ "drizzle-orm": "^0.45.1",
78
+ "drizzle-valibot": "^0.4.2",
79
+ "typescript": "^6.0.2",
80
+ "valibot": "^1.3.1"
81
+ }
82
+ }
@@ -0,0 +1,138 @@
1
+ import type {
2
+ InferSchemaOutput,
3
+ SyncMode,
4
+ CollectionConfig,
5
+ } from "@tanstack/db";
6
+ import type { Table } from "drizzle-orm";
7
+ import type { DrizzleSqliteDODatabase } from "drizzle-orm/durable-sqlite";
8
+ import type { CollectionUtils } from "@firtoz/db-helpers";
9
+ import type {
10
+ SelectSchema,
11
+ InsertToSelectSchema,
12
+ TableWithRequiredFields,
13
+ BaseSyncConfig,
14
+ } from "@firtoz/drizzle-utils";
15
+ import {
16
+ createSyncFunction,
17
+ createInsertSchemaWithIdDefault,
18
+ createGetKeyFunction,
19
+ createCollectionConfig,
20
+ createSqliteTableSyncBackend,
21
+ type SQLOperation,
22
+ type SQLInterceptor,
23
+ } from "@firtoz/drizzle-utils";
24
+ export type { SQLOperation, SQLInterceptor };
25
+
26
+ /**
27
+ * Drizzle database type for `drizzle-orm/durable-sqlite` (Cloudflare DO SQLite).
28
+ */
29
+ export type AnyDurableSqliteDatabase = DrizzleSqliteDODatabase<
30
+ Record<string, unknown>
31
+ >;
32
+
33
+ export type DurableDrizzleSchema<TDrizzle extends AnyDurableSqliteDatabase> =
34
+ TDrizzle["_"]["fullSchema"];
35
+
36
+ export interface DurableSqliteCollectionConfig<
37
+ TDrizzle extends AnyDurableSqliteDatabase,
38
+ TTableName extends ValidTableNames<DurableDrizzleSchema<TDrizzle>>,
39
+ > {
40
+ drizzle: TDrizzle;
41
+ tableName: ValidTableNames<DurableDrizzleSchema<TDrizzle>> extends never
42
+ ? {
43
+ $error: "The schema needs to include at least one table that uses the syncableTable function.";
44
+ }
45
+ : TTableName;
46
+ /**
47
+ * Await before running sync queries (e.g. migrations finishing). Omit or leave undefined to use an already-resolved promise (no extra wait).
48
+ */
49
+ readyPromise?: Promise<void>;
50
+ syncMode?: SyncMode;
51
+ debug?: boolean;
52
+ interceptor?: SQLInterceptor;
53
+ }
54
+
55
+ export type ValidTableNames<TSchema extends Record<string, unknown>> = {
56
+ [K in keyof TSchema]: TSchema[K] extends TableWithRequiredFields ? K : never;
57
+ }[keyof TSchema];
58
+
59
+ export type DurableSqliteCollectionConfigResult<TTable extends Table> = Omit<
60
+ CollectionConfig<
61
+ InferSchemaOutput<SelectSchema<TTable>>,
62
+ string,
63
+ InsertToSelectSchema<TTable>
64
+ >,
65
+ "utils"
66
+ > & {
67
+ schema: InsertToSelectSchema<TTable>;
68
+ utils: CollectionUtils<InferSchemaOutput<SelectSchema<TTable>>>;
69
+ };
70
+
71
+ /**
72
+ * TanStack DB collection configuration for a table stored in Durable Object SQLite via Drizzle.
73
+ *
74
+ * Uses `driverMode: "sync"` internally: DO SQLite runs `transactionSync`, so mutations use
75
+ * `.all()` / `.run()` inside a synchronous transaction callback (see `createSqliteTableSyncBackend` in `@firtoz/drizzle-utils`).
76
+ */
77
+ export function durableSqliteCollectionOptions<
78
+ const TDrizzle extends AnyDurableSqliteDatabase,
79
+ const TTableName extends string &
80
+ ValidTableNames<DurableDrizzleSchema<TDrizzle>>,
81
+ TTable extends DurableDrizzleSchema<TDrizzle>[TTableName] &
82
+ TableWithRequiredFields,
83
+ >(
84
+ config: DurableSqliteCollectionConfig<TDrizzle, TTableName>,
85
+ ): DurableSqliteCollectionConfigResult<TTable> {
86
+ const tableName = config.tableName as string &
87
+ ValidTableNames<DurableDrizzleSchema<TDrizzle>>;
88
+
89
+ const table = config.drizzle._.fullSchema[tableName] as TTable;
90
+
91
+ const backend = createSqliteTableSyncBackend({
92
+ drizzle: config.drizzle,
93
+ table,
94
+ tableName: config.tableName as string,
95
+ debug: config.debug,
96
+ interceptor: config.interceptor,
97
+ driverMode: "sync",
98
+ });
99
+
100
+ const baseSyncConfig: BaseSyncConfig<TTable> = {
101
+ table,
102
+ readyPromise: config.readyPromise ?? Promise.resolve(),
103
+ syncMode: config.syncMode,
104
+ debug: config.debug,
105
+ };
106
+
107
+ const syncResult = createSyncFunction(baseSyncConfig, backend);
108
+
109
+ const schema = createInsertSchemaWithIdDefault(table);
110
+
111
+ return createCollectionConfig({
112
+ schema,
113
+ getKey: createGetKeyFunction<TTable>(),
114
+ syncResult,
115
+ onInsert: config.debug
116
+ ? async (params) => {
117
+ console.log("onInsert", params);
118
+ // biome-ignore lint/style/noNonNullAssertion: defined when sync runs
119
+ await syncResult.onInsert!(params);
120
+ }
121
+ : undefined,
122
+ onUpdate: config.debug
123
+ ? async (params) => {
124
+ console.log("onUpdate", params);
125
+ // biome-ignore lint/style/noNonNullAssertion: defined when sync runs
126
+ await syncResult.onUpdate!(params);
127
+ }
128
+ : undefined,
129
+ onDelete: config.debug
130
+ ? async (params) => {
131
+ console.log("onDelete", params);
132
+ // biome-ignore lint/style/noNonNullAssertion: defined when sync runs
133
+ await syncResult.onDelete!(params);
134
+ }
135
+ : undefined,
136
+ syncMode: config.syncMode,
137
+ });
138
+ }
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ export {
2
+ durableSqliteCollectionOptions,
3
+ type AnyDurableSqliteDatabase,
4
+ type DurableDrizzleSchema,
5
+ type DurableSqliteCollectionConfig,
6
+ type DurableSqliteCollectionConfigResult,
7
+ type ValidTableNames,
8
+ type SQLOperation,
9
+ type SQLInterceptor,
10
+ } from "./durable-sqlite-collection";