@firtoz/drizzle-durable-sqlite 0.2.1 → 1.0.1
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 +26 -0
- package/README.md +21 -11
- package/package.json +14 -10
- package/src/drizzle-mutation-store.ts +105 -0
- package/src/drizzle-partial-sync-changelog.ts +65 -0
- package/src/drizzle-partial-sync-store.ts +442 -0
- package/src/durable-sqlite-collection.ts +5 -12
- package/src/durable-sqlite-sync-server.ts +91 -0
- package/src/index.ts +47 -1
- package/src/partial-sync-predicate-sql.ts +157 -0
- package/src/partial-sync-sqlite-db.ts +8 -0
- package/src/queryable-durable-object.ts +413 -0
- package/src/syncable-durable-object.ts +284 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @firtoz/drizzle-durable-sqlite
|
|
2
2
|
|
|
3
|
+
## 1.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies []:
|
|
8
|
+
- @firtoz/websocket-do@8.0.0
|
|
9
|
+
- @firtoz/collection-sync@2.0.0
|
|
10
|
+
|
|
11
|
+
## 1.0.0
|
|
12
|
+
|
|
13
|
+
### Major Changes
|
|
14
|
+
|
|
15
|
+
- [#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`.
|
|
16
|
+
|
|
17
|
+
**`@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.
|
|
18
|
+
|
|
19
|
+
**`@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.
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- 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)]:
|
|
24
|
+
- @firtoz/collection-sync@1.0.0
|
|
25
|
+
- @firtoz/db-helpers@2.1.0
|
|
26
|
+
- @firtoz/drizzle-utils@1.2.0
|
|
27
|
+
- @firtoz/websocket-do@7.1.0
|
|
28
|
+
|
|
3
29
|
## 0.2.1
|
|
4
30
|
|
|
5
31
|
### Patch 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
|
-
|
|
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
|
-
|
|
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,9 +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
|
|
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.
|
|
81
93
|
|
|
82
|
-
For explicit collection type annotations, use `
|
|
94
|
+
For explicit collection type annotations, use `DrizzleSqliteTableCollection<TTable>` from `@firtoz/drizzle-utils` (same shape as WASM SQLite collections).
|
|
83
95
|
|
|
84
96
|
Example `schema.ts`:
|
|
85
97
|
|
|
@@ -109,16 +121,14 @@ import { createCollection } from "@tanstack/db";
|
|
|
109
121
|
import type { DrizzleSqliteDODatabase } from "drizzle-orm/durable-sqlite";
|
|
110
122
|
import { drizzle } from "drizzle-orm/durable-sqlite";
|
|
111
123
|
import { migrate } from "drizzle-orm/durable-sqlite/migrator";
|
|
112
|
-
import {
|
|
113
|
-
|
|
114
|
-
type DurableSqliteCollection,
|
|
115
|
-
} from "@firtoz/drizzle-durable-sqlite";
|
|
124
|
+
import { durableSqliteCollectionOptions } from "@firtoz/drizzle-durable-sqlite";
|
|
125
|
+
import type { DrizzleSqliteTableCollection } from "@firtoz/drizzle-utils";
|
|
116
126
|
import { Hono } from "hono";
|
|
117
127
|
import { z } from "zod";
|
|
118
128
|
import migrations from "../drizzle/migrations.js";
|
|
119
129
|
import * as schema from "./schema";
|
|
120
130
|
|
|
121
|
-
type TodosCollection =
|
|
131
|
+
type TodosCollection = DrizzleSqliteTableCollection<typeof schema.todosTable>;
|
|
122
132
|
|
|
123
133
|
export class TodosDurableObject extends DurableObject<Env> {
|
|
124
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.
|
|
3
|
+
"version": "1.0.1",
|
|
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": "
|
|
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.
|
|
65
|
-
"@tanstack/db": "^0.
|
|
66
|
-
"drizzle-orm": "^0.45.
|
|
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/
|
|
72
|
-
"@firtoz/
|
|
71
|
+
"@firtoz/collection-sync": "^2.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": "^8.0.0"
|
|
73
76
|
},
|
|
74
77
|
"devDependencies": {
|
|
75
|
-
"@cloudflare/workers-types": "^4.
|
|
76
|
-
"@tanstack/db": "^0.
|
|
77
|
-
"drizzle-orm": "^0.45.
|
|
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>>;
|