@firtoz/drizzle-durable-sqlite 0.2.0 → 1.0.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 CHANGED
@@ -1,5 +1,29 @@
1
1
  # @firtoz/drizzle-durable-sqlite
2
2
 
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - [#64](https://github.com/firtoz/fullstack-toolkit/pull/64) [`556555a`](https://github.com/firtoz/fullstack-toolkit/commit/556555a2e09030a8658be8c07b5881e72be64b2f) Thanks [@firtoz](https://github.com/firtoz)! - **Breaking:** Shared TanStack collection row type is `DrizzleSqliteTableCollection` from `@firtoz/drizzle-utils` — remove `DrizzleSqliteCollection` / `DurableSqliteCollection` type exports from the wasm and durable packages and import from `@firtoz/drizzle-utils` instead. Align bridge/session row types with `PartialSyncRowShape`.
8
+
9
+ **`@firtoz/drizzle-durable-sqlite`:** `SyncableDurableObject` / `QueryableDurableObject`, Drizzle partial sync store (`createDrizzlePartialSyncStore`) with scoped `changesSince`, `getRow`, visibility and `rangeReconcile` hooks, `PartialSyncMutationHandler`, `applyDurableMutationIntents`, queued WS message handling, optional `seedInBackground`, and integration with `PartialSyncServerBridge` / `SyncServerBridge` as documented in the package.
10
+
11
+ **`@firtoz/drizzle-sqlite-wasm`:** `createSyncedSqliteCollection`, optional `workerOpenOptions` for worker `Start` / provider hooks, table sync upsert on `id` for replayed inserts, and receive-sync persist key alignment with generic sync.
12
+
13
+ ### Patch Changes
14
+
15
+ - Updated dependencies [[`afb1873`](https://github.com/firtoz/fullstack-toolkit/commit/afb187331bebb1f0231f6615c5b74989191cf30d), [`556555a`](https://github.com/firtoz/fullstack-toolkit/commit/556555a2e09030a8658be8c07b5881e72be64b2f), [`556555a`](https://github.com/firtoz/fullstack-toolkit/commit/556555a2e09030a8658be8c07b5881e72be64b2f), [`556555a`](https://github.com/firtoz/fullstack-toolkit/commit/556555a2e09030a8658be8c07b5881e72be64b2f), [`556555a`](https://github.com/firtoz/fullstack-toolkit/commit/556555a2e09030a8658be8c07b5881e72be64b2f)]:
16
+ - @firtoz/collection-sync@1.0.0
17
+ - @firtoz/db-helpers@2.1.0
18
+ - @firtoz/drizzle-utils@1.2.0
19
+ - @firtoz/websocket-do@7.1.0
20
+
21
+ ## 0.2.1
22
+
23
+ ### Patch Changes
24
+
25
+ - [`8b839f2`](https://github.com/firtoz/fullstack-toolkit/commit/8b839f2227f50409af649aab87178e039aad55dc) Thanks [@firtoz](https://github.com/firtoz)! - Export collection helper types for Drizzle-backed TanStack DB collections so users can declare collection variables with preserved select and insert inference from table schemas.
26
+
3
27
  ## 0.2.0
4
28
 
5
29
  ### Minor Changes
package/README.md CHANGED
@@ -15,9 +15,12 @@ bun add -d drizzle-kit @cloudflare/workers-types
15
15
  bun add hono zod @hono/zod-validator @firtoz/hono-fetcher
16
16
  ```
17
17
 
18
- ## Drizzle Kit
18
+ ## Drizzle Kit (Required)
19
19
 
20
- Use the durable-sqlite driver so generated migrations match DO storage:
20
+ Drizzle Kit migrations are mandatory for Durable Object SQLite setups in this toolkit.
21
+ Do not skip migrations and do not rely on ad-hoc runtime table creation.
22
+
23
+ Use this exact durable-sqlite driver config so generated migrations match DO storage:
21
24
 
22
25
  ```typescript
23
26
  import { defineConfig } from "drizzle-kit";
@@ -30,6 +33,15 @@ export default defineConfig({
30
33
  });
31
34
  ```
32
35
 
36
+ Then generate migrations:
37
+
38
+ ```bash
39
+ bun run db:generate
40
+ ```
41
+
42
+ `db:generate` should run `drizzle-kit generate` and write SQL files under `./drizzle`.
43
+ Keep `drizzle/migrations.js` and `drizzle/migrations.d.ts` in sync with the generated SQL files.
44
+
33
45
  ## Wrangler
34
46
 
35
47
  - Import SQL as text for the migrator ([Drizzle DO docs](https://orm.drizzle.team/docs/connect-cloudflare-do)).
@@ -44,9 +56,9 @@ export default defineConfig({
44
56
  }
45
57
  ```
46
58
 
47
- ## Durable Object initialization
59
+ ## Durable Object initialization (migrate first)
48
60
 
49
- **Recommended for generic DOs:** run migrations inside `ctx.blockConcurrencyWhile` so the schema is ready before `fetch` or alarms. Example:
61
+ Run migrations in `ctx.blockConcurrencyWhile` before handling requests so schema is ready before `fetch` or alarms. Example:
50
62
 
51
63
  ```typescript
52
64
  import { DurableObject } from "cloudflare:workers";
@@ -77,7 +89,9 @@ Use `durableSqliteCollectionOptions` with tables built via `syncableTable` from
77
89
 
78
90
  `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
91
 
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()`).
92
+ If something else must finish before sync runs, pass `readyPromise`. This is not a replacement for Drizzle migrations; run `migrate(db, migrations)` first in your DO initialization.
93
+
94
+ For explicit collection type annotations, use `DrizzleSqliteTableCollection<TTable>` from `@firtoz/drizzle-utils` (same shape as WASM SQLite collections).
81
95
 
82
96
  Example `schema.ts`:
83
97
 
@@ -107,13 +121,14 @@ import { createCollection } from "@tanstack/db";
107
121
  import type { DrizzleSqliteDODatabase } from "drizzle-orm/durable-sqlite";
108
122
  import { drizzle } from "drizzle-orm/durable-sqlite";
109
123
  import { migrate } from "drizzle-orm/durable-sqlite/migrator";
124
+ import { durableSqliteCollectionOptions } from "@firtoz/drizzle-durable-sqlite";
125
+ import type { DrizzleSqliteTableCollection } from "@firtoz/drizzle-utils";
110
126
  import { Hono } from "hono";
111
127
  import { z } from "zod";
112
- import { durableSqliteCollectionOptions } from "@firtoz/drizzle-durable-sqlite";
113
128
  import migrations from "../drizzle/migrations.js";
114
129
  import * as schema from "./schema";
115
130
 
116
- type TodosCollection = ReturnType<typeof createCollection>;
131
+ type TodosCollection = DrizzleSqliteTableCollection<typeof schema.todosTable>;
117
132
 
118
133
  export class TodosDurableObject extends DurableObject<Env> {
119
134
  private db!: DrizzleSqliteDODatabase<typeof schema>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firtoz/drizzle-durable-sqlite",
3
- "version": "0.2.0",
3
+ "version": "1.0.0",
4
4
  "description": "TanStack DB collections backed by Drizzle on Cloudflare Durable Object SQLite",
5
5
  "main": "./src/index.ts",
6
6
  "module": "./src/index.ts",
@@ -30,7 +30,7 @@
30
30
  "CHANGELOG.md"
31
31
  ],
32
32
  "scripts": {
33
- "typecheck": "tsc --noEmit -p ./tsconfig.json",
33
+ "typecheck": "tsgo --noEmit -p ./tsconfig.json",
34
34
  "lint": "biome check --write src",
35
35
  "lint:ci": "biome ci src",
36
36
  "format": "biome format src --write"
@@ -61,21 +61,25 @@
61
61
  "access": "public"
62
62
  },
63
63
  "peerDependencies": {
64
- "@cloudflare/workers-types": "^4.20260317.1",
65
- "@tanstack/db": "^0.5.33",
66
- "drizzle-orm": "^0.45.1",
64
+ "@cloudflare/workers-types": "^4.20260329.1",
65
+ "@tanstack/db": "^0.6.1",
66
+ "drizzle-orm": "^0.45.2",
67
67
  "drizzle-valibot": ">=0.4.0",
68
68
  "valibot": ">=1.3.1"
69
69
  },
70
70
  "dependencies": {
71
- "@firtoz/db-helpers": "^2.0.0",
72
- "@firtoz/drizzle-utils": "^1.1.0"
71
+ "@firtoz/collection-sync": "^1.0.0",
72
+ "@firtoz/db-helpers": "^2.1.0",
73
+ "@firtoz/drizzle-utils": "^1.2.0",
74
+ "@firtoz/maybe-error": "^1.5.2",
75
+ "@firtoz/websocket-do": "^7.1.0"
73
76
  },
74
77
  "devDependencies": {
75
- "@cloudflare/workers-types": "^4.20260317.1",
76
- "@tanstack/db": "^0.5.33",
77
- "drizzle-orm": "^0.45.1",
78
+ "@cloudflare/workers-types": "^4.20260329.1",
79
+ "@tanstack/db": "^0.6.1",
80
+ "drizzle-orm": "^0.45.2",
78
81
  "drizzle-valibot": "^0.4.2",
82
+ "hono": "^4.12.9",
79
83
  "typescript": "^6.0.2",
80
84
  "valibot": "^1.3.1"
81
85
  }
@@ -0,0 +1,105 @@
1
+ import type { SyncServerBridgeStore } from "@firtoz/collection-sync";
2
+ import type { SyncMessage } from "@firtoz/db-helpers";
3
+ import { exhaustiveGuard } from "@firtoz/maybe-error";
4
+ import { eq, getTableColumns } from "drizzle-orm";
5
+ import type { SQLiteTable } from "drizzle-orm/sqlite-core";
6
+ import type { DrizzleChangelogHelper } from "./drizzle-partial-sync-changelog";
7
+ import type { PartialSyncSqliteDatabase } from "./partial-sync-sqlite-db";
8
+
9
+ export type CreateDrizzleMutationStoreOptions<
10
+ TSchema extends Record<string, unknown>,
11
+ TRow extends { id: string | number },
12
+ > = {
13
+ db: PartialSyncSqliteDatabase<TSchema>;
14
+ table: SQLiteTable;
15
+ changelogHelper: DrizzleChangelogHelper<TSchema>;
16
+ /** Columns to copy from `update` message.value into SET (excluding id). */
17
+ updateColumns: readonly (keyof TRow & string)[];
18
+ };
19
+
20
+ export function createDrizzleMutationStore<
21
+ TSchema extends Record<string, unknown>,
22
+ TRow extends { id: string | number },
23
+ >(
24
+ options: CreateDrizzleMutationStoreOptions<TSchema, TRow>,
25
+ ): SyncServerBridgeStore<TRow> {
26
+ const { db, table, changelogHelper, updateColumns } = options;
27
+ const tableColumns = getTableColumns(table);
28
+ const idCol = tableColumns.id;
29
+ if (idCol === undefined) {
30
+ throw new Error("Mutation table must have an id column");
31
+ }
32
+
33
+ return {
34
+ applySyncMessages: async (messages: SyncMessage<TRow>[]) => {
35
+ for (const message of messages) {
36
+ switch (message.type) {
37
+ case "insert":
38
+ await db
39
+ .insert(table)
40
+ .values(message.value as Record<string, unknown>);
41
+ await changelogHelper.append(
42
+ "insert",
43
+ String(message.value.id),
44
+ message.value,
45
+ );
46
+ break;
47
+ case "update": {
48
+ const setPayload: Record<string, unknown> = {};
49
+ const v = message.value as Record<string, unknown>;
50
+ for (const col of updateColumns) {
51
+ setPayload[col] = v[col];
52
+ }
53
+ await db
54
+ .update(table)
55
+ .set(setPayload as never)
56
+ .where(eq(idCol, message.value.id as never));
57
+ await changelogHelper.append("update", String(message.value.id), {
58
+ value: message.value,
59
+ previousValue: message.previousValue,
60
+ });
61
+ break;
62
+ }
63
+ case "delete": {
64
+ const existing = await db
65
+ .select()
66
+ .from(table)
67
+ .where(eq(idCol, message.key as never))
68
+ .limit(1);
69
+ const prev = existing[0];
70
+ await db.delete(table).where(eq(idCol, message.key as never));
71
+ await changelogHelper.append(
72
+ "delete",
73
+ String(message.key),
74
+ prev ?? null,
75
+ );
76
+ break;
77
+ }
78
+ case "truncate":
79
+ await changelogHelper.deleteAll();
80
+ await db.delete(table);
81
+ break;
82
+ default:
83
+ exhaustiveGuard(message);
84
+ }
85
+ }
86
+ },
87
+
88
+ getSnapshotMessages: async () => {
89
+ const rows = await db.select().from(table);
90
+ return rows.map((row) => ({
91
+ type: "insert" as const,
92
+ value: row as TRow,
93
+ }));
94
+ },
95
+
96
+ getRow: async (key: string | number) => {
97
+ const rows = await db
98
+ .select()
99
+ .from(table)
100
+ .where(eq(idCol, key as never))
101
+ .limit(1);
102
+ return rows[0] as TRow | undefined;
103
+ },
104
+ };
105
+ }
@@ -0,0 +1,65 @@
1
+ import { getTableColumns, gt } from "drizzle-orm";
2
+ import type { SQLiteTable } from "drizzle-orm/sqlite-core";
3
+ import type { PartialSyncSqliteDatabase } from "./partial-sync-sqlite-db";
4
+
5
+ export type ChangelogOperation = "insert" | "update" | "delete";
6
+
7
+ export type DrizzleChangelogHelperOptions<
8
+ TSchema extends Record<string, unknown>,
9
+ > = {
10
+ db: PartialSyncSqliteDatabase<TSchema>;
11
+ changelogTable: SQLiteTable;
12
+ serializeJson: (value: unknown) => string;
13
+ };
14
+
15
+ export function createDrizzleChangelogHelper<
16
+ TSchema extends Record<string, unknown>,
17
+ >(options: DrizzleChangelogHelperOptions<TSchema>) {
18
+ const cols = getTableColumns(options.changelogTable);
19
+ const rowIdCol = cols.rowId;
20
+ const operationCol = cols.operation;
21
+ const versionCol = cols.version;
22
+ if (
23
+ rowIdCol === undefined ||
24
+ operationCol === undefined ||
25
+ versionCol === undefined
26
+ ) {
27
+ throw new Error(
28
+ "changelogTable must have rowId, operation, and version columns",
29
+ );
30
+ }
31
+
32
+ return {
33
+ append: async (
34
+ operation: ChangelogOperation,
35
+ rowId: string,
36
+ payload: unknown,
37
+ ): Promise<void> => {
38
+ const version = new Date();
39
+ await options.db.insert(options.changelogTable).values({
40
+ rowId,
41
+ operation,
42
+ version,
43
+ payloadJson:
44
+ payload === null || payload === undefined
45
+ ? null
46
+ : options.serializeJson(payload),
47
+ } as Record<string, unknown>);
48
+ },
49
+
50
+ selectAfterVersion: async (sinceVersionMs: number) => {
51
+ const rows = await options.db
52
+ .select()
53
+ .from(options.changelogTable)
54
+ .where(gt(versionCol, new Date(sinceVersionMs)));
55
+ return rows;
56
+ },
57
+
58
+ deleteAll: async (): Promise<void> => {
59
+ await options.db.delete(options.changelogTable);
60
+ },
61
+ };
62
+ }
63
+
64
+ export type DrizzleChangelogHelper<TSchema extends Record<string, unknown>> =
65
+ ReturnType<typeof createDrizzleChangelogHelper<TSchema>>;