@firtoz/drizzle-utils 1.0.2 → 1.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 +19 -0
- package/package.json +7 -7
- package/src/collection-utils.ts +32 -27
- package/src/drizzle-sqlite-table-collection.ts +29 -0
- package/src/index.ts +15 -0
- package/src/sqlite-table-sync/convert-ir.ts +121 -0
- package/src/sqlite-table-sync/index.ts +10 -0
- package/src/sqlite-table-sync/sqlite-table-sync-backend.ts +535 -0
- package/src/sqlite-table-sync/types.ts +65 -0
- package/src/syncableTable.ts +4 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @firtoz/drizzle-utils
|
|
2
2
|
|
|
3
|
+
## 1.2.0
|
|
4
|
+
|
|
5
|
+
### Minor 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)! - **`@firtoz/drizzle-utils`:** Export `DrizzleSqliteTableCollection`; extend `BaseSyncConfig` / `createCollectionConfig` for typed `getSyncPersistKey` and `getKey` as `IdOf<TTable>`; avoid executing `syncableTable` default functions during table definition for Worker/DO globals; export collection helper types for Drizzle-backed TanStack collections.
|
|
8
|
+
|
|
9
|
+
**`@firtoz/drizzle-indexeddb` (major):** `deferLocalPersistence`, `handleBatchPut`, and related collection options; `receiveSync` persistence aligned with generic sync and partial-sync traffic; remove debug ingest usage. **Breaking** alongside the `@firtoz/drizzle-utils` sync/collection typing changes above (including `DrizzleSqliteTableCollection` and `BaseSyncConfig` expectations).
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Updated dependencies [[`556555a`](https://github.com/firtoz/fullstack-toolkit/commit/556555a2e09030a8658be8c07b5881e72be64b2f)]:
|
|
14
|
+
- @firtoz/db-helpers@2.1.0
|
|
15
|
+
|
|
16
|
+
## 1.1.0
|
|
17
|
+
|
|
18
|
+
### Minor Changes
|
|
19
|
+
|
|
20
|
+
- [`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`.
|
|
21
|
+
|
|
3
22
|
## 1.0.2
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firtoz/drizzle-utils",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Shared utilities and types for Drizzle-based packages",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"module": "./src/index.ts",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"CHANGELOG.md"
|
|
26
26
|
],
|
|
27
27
|
"scripts": {
|
|
28
|
-
"typecheck": "
|
|
28
|
+
"typecheck": "tsgo --noEmit -p ./tsconfig.json",
|
|
29
29
|
"lint": "biome check --write src",
|
|
30
30
|
"lint:ci": "biome ci src",
|
|
31
31
|
"format": "biome format src --write"
|
|
@@ -53,19 +53,19 @@
|
|
|
53
53
|
"access": "public"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@tanstack/db": ">=0.
|
|
57
|
-
"drizzle-orm": ">=0.45.
|
|
56
|
+
"@tanstack/db": ">=0.6.1",
|
|
57
|
+
"drizzle-orm": ">=0.45.2",
|
|
58
58
|
"drizzle-valibot": ">=0.4.0",
|
|
59
59
|
"valibot": ">=1.3.1"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
|
-
"@tanstack/db": "^0.
|
|
63
|
-
"drizzle-orm": "^0.45.
|
|
62
|
+
"@tanstack/db": "^0.6.1",
|
|
63
|
+
"drizzle-orm": "^0.45.2",
|
|
64
64
|
"drizzle-valibot": "^0.4.2",
|
|
65
65
|
"valibot": "^1.3.1"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@firtoz/db-helpers": "^2.
|
|
68
|
+
"@firtoz/db-helpers": "^2.1.0",
|
|
69
69
|
"@firtoz/maybe-error": "^1.5.2"
|
|
70
70
|
}
|
|
71
71
|
}
|
package/src/collection-utils.ts
CHANGED
|
@@ -66,6 +66,15 @@ export type InsertSchema<TTable extends Table> = BuildSchema<
|
|
|
66
66
|
undefined
|
|
67
67
|
>;
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Schema type with insert input (optionals for defaults) and select output (all fields present).
|
|
71
|
+
* Represents the standard input/output pair for collection schemas.
|
|
72
|
+
*/
|
|
73
|
+
export type InsertToSelectSchema<TTable extends Table> = v.GenericSchema<
|
|
74
|
+
v.InferInput<InsertSchema<TTable>>,
|
|
75
|
+
v.InferOutput<SelectSchema<TTable>>
|
|
76
|
+
>;
|
|
77
|
+
|
|
69
78
|
/**
|
|
70
79
|
* Helper type to get the table from schema by name
|
|
71
80
|
*/
|
|
@@ -82,7 +91,7 @@ export type InferCollectionFromTable<TTable extends Table> = Collection<
|
|
|
82
91
|
TTable["$inferSelect"],
|
|
83
92
|
IdOf<TTable>,
|
|
84
93
|
UtilsRecord,
|
|
85
|
-
|
|
94
|
+
InsertToSelectSchema<TTable>,
|
|
86
95
|
Omit<
|
|
87
96
|
TTable["$inferInsert"],
|
|
88
97
|
"id"
|
|
@@ -99,7 +108,7 @@ export const USE_DEDUPE = _USE_DEDUPE;
|
|
|
99
108
|
* Extends the generic (Drizzle-free) config with a Drizzle table reference.
|
|
100
109
|
*/
|
|
101
110
|
export interface BaseSyncConfig<TTable extends Table>
|
|
102
|
-
extends GenericBaseSyncConfig {
|
|
111
|
+
extends GenericBaseSyncConfig<InferSchemaOutput<SelectSchema<TTable>>> {
|
|
103
112
|
table: TTable;
|
|
104
113
|
}
|
|
105
114
|
|
|
@@ -135,7 +144,7 @@ export function createSyncFunction<TTable extends Table>(
|
|
|
135
144
|
*/
|
|
136
145
|
export function createInsertSchemaWithDefaults<TTable extends Table>(
|
|
137
146
|
table: TTable,
|
|
138
|
-
):
|
|
147
|
+
): InsertToSelectSchema<TTable> {
|
|
139
148
|
const insertSchema = createInsertSchema(table);
|
|
140
149
|
const columns = getTableColumns(table);
|
|
141
150
|
|
|
@@ -194,7 +203,7 @@ export function createInsertSchemaWithDefaults<TTable extends Table>(
|
|
|
194
203
|
|
|
195
204
|
return result;
|
|
196
205
|
}),
|
|
197
|
-
) as
|
|
206
|
+
) as InsertToSelectSchema<TTable>;
|
|
198
207
|
}
|
|
199
208
|
|
|
200
209
|
/**
|
|
@@ -203,7 +212,7 @@ export function createInsertSchemaWithDefaults<TTable extends Table>(
|
|
|
203
212
|
*/
|
|
204
213
|
export function createInsertSchemaWithIdDefault<TTable extends Table>(
|
|
205
214
|
table: TTable,
|
|
206
|
-
):
|
|
215
|
+
): InsertToSelectSchema<TTable> {
|
|
207
216
|
const insertSchema = createInsertSchema(table);
|
|
208
217
|
const columns = getTableColumns(table);
|
|
209
218
|
const idColumn = columns.id;
|
|
@@ -220,17 +229,16 @@ export function createInsertSchemaWithIdDefault<TTable extends Table>(
|
|
|
220
229
|
|
|
221
230
|
return result;
|
|
222
231
|
}),
|
|
223
|
-
) as
|
|
232
|
+
) as InsertToSelectSchema<TTable>;
|
|
224
233
|
}
|
|
225
234
|
|
|
226
235
|
/**
|
|
227
236
|
* Standard getKey function for collections
|
|
228
237
|
*/
|
|
229
238
|
export function createGetKeyFunction<TTable extends Table>() {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
};
|
|
239
|
+
type TItem = InferSchemaOutput<SelectSchema<TTable>>;
|
|
240
|
+
type TKey = IdOf<TTable>;
|
|
241
|
+
return (item: TItem): TKey => (item as { id: TKey }).id;
|
|
234
242
|
}
|
|
235
243
|
|
|
236
244
|
/**
|
|
@@ -242,33 +250,30 @@ export function createCollectionConfig<
|
|
|
242
250
|
TSchema extends v.GenericSchema<unknown>,
|
|
243
251
|
>(config: {
|
|
244
252
|
schema: TSchema;
|
|
245
|
-
getKey: (item: InferSchemaOutput<SelectSchema<TTable>>) =>
|
|
253
|
+
getKey: (item: InferSchemaOutput<SelectSchema<TTable>>) => IdOf<TTable>;
|
|
246
254
|
syncResult: SyncFunctionResult<TTable>;
|
|
247
255
|
onInsert?: CollectionConfig<
|
|
248
256
|
InferSchemaOutput<SelectSchema<TTable>>,
|
|
249
257
|
string,
|
|
250
|
-
|
|
251
|
-
any
|
|
258
|
+
TSchema
|
|
252
259
|
>["onInsert"];
|
|
253
260
|
onUpdate?: CollectionConfig<
|
|
254
261
|
InferSchemaOutput<SelectSchema<TTable>>,
|
|
255
262
|
string,
|
|
256
|
-
|
|
257
|
-
any
|
|
263
|
+
TSchema
|
|
258
264
|
>["onUpdate"];
|
|
259
265
|
onDelete?: CollectionConfig<
|
|
260
266
|
InferSchemaOutput<SelectSchema<TTable>>,
|
|
261
267
|
string,
|
|
262
|
-
|
|
263
|
-
any
|
|
268
|
+
TSchema
|
|
264
269
|
>["onDelete"];
|
|
265
270
|
syncMode?: SyncMode;
|
|
266
271
|
}): Omit<
|
|
267
272
|
CollectionConfig<
|
|
268
273
|
InferSchemaOutput<SelectSchema<TTable>>,
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
274
|
+
IdOf<TTable>,
|
|
275
|
+
TSchema,
|
|
276
|
+
CollectionUtils<InferSchemaOutput<SelectSchema<TTable>>>
|
|
272
277
|
>,
|
|
273
278
|
"utils"
|
|
274
279
|
> & {
|
|
@@ -277,17 +282,17 @@ export function createCollectionConfig<
|
|
|
277
282
|
} {
|
|
278
283
|
type TItem = InferSchemaOutput<SelectSchema<TTable>>;
|
|
279
284
|
type ReturnType = Omit<
|
|
280
|
-
CollectionConfig<
|
|
281
|
-
TItem,
|
|
282
|
-
string,
|
|
283
|
-
// biome-ignore lint/suspicious/noExplicitAny: Schema type parameter needs to be flexible
|
|
284
|
-
any
|
|
285
|
-
>,
|
|
285
|
+
CollectionConfig<TItem, IdOf<TTable>, TSchema, CollectionUtils<TItem>>,
|
|
286
286
|
"utils"
|
|
287
287
|
> & {
|
|
288
288
|
schema: TSchema;
|
|
289
289
|
utils: CollectionUtils<TItem>;
|
|
290
290
|
};
|
|
291
291
|
|
|
292
|
-
|
|
292
|
+
const { getKey: getId, ...rest } = config;
|
|
293
|
+
return createGenericCollectionConfig<TItem, TSchema>({
|
|
294
|
+
...rest,
|
|
295
|
+
// Generic sync is typed with string keys; runtime id may be number — same value as Drizzle row id.
|
|
296
|
+
getKey: (item: TItem) => getId(item) as string,
|
|
297
|
+
}) as ReturnType;
|
|
293
298
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { CollectionUtils } from "@firtoz/db-helpers";
|
|
2
|
+
import type {
|
|
3
|
+
Collection,
|
|
4
|
+
InferSchemaInput,
|
|
5
|
+
InferSchemaOutput,
|
|
6
|
+
} from "@tanstack/db";
|
|
7
|
+
import type {
|
|
8
|
+
IdOf,
|
|
9
|
+
InsertToSelectSchema,
|
|
10
|
+
SelectSchema,
|
|
11
|
+
} from "./collection-utils";
|
|
12
|
+
import type { TableWithRequiredFields } from "./syncableTable";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* TanStack {@link Collection} for a syncable Drizzle SQLite table (`syncableTable` columns).
|
|
16
|
+
*
|
|
17
|
+
* Shared by `@firtoz/drizzle-sqlite-wasm` (async driver) and `@firtoz/drizzle-durable-sqlite`
|
|
18
|
+
* (Durable Object sync driver). Import this type from `@firtoz/drizzle-utils` rather than
|
|
19
|
+
* duplicating it per package.
|
|
20
|
+
*/
|
|
21
|
+
export type DrizzleSqliteTableCollection<
|
|
22
|
+
TTable extends TableWithRequiredFields,
|
|
23
|
+
> = Collection<
|
|
24
|
+
InferSchemaOutput<SelectSchema<TTable>>,
|
|
25
|
+
IdOf<TTable>,
|
|
26
|
+
CollectionUtils<InferSchemaOutput<SelectSchema<TTable>>>,
|
|
27
|
+
InsertToSelectSchema<TTable>,
|
|
28
|
+
InferSchemaInput<InsertToSelectSchema<TTable>>
|
|
29
|
+
>;
|
package/src/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ export type {
|
|
|
22
22
|
IdOf,
|
|
23
23
|
SelectSchema,
|
|
24
24
|
InsertSchema,
|
|
25
|
+
InsertToSelectSchema,
|
|
25
26
|
GetTableFromSchema,
|
|
26
27
|
InferCollectionFromTable,
|
|
27
28
|
BaseSyncConfig,
|
|
@@ -47,3 +48,17 @@ export {
|
|
|
47
48
|
} from "./syncableTable";
|
|
48
49
|
|
|
49
50
|
export type { TableWithRequiredFields } from "./syncableTable";
|
|
51
|
+
|
|
52
|
+
export type { DrizzleSqliteTableCollection } from "./drizzle-sqlite-table-collection";
|
|
53
|
+
|
|
54
|
+
export type {
|
|
55
|
+
SQLOperation,
|
|
56
|
+
SQLInterceptor,
|
|
57
|
+
SqliteDriverMode,
|
|
58
|
+
SqliteTableSyncBackendConfig,
|
|
59
|
+
} from "./sqlite-table-sync";
|
|
60
|
+
export {
|
|
61
|
+
convertBasicExpressionToDrizzle,
|
|
62
|
+
convertOrderByToDrizzle,
|
|
63
|
+
createSqliteTableSyncBackend,
|
|
64
|
+
} from "./sqlite-table-sync";
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { IR } from "@tanstack/db";
|
|
2
|
+
import {
|
|
3
|
+
eq,
|
|
4
|
+
sql,
|
|
5
|
+
type Table,
|
|
6
|
+
gt,
|
|
7
|
+
gte,
|
|
8
|
+
lt,
|
|
9
|
+
lte,
|
|
10
|
+
ne,
|
|
11
|
+
and,
|
|
12
|
+
or,
|
|
13
|
+
not,
|
|
14
|
+
isNull,
|
|
15
|
+
isNotNull,
|
|
16
|
+
like,
|
|
17
|
+
inArray,
|
|
18
|
+
asc,
|
|
19
|
+
desc,
|
|
20
|
+
type SQL,
|
|
21
|
+
} from "drizzle-orm";
|
|
22
|
+
import { SQLiteColumn } from "drizzle-orm/sqlite-core";
|
|
23
|
+
import { exhaustiveGuard } from "@firtoz/maybe-error";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Converts TanStack DB IR BasicExpression to Drizzle SQL expression.
|
|
27
|
+
*/
|
|
28
|
+
export function convertBasicExpressionToDrizzle<TTable extends Table>(
|
|
29
|
+
expression: IR.BasicExpression,
|
|
30
|
+
table: TTable,
|
|
31
|
+
): SQL {
|
|
32
|
+
switch (expression.type) {
|
|
33
|
+
case "ref": {
|
|
34
|
+
const propRef = expression;
|
|
35
|
+
const columnName = propRef.path[propRef.path.length - 1];
|
|
36
|
+
const column = table[columnName as keyof typeof table];
|
|
37
|
+
|
|
38
|
+
if (!column || !(column instanceof SQLiteColumn)) {
|
|
39
|
+
console.error("[SQLite sync backend] Column lookup failed:", {
|
|
40
|
+
columnName,
|
|
41
|
+
column,
|
|
42
|
+
tableKeys: Object.keys(table),
|
|
43
|
+
hasColumn: columnName in table,
|
|
44
|
+
});
|
|
45
|
+
throw new Error(`Column ${String(columnName)} not found in table`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return column as unknown as SQL;
|
|
49
|
+
}
|
|
50
|
+
case "val": {
|
|
51
|
+
const value = expression;
|
|
52
|
+
return sql`${value.value}`;
|
|
53
|
+
}
|
|
54
|
+
case "func": {
|
|
55
|
+
const func = expression;
|
|
56
|
+
const args = func.args.map((arg) =>
|
|
57
|
+
convertBasicExpressionToDrizzle(arg, table),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
switch (func.name) {
|
|
61
|
+
case "eq":
|
|
62
|
+
return eq(args[0], args[1]);
|
|
63
|
+
case "ne":
|
|
64
|
+
return ne(args[0], args[1]);
|
|
65
|
+
case "gt":
|
|
66
|
+
return gt(args[0], args[1]);
|
|
67
|
+
case "gte":
|
|
68
|
+
return gte(args[0], args[1]);
|
|
69
|
+
case "lt":
|
|
70
|
+
return lt(args[0], args[1]);
|
|
71
|
+
case "lte":
|
|
72
|
+
return lte(args[0], args[1]);
|
|
73
|
+
case "and": {
|
|
74
|
+
const result = and(...args);
|
|
75
|
+
if (!result) {
|
|
76
|
+
throw new Error("Invalid 'and' expression - no arguments provided");
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
case "or": {
|
|
81
|
+
const result = or(...args);
|
|
82
|
+
if (!result) {
|
|
83
|
+
throw new Error("Invalid 'or' expression - no arguments provided");
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
case "not":
|
|
88
|
+
return not(args[0]);
|
|
89
|
+
case "isNull":
|
|
90
|
+
return isNull(args[0]);
|
|
91
|
+
case "isNotNull":
|
|
92
|
+
return isNotNull(args[0]);
|
|
93
|
+
case "like":
|
|
94
|
+
return like(args[0], args[1]);
|
|
95
|
+
case "in":
|
|
96
|
+
return inArray(args[0], args[1]);
|
|
97
|
+
case "isUndefined":
|
|
98
|
+
return isNull(args[0]);
|
|
99
|
+
default:
|
|
100
|
+
throw new Error(`Unsupported function: ${func.name}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
default:
|
|
104
|
+
exhaustiveGuard(expression);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function convertOrderByToDrizzle<TTable extends Table>(
|
|
109
|
+
orderBy: IR.OrderBy,
|
|
110
|
+
table: TTable,
|
|
111
|
+
): SQL[] {
|
|
112
|
+
return orderBy.map((clause) => {
|
|
113
|
+
const expression = convertBasicExpressionToDrizzle(
|
|
114
|
+
clause.expression,
|
|
115
|
+
table,
|
|
116
|
+
);
|
|
117
|
+
const direction = clause.compareOptions.direction || "asc";
|
|
118
|
+
|
|
119
|
+
return direction === "asc" ? asc(expression) : desc(expression);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type { SQLOperation, SQLInterceptor } from "./types";
|
|
2
|
+
export {
|
|
3
|
+
convertBasicExpressionToDrizzle,
|
|
4
|
+
convertOrderByToDrizzle,
|
|
5
|
+
} from "./convert-ir";
|
|
6
|
+
export {
|
|
7
|
+
createSqliteTableSyncBackend,
|
|
8
|
+
type SqliteDriverMode,
|
|
9
|
+
type SqliteTableSyncBackendConfig,
|
|
10
|
+
} from "./sqlite-table-sync-backend";
|
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
import type { ReceiveSyncDurableOp } from "@firtoz/db-helpers";
|
|
2
|
+
import { exhaustiveGuard } from "@firtoz/maybe-error";
|
|
3
|
+
import type { InferSchemaOutput } from "@tanstack/db";
|
|
4
|
+
import { and, eq, getTableColumns, sql, type SQL } from "drizzle-orm";
|
|
5
|
+
import type {
|
|
6
|
+
SQLiteInsertValue,
|
|
7
|
+
SQLiteUpdateSetSource,
|
|
8
|
+
} from "drizzle-orm/sqlite-core";
|
|
9
|
+
import type { SelectSchema, SyncBackend } from "../collection-utils";
|
|
10
|
+
import type { TableWithRequiredFields } from "../syncableTable";
|
|
11
|
+
import {
|
|
12
|
+
convertBasicExpressionToDrizzle,
|
|
13
|
+
convertOrderByToDrizzle,
|
|
14
|
+
} from "./convert-ir";
|
|
15
|
+
import type { SQLInterceptor } from "./types";
|
|
16
|
+
|
|
17
|
+
export type SqliteDriverMode = "async" | "sync";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* `ON CONFLICT DO UPDATE` set map using SQLite `excluded.*` so inserts are idempotent
|
|
21
|
+
* (matches IndexedDB `put` / replayed partial-sync rows already present from `initialLoad`).
|
|
22
|
+
*/
|
|
23
|
+
function sqliteExcludedUpsertSet<TTable extends TableWithRequiredFields>(
|
|
24
|
+
table: TTable,
|
|
25
|
+
): SQLiteUpdateSetSource<TTable> {
|
|
26
|
+
const cols = getTableColumns(table);
|
|
27
|
+
const set: Record<string, SQL> = {};
|
|
28
|
+
for (const [jsName, col] of Object.entries(cols)) {
|
|
29
|
+
if (jsName === "id") continue;
|
|
30
|
+
set[jsName] = sql.raw(`excluded."${col.name}"`);
|
|
31
|
+
}
|
|
32
|
+
return set as SQLiteUpdateSetSource<TTable>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface SqliteTableSyncBackendConfig<
|
|
36
|
+
TTable extends TableWithRequiredFields,
|
|
37
|
+
> {
|
|
38
|
+
/** drizzle-orm SQLite database (async WASM/libsql or sync Durable Object) */
|
|
39
|
+
// biome-ignore lint/suspicious/noExplicitAny: generic over sync/async Drizzle DB shapes
|
|
40
|
+
drizzle: any;
|
|
41
|
+
table: TTable;
|
|
42
|
+
tableName: string;
|
|
43
|
+
debug?: boolean;
|
|
44
|
+
checkpoint?: () => Promise<void>;
|
|
45
|
+
interceptor?: SQLInterceptor;
|
|
46
|
+
/**
|
|
47
|
+
* `async`: libsql/WASM — use `await db.transaction(async (tx) => …)`.
|
|
48
|
+
* `sync`: Cloudflare DO SQLite — `transactionSync` requires a **synchronous** callback; use `.all()` / `.run()` on builders inside `tx`.
|
|
49
|
+
*/
|
|
50
|
+
driverMode: SqliteDriverMode;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createSqliteTableSyncBackend<
|
|
54
|
+
TTable extends TableWithRequiredFields,
|
|
55
|
+
>(config: SqliteTableSyncBackendConfig<TTable>): SyncBackend<TTable> {
|
|
56
|
+
type TItem = InferSchemaOutput<SelectSchema<TTable>>;
|
|
57
|
+
const table = config.table;
|
|
58
|
+
const driverMode = config.driverMode;
|
|
59
|
+
|
|
60
|
+
let transactionQueue = Promise.resolve();
|
|
61
|
+
const queueTransaction = <T>(
|
|
62
|
+
_label: string,
|
|
63
|
+
fn: () => Promise<T>,
|
|
64
|
+
): Promise<T> => {
|
|
65
|
+
const run = (): Promise<T> => fn();
|
|
66
|
+
const result = transactionQueue.then(run, run);
|
|
67
|
+
transactionQueue = result.then(
|
|
68
|
+
() => {},
|
|
69
|
+
() => {},
|
|
70
|
+
);
|
|
71
|
+
return result;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const backend: SyncBackend<TTable> = {
|
|
75
|
+
initialLoad: async () => {
|
|
76
|
+
const items = (await config.drizzle
|
|
77
|
+
.select()
|
|
78
|
+
.from(table)) as unknown as InferSchemaOutput<SelectSchema<TTable>>[];
|
|
79
|
+
|
|
80
|
+
if (config.interceptor?.onOperation) {
|
|
81
|
+
config.interceptor.onOperation({
|
|
82
|
+
type: "select-all",
|
|
83
|
+
tableName: config.tableName,
|
|
84
|
+
itemsReturned: items,
|
|
85
|
+
itemCount: items.length,
|
|
86
|
+
context: "Initial load (eager mode)",
|
|
87
|
+
timestamp: Date.now(),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (config.interceptor?.onOperation) {
|
|
91
|
+
config.interceptor.onOperation({
|
|
92
|
+
type: "write",
|
|
93
|
+
tableName: config.tableName,
|
|
94
|
+
itemsWritten: items,
|
|
95
|
+
writeCount: items.length,
|
|
96
|
+
context: "Initial load (eager mode)",
|
|
97
|
+
timestamp: Date.now(),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return items as unknown as InferSchemaOutput<SelectSchema<TTable>>[];
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
loadSubset: async (options) => {
|
|
105
|
+
let query = config.drizzle.select().from(table).$dynamic();
|
|
106
|
+
|
|
107
|
+
let hasWhere = false;
|
|
108
|
+
if (options.where || options.cursor?.whereFrom) {
|
|
109
|
+
let drizzleWhere: SQL | undefined;
|
|
110
|
+
|
|
111
|
+
if (options.where && options.cursor?.whereFrom) {
|
|
112
|
+
const mainWhere = convertBasicExpressionToDrizzle(
|
|
113
|
+
options.where,
|
|
114
|
+
table,
|
|
115
|
+
);
|
|
116
|
+
const cursorWhere = convertBasicExpressionToDrizzle(
|
|
117
|
+
options.cursor.whereFrom,
|
|
118
|
+
table,
|
|
119
|
+
);
|
|
120
|
+
drizzleWhere = and(mainWhere, cursorWhere);
|
|
121
|
+
} else if (options.where) {
|
|
122
|
+
drizzleWhere = convertBasicExpressionToDrizzle(options.where, table);
|
|
123
|
+
} else if (options.cursor?.whereFrom) {
|
|
124
|
+
drizzleWhere = convertBasicExpressionToDrizzle(
|
|
125
|
+
options.cursor.whereFrom,
|
|
126
|
+
table,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (drizzleWhere) {
|
|
131
|
+
query = query.where(drizzleWhere);
|
|
132
|
+
hasWhere = true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (options.orderBy) {
|
|
137
|
+
const drizzleOrderBy = convertOrderByToDrizzle(options.orderBy, table);
|
|
138
|
+
query = query.orderBy(...drizzleOrderBy);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (options.limit !== undefined) {
|
|
142
|
+
query = query.limit(options.limit);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (options.offset !== undefined && options.offset > 0) {
|
|
146
|
+
query = query.offset(options.offset);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const items = (await query) as unknown as InferSchemaOutput<
|
|
150
|
+
SelectSchema<TTable>
|
|
151
|
+
>[];
|
|
152
|
+
|
|
153
|
+
if (config.interceptor?.onOperation) {
|
|
154
|
+
const contextParts: string[] = ["On-demand load"];
|
|
155
|
+
if (options.orderBy) contextParts.push("with sorting");
|
|
156
|
+
if (options.limit !== undefined)
|
|
157
|
+
contextParts.push(`limit ${options.limit}`);
|
|
158
|
+
if (options.offset !== undefined && options.offset > 0)
|
|
159
|
+
contextParts.push(`offset ${options.offset}`);
|
|
160
|
+
if (options.cursor) contextParts.push("with cursor pagination");
|
|
161
|
+
|
|
162
|
+
if (hasWhere) {
|
|
163
|
+
config.interceptor.onOperation({
|
|
164
|
+
type: "select-where",
|
|
165
|
+
tableName: config.tableName,
|
|
166
|
+
whereClause: "WHERE clause applied",
|
|
167
|
+
itemsReturned: items,
|
|
168
|
+
itemCount: items.length,
|
|
169
|
+
context: contextParts.join(", "),
|
|
170
|
+
timestamp: Date.now(),
|
|
171
|
+
});
|
|
172
|
+
} else {
|
|
173
|
+
config.interceptor.onOperation({
|
|
174
|
+
type: "select-all",
|
|
175
|
+
tableName: config.tableName,
|
|
176
|
+
itemsReturned: items,
|
|
177
|
+
itemCount: items.length,
|
|
178
|
+
context: contextParts.join(", "),
|
|
179
|
+
timestamp: Date.now(),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (config.interceptor?.onOperation) {
|
|
185
|
+
const contextParts: string[] = ["On-demand load"];
|
|
186
|
+
if (hasWhere) contextParts.push("with WHERE clause");
|
|
187
|
+
if (options.orderBy) contextParts.push("with sorting");
|
|
188
|
+
if (options.limit !== undefined)
|
|
189
|
+
contextParts.push(`limit ${options.limit}`);
|
|
190
|
+
if (options.offset !== undefined && options.offset > 0)
|
|
191
|
+
contextParts.push(`offset ${options.offset}`);
|
|
192
|
+
|
|
193
|
+
config.interceptor.onOperation({
|
|
194
|
+
type: "write",
|
|
195
|
+
tableName: config.tableName,
|
|
196
|
+
itemsWritten: items,
|
|
197
|
+
writeCount: items.length,
|
|
198
|
+
context: contextParts.join(", "),
|
|
199
|
+
timestamp: Date.now(),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return items as unknown as InferSchemaOutput<SelectSchema<TTable>>[];
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
handleInsert: async (items) => {
|
|
207
|
+
const results: Array<InferSchemaOutput<SelectSchema<TTable>>> = [];
|
|
208
|
+
|
|
209
|
+
await queueTransaction("handleInsert", async () => {
|
|
210
|
+
if (driverMode === "sync") {
|
|
211
|
+
config.drizzle.transaction((tx: typeof config.drizzle) => {
|
|
212
|
+
for (const itemToInsert of items) {
|
|
213
|
+
if (config.debug) {
|
|
214
|
+
console.log(
|
|
215
|
+
`[${new Date().toISOString()}] insertListener inserting`,
|
|
216
|
+
itemToInsert,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
const result = tx
|
|
220
|
+
.insert(table)
|
|
221
|
+
.values(
|
|
222
|
+
itemToInsert as unknown as SQLiteInsertValue<typeof table>,
|
|
223
|
+
)
|
|
224
|
+
.onConflictDoUpdate({
|
|
225
|
+
target: table.id,
|
|
226
|
+
set: sqliteExcludedUpsertSet(table),
|
|
227
|
+
})
|
|
228
|
+
.returning()
|
|
229
|
+
.all() as Array<InferSchemaOutput<SelectSchema<TTable>>>;
|
|
230
|
+
if (config.debug) {
|
|
231
|
+
console.log(
|
|
232
|
+
`[${new Date().toISOString()}] insertListener result`,
|
|
233
|
+
result,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
if (result.length > 0) {
|
|
237
|
+
results.push(result[0]);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
} else {
|
|
242
|
+
await config.drizzle.transaction(
|
|
243
|
+
async (tx: typeof config.drizzle) => {
|
|
244
|
+
for (const itemToInsert of items) {
|
|
245
|
+
if (config.debug) {
|
|
246
|
+
console.log(
|
|
247
|
+
`[${new Date().toISOString()}] insertListener inserting`,
|
|
248
|
+
itemToInsert,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
const result = (await tx
|
|
252
|
+
.insert(table)
|
|
253
|
+
.values(
|
|
254
|
+
itemToInsert as unknown as SQLiteInsertValue<typeof table>,
|
|
255
|
+
)
|
|
256
|
+
.onConflictDoUpdate({
|
|
257
|
+
target: table.id,
|
|
258
|
+
set: sqliteExcludedUpsertSet(table),
|
|
259
|
+
})
|
|
260
|
+
.returning()) as Array<
|
|
261
|
+
InferSchemaOutput<SelectSchema<TTable>>
|
|
262
|
+
>;
|
|
263
|
+
if (config.debug) {
|
|
264
|
+
console.log(
|
|
265
|
+
`[${new Date().toISOString()}] insertListener result`,
|
|
266
|
+
result,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
if (result.length > 0) {
|
|
270
|
+
results.push(result[0]);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (config.checkpoint) {
|
|
278
|
+
await config.checkpoint();
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
return results;
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
handleUpdate: async (mutations) => {
|
|
286
|
+
const results: Array<InferSchemaOutput<SelectSchema<TTable>>> = [];
|
|
287
|
+
|
|
288
|
+
await queueTransaction("handleUpdate", async () => {
|
|
289
|
+
if (driverMode === "sync") {
|
|
290
|
+
config.drizzle.transaction((tx: typeof config.drizzle) => {
|
|
291
|
+
for (const mutation of mutations) {
|
|
292
|
+
if (config.debug) {
|
|
293
|
+
console.log(
|
|
294
|
+
`[${new Date().toISOString()}] updateListener updating`,
|
|
295
|
+
mutation,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
const updateTime = new Date();
|
|
299
|
+
const result = tx
|
|
300
|
+
.update(table)
|
|
301
|
+
.set({
|
|
302
|
+
...mutation.changes,
|
|
303
|
+
updatedAt: updateTime,
|
|
304
|
+
} as SQLiteUpdateSetSource<typeof table>)
|
|
305
|
+
// biome-ignore lint/suspicious/noExplicitAny: branded id key
|
|
306
|
+
.where(eq(table.id, mutation.key as any))
|
|
307
|
+
.returning()
|
|
308
|
+
.all() as Array<InferSchemaOutput<SelectSchema<TTable>>>;
|
|
309
|
+
if (config.debug) {
|
|
310
|
+
console.log(
|
|
311
|
+
`[${new Date().toISOString()}] updateListener result`,
|
|
312
|
+
result,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
results.push(...result);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
} else {
|
|
319
|
+
await config.drizzle.transaction(
|
|
320
|
+
async (tx: typeof config.drizzle) => {
|
|
321
|
+
for (const mutation of mutations) {
|
|
322
|
+
if (config.debug) {
|
|
323
|
+
console.log(
|
|
324
|
+
`[${new Date().toISOString()}] updateListener updating`,
|
|
325
|
+
mutation,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
const updateTime = new Date();
|
|
329
|
+
const result = (await tx
|
|
330
|
+
.update(table)
|
|
331
|
+
.set({
|
|
332
|
+
...mutation.changes,
|
|
333
|
+
updatedAt: updateTime,
|
|
334
|
+
} as SQLiteUpdateSetSource<typeof table>)
|
|
335
|
+
// biome-ignore lint/suspicious/noExplicitAny: branded id key
|
|
336
|
+
.where(eq(table.id, mutation.key as any))
|
|
337
|
+
.returning()) as Array<
|
|
338
|
+
InferSchemaOutput<SelectSchema<TTable>>
|
|
339
|
+
>;
|
|
340
|
+
if (config.debug) {
|
|
341
|
+
console.log(
|
|
342
|
+
`[${new Date().toISOString()}] updateListener result`,
|
|
343
|
+
result,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
results.push(...result);
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (config.checkpoint) {
|
|
353
|
+
await config.checkpoint();
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
return results;
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
handleDelete: async (mutations) => {
|
|
361
|
+
await queueTransaction("handleDelete", async () => {
|
|
362
|
+
if (driverMode === "sync") {
|
|
363
|
+
config.drizzle.transaction((tx: typeof config.drizzle) => {
|
|
364
|
+
for (const mutation of mutations) {
|
|
365
|
+
tx.delete(table)
|
|
366
|
+
// biome-ignore lint/suspicious/noExplicitAny: branded id key
|
|
367
|
+
.where(eq(table.id, mutation.key as any))
|
|
368
|
+
.run();
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
} else {
|
|
372
|
+
await config.drizzle.transaction(
|
|
373
|
+
async (tx: typeof config.drizzle) => {
|
|
374
|
+
for (const mutation of mutations) {
|
|
375
|
+
await tx
|
|
376
|
+
.delete(table)
|
|
377
|
+
// biome-ignore lint/suspicious/noExplicitAny: branded id key
|
|
378
|
+
.where(eq(table.id, mutation.key as any));
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (config.checkpoint) {
|
|
385
|
+
await config.checkpoint();
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
},
|
|
389
|
+
handleTruncate: async () => {
|
|
390
|
+
await queueTransaction("handleTruncate", async () => {
|
|
391
|
+
if (driverMode === "sync") {
|
|
392
|
+
config.drizzle.transaction((tx: typeof config.drizzle) => {
|
|
393
|
+
tx.delete(table).run();
|
|
394
|
+
});
|
|
395
|
+
} else {
|
|
396
|
+
await config.drizzle.transaction(
|
|
397
|
+
async (tx: typeof config.drizzle) => {
|
|
398
|
+
await tx.delete(table);
|
|
399
|
+
},
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (config.checkpoint) {
|
|
404
|
+
await config.checkpoint();
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
},
|
|
408
|
+
|
|
409
|
+
applyReceiveSyncDurableWrites: async (
|
|
410
|
+
ops: ReceiveSyncDurableOp<TItem>[],
|
|
411
|
+
) => {
|
|
412
|
+
if (ops.length === 0) return;
|
|
413
|
+
|
|
414
|
+
await queueTransaction("applyReceiveSyncDurableWrites", async () => {
|
|
415
|
+
if (driverMode === "sync") {
|
|
416
|
+
config.drizzle.transaction((tx: typeof config.drizzle) => {
|
|
417
|
+
for (const op of ops) {
|
|
418
|
+
switch (op.type) {
|
|
419
|
+
case "insert": {
|
|
420
|
+
if (config.debug) {
|
|
421
|
+
console.log(
|
|
422
|
+
`[${new Date().toISOString()}] receiveSync batch insert`,
|
|
423
|
+
op.value,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
tx.insert(table)
|
|
427
|
+
.values(
|
|
428
|
+
op.value as unknown as SQLiteInsertValue<typeof table>,
|
|
429
|
+
)
|
|
430
|
+
.onConflictDoUpdate({
|
|
431
|
+
target: table.id,
|
|
432
|
+
set: sqliteExcludedUpsertSet(table),
|
|
433
|
+
})
|
|
434
|
+
.run();
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
case "update": {
|
|
438
|
+
if (config.debug) {
|
|
439
|
+
console.log(
|
|
440
|
+
`[${new Date().toISOString()}] receiveSync batch update`,
|
|
441
|
+
op,
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
const updateTime = new Date();
|
|
445
|
+
tx.update(table)
|
|
446
|
+
.set({
|
|
447
|
+
...op.changes,
|
|
448
|
+
updatedAt: updateTime,
|
|
449
|
+
} as SQLiteUpdateSetSource<typeof table>)
|
|
450
|
+
// biome-ignore lint/suspicious/noExplicitAny: branded id key
|
|
451
|
+
.where(eq(table.id, op.key as any))
|
|
452
|
+
.run();
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
case "delete":
|
|
456
|
+
tx.delete(table)
|
|
457
|
+
// biome-ignore lint/suspicious/noExplicitAny: branded id key
|
|
458
|
+
.where(eq(table.id, op.key as any))
|
|
459
|
+
.run();
|
|
460
|
+
break;
|
|
461
|
+
case "truncate":
|
|
462
|
+
tx.delete(table).run();
|
|
463
|
+
break;
|
|
464
|
+
default:
|
|
465
|
+
exhaustiveGuard(op);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
} else {
|
|
470
|
+
await config.drizzle.transaction(
|
|
471
|
+
async (tx: typeof config.drizzle) => {
|
|
472
|
+
for (const op of ops) {
|
|
473
|
+
switch (op.type) {
|
|
474
|
+
case "insert": {
|
|
475
|
+
if (config.debug) {
|
|
476
|
+
console.log(
|
|
477
|
+
`[${new Date().toISOString()}] receiveSync batch insert`,
|
|
478
|
+
op.value,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
await tx
|
|
482
|
+
.insert(table)
|
|
483
|
+
.values(
|
|
484
|
+
op.value as unknown as SQLiteInsertValue<typeof table>,
|
|
485
|
+
)
|
|
486
|
+
.onConflictDoUpdate({
|
|
487
|
+
target: table.id,
|
|
488
|
+
set: sqliteExcludedUpsertSet(table),
|
|
489
|
+
});
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
case "update": {
|
|
493
|
+
if (config.debug) {
|
|
494
|
+
console.log(
|
|
495
|
+
`[${new Date().toISOString()}] receiveSync batch update`,
|
|
496
|
+
op,
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
const updateTime = new Date();
|
|
500
|
+
await tx
|
|
501
|
+
.update(table)
|
|
502
|
+
.set({
|
|
503
|
+
...op.changes,
|
|
504
|
+
updatedAt: updateTime,
|
|
505
|
+
} as SQLiteUpdateSetSource<typeof table>)
|
|
506
|
+
// biome-ignore lint/suspicious/noExplicitAny: branded id key
|
|
507
|
+
.where(eq(table.id, op.key as any));
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
case "delete":
|
|
511
|
+
await tx
|
|
512
|
+
.delete(table)
|
|
513
|
+
// biome-ignore lint/suspicious/noExplicitAny: branded id key
|
|
514
|
+
.where(eq(table.id, op.key as any));
|
|
515
|
+
break;
|
|
516
|
+
case "truncate":
|
|
517
|
+
await tx.delete(table);
|
|
518
|
+
break;
|
|
519
|
+
default:
|
|
520
|
+
exhaustiveGuard(op);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (config.checkpoint) {
|
|
528
|
+
await config.checkpoint();
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
},
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
return backend;
|
|
535
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operation tracking for SQLite-backed TanStack sync backends.
|
|
3
|
+
* Uses discriminated unions so TypeScript can narrow on `type`.
|
|
4
|
+
*/
|
|
5
|
+
export type SQLOperation =
|
|
6
|
+
| {
|
|
7
|
+
type: "select-all";
|
|
8
|
+
tableName: string;
|
|
9
|
+
itemsReturned: unknown[];
|
|
10
|
+
itemCount: number;
|
|
11
|
+
context: string;
|
|
12
|
+
sql?: string;
|
|
13
|
+
timestamp: number;
|
|
14
|
+
}
|
|
15
|
+
| {
|
|
16
|
+
type: "select-where";
|
|
17
|
+
tableName: string;
|
|
18
|
+
whereClause: string;
|
|
19
|
+
itemsReturned: unknown[];
|
|
20
|
+
itemCount: number;
|
|
21
|
+
context: string;
|
|
22
|
+
sql?: string;
|
|
23
|
+
timestamp: number;
|
|
24
|
+
}
|
|
25
|
+
| {
|
|
26
|
+
type: "write";
|
|
27
|
+
tableName: string;
|
|
28
|
+
itemsWritten: unknown[];
|
|
29
|
+
writeCount: number;
|
|
30
|
+
context: string;
|
|
31
|
+
timestamp: number;
|
|
32
|
+
}
|
|
33
|
+
| {
|
|
34
|
+
type: "insert";
|
|
35
|
+
tableName: string;
|
|
36
|
+
item: unknown;
|
|
37
|
+
sql?: string;
|
|
38
|
+
timestamp: number;
|
|
39
|
+
}
|
|
40
|
+
| {
|
|
41
|
+
type: "update";
|
|
42
|
+
tableName: string;
|
|
43
|
+
updates: unknown;
|
|
44
|
+
sql?: string;
|
|
45
|
+
timestamp: number;
|
|
46
|
+
}
|
|
47
|
+
| {
|
|
48
|
+
type: "delete";
|
|
49
|
+
tableName: string;
|
|
50
|
+
sql?: string;
|
|
51
|
+
timestamp: number;
|
|
52
|
+
}
|
|
53
|
+
| {
|
|
54
|
+
type: "raw-query";
|
|
55
|
+
sql: string;
|
|
56
|
+
params?: unknown[];
|
|
57
|
+
method: string;
|
|
58
|
+
rowCount: number;
|
|
59
|
+
context: string;
|
|
60
|
+
timestamp: number;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export interface SQLInterceptor {
|
|
64
|
+
onOperation?: (operation: SQLOperation) => void;
|
|
65
|
+
}
|
package/src/syncableTable.ts
CHANGED
|
@@ -75,14 +75,12 @@ export const syncableTable = <
|
|
|
75
75
|
for (const columnName in tableColumns) {
|
|
76
76
|
const column = tableColumns[columnName];
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
// Avoid executing defaultFn at module initialization time.
|
|
79
|
+
// In Cloudflare Workers this can trigger disallowed global-scope APIs.
|
|
79
80
|
if (column.defaultFn) {
|
|
80
|
-
|
|
81
|
-
} else if (column.default !== undefined) {
|
|
82
|
-
defaultValue = column.default;
|
|
81
|
+
continue;
|
|
83
82
|
}
|
|
84
|
-
|
|
85
|
-
if (defaultValue instanceof SQL) {
|
|
83
|
+
if (column.default instanceof SQL) {
|
|
86
84
|
throw new Error(
|
|
87
85
|
`Default value for column ${tableName}.${columnName} is a SQL expression, which is not supported for IndexedDB.\n\nYou can use a default value or a default function instead.`,
|
|
88
86
|
);
|