@firtoz/drizzle-utils 1.0.1 → 1.1.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 +12 -0
- package/package.json +3 -3
- package/src/collection-utils.ts +19 -26
- package/src/index.ts +13 -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 +361 -0
- package/src/sqlite-table-sync/types.ts +65 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @firtoz/drizzle-utils
|
|
2
2
|
|
|
3
|
+
## 1.1.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
|
+
## 1.0.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [`bca3758`](https://github.com/firtoz/fullstack-toolkit/commit/bca3758ab5ad2661b950360dc35edda2680c3b4e) Thanks [@firtoz](https://github.com/firtoz)! - Bump minimum `valibot` peer dependency from `>=1.0.0` to `>=1.3.1`.
|
|
14
|
+
|
|
3
15
|
## 1.0.1
|
|
4
16
|
|
|
5
17
|
### 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.1.0",
|
|
4
4
|
"description": "Shared utilities and types for Drizzle-based packages",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"module": "./src/index.ts",
|
|
@@ -56,13 +56,13 @@
|
|
|
56
56
|
"@tanstack/db": ">=0.5.33",
|
|
57
57
|
"drizzle-orm": ">=0.45.1",
|
|
58
58
|
"drizzle-valibot": ">=0.4.0",
|
|
59
|
-
"valibot": ">=1.
|
|
59
|
+
"valibot": ">=1.3.1"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@tanstack/db": "^0.5.33",
|
|
63
63
|
"drizzle-orm": "^0.45.1",
|
|
64
64
|
"drizzle-valibot": "^0.4.2",
|
|
65
|
-
"valibot": "^1.
|
|
65
|
+
"valibot": "^1.3.1"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
68
|
"@firtoz/db-helpers": "^2.0.0",
|
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"
|
|
@@ -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,7 +229,7 @@ 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
|
/**
|
|
@@ -247,44 +256,28 @@ export function createCollectionConfig<
|
|
|
247
256
|
onInsert?: CollectionConfig<
|
|
248
257
|
InferSchemaOutput<SelectSchema<TTable>>,
|
|
249
258
|
string,
|
|
250
|
-
|
|
251
|
-
any
|
|
259
|
+
TSchema
|
|
252
260
|
>["onInsert"];
|
|
253
261
|
onUpdate?: CollectionConfig<
|
|
254
262
|
InferSchemaOutput<SelectSchema<TTable>>,
|
|
255
263
|
string,
|
|
256
|
-
|
|
257
|
-
any
|
|
264
|
+
TSchema
|
|
258
265
|
>["onUpdate"];
|
|
259
266
|
onDelete?: CollectionConfig<
|
|
260
267
|
InferSchemaOutput<SelectSchema<TTable>>,
|
|
261
268
|
string,
|
|
262
|
-
|
|
263
|
-
any
|
|
269
|
+
TSchema
|
|
264
270
|
>["onDelete"];
|
|
265
271
|
syncMode?: SyncMode;
|
|
266
272
|
}): Omit<
|
|
267
|
-
CollectionConfig<
|
|
268
|
-
InferSchemaOutput<SelectSchema<TTable>>,
|
|
269
|
-
string,
|
|
270
|
-
// biome-ignore lint/suspicious/noExplicitAny: Schema type parameter needs to be flexible
|
|
271
|
-
any
|
|
272
|
-
>,
|
|
273
|
+
CollectionConfig<InferSchemaOutput<SelectSchema<TTable>>, string, TSchema>,
|
|
273
274
|
"utils"
|
|
274
275
|
> & {
|
|
275
276
|
schema: TSchema;
|
|
276
277
|
utils: CollectionUtils<InferSchemaOutput<SelectSchema<TTable>>>;
|
|
277
278
|
} {
|
|
278
279
|
type TItem = InferSchemaOutput<SelectSchema<TTable>>;
|
|
279
|
-
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
|
-
>,
|
|
286
|
-
"utils"
|
|
287
|
-
> & {
|
|
280
|
+
type ReturnType = Omit<CollectionConfig<TItem, string, TSchema>, "utils"> & {
|
|
288
281
|
schema: TSchema;
|
|
289
282
|
utils: CollectionUtils<TItem>;
|
|
290
283
|
};
|
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,15 @@ export {
|
|
|
47
48
|
} from "./syncableTable";
|
|
48
49
|
|
|
49
50
|
export type { TableWithRequiredFields } from "./syncableTable";
|
|
51
|
+
|
|
52
|
+
export type {
|
|
53
|
+
SQLOperation,
|
|
54
|
+
SQLInterceptor,
|
|
55
|
+
SqliteDriverMode,
|
|
56
|
+
SqliteTableSyncBackendConfig,
|
|
57
|
+
} from "./sqlite-table-sync";
|
|
58
|
+
export {
|
|
59
|
+
convertBasicExpressionToDrizzle,
|
|
60
|
+
convertOrderByToDrizzle,
|
|
61
|
+
createSqliteTableSyncBackend,
|
|
62
|
+
} 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,361 @@
|
|
|
1
|
+
import type { InferSchemaOutput } from "@tanstack/db";
|
|
2
|
+
import { and, eq, type SQL } from "drizzle-orm";
|
|
3
|
+
import type {
|
|
4
|
+
SQLiteInsertValue,
|
|
5
|
+
SQLiteUpdateSetSource,
|
|
6
|
+
} from "drizzle-orm/sqlite-core";
|
|
7
|
+
import type { SelectSchema, SyncBackend } from "../collection-utils";
|
|
8
|
+
import type { TableWithRequiredFields } from "../syncableTable";
|
|
9
|
+
import {
|
|
10
|
+
convertBasicExpressionToDrizzle,
|
|
11
|
+
convertOrderByToDrizzle,
|
|
12
|
+
} from "./convert-ir";
|
|
13
|
+
import type { SQLInterceptor } from "./types";
|
|
14
|
+
|
|
15
|
+
export type SqliteDriverMode = "async" | "sync";
|
|
16
|
+
|
|
17
|
+
export interface SqliteTableSyncBackendConfig<
|
|
18
|
+
TTable extends TableWithRequiredFields,
|
|
19
|
+
> {
|
|
20
|
+
/** drizzle-orm SQLite database (async WASM/libsql or sync Durable Object) */
|
|
21
|
+
// biome-ignore lint/suspicious/noExplicitAny: generic over sync/async Drizzle DB shapes
|
|
22
|
+
drizzle: any;
|
|
23
|
+
table: TTable;
|
|
24
|
+
tableName: string;
|
|
25
|
+
debug?: boolean;
|
|
26
|
+
checkpoint?: () => Promise<void>;
|
|
27
|
+
interceptor?: SQLInterceptor;
|
|
28
|
+
/**
|
|
29
|
+
* `async`: libsql/WASM — use `await db.transaction(async (tx) => …)`.
|
|
30
|
+
* `sync`: Cloudflare DO SQLite — `transactionSync` requires a **synchronous** callback; use `.all()` / `.run()` on builders inside `tx`.
|
|
31
|
+
*/
|
|
32
|
+
driverMode: SqliteDriverMode;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createSqliteTableSyncBackend<
|
|
36
|
+
TTable extends TableWithRequiredFields,
|
|
37
|
+
>(config: SqliteTableSyncBackendConfig<TTable>): SyncBackend<TTable> {
|
|
38
|
+
const table = config.table;
|
|
39
|
+
const driverMode = config.driverMode;
|
|
40
|
+
|
|
41
|
+
let transactionQueue = Promise.resolve();
|
|
42
|
+
const queueTransaction = <T>(fn: () => Promise<T>): Promise<T> => {
|
|
43
|
+
const result = transactionQueue.then(fn, fn);
|
|
44
|
+
transactionQueue = result.then(
|
|
45
|
+
() => {},
|
|
46
|
+
() => {},
|
|
47
|
+
);
|
|
48
|
+
return result;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const backend: SyncBackend<TTable> = {
|
|
52
|
+
initialLoad: async () => {
|
|
53
|
+
const items = (await config.drizzle
|
|
54
|
+
.select()
|
|
55
|
+
.from(table)) as unknown as InferSchemaOutput<SelectSchema<TTable>>[];
|
|
56
|
+
|
|
57
|
+
if (config.interceptor?.onOperation) {
|
|
58
|
+
config.interceptor.onOperation({
|
|
59
|
+
type: "select-all",
|
|
60
|
+
tableName: config.tableName,
|
|
61
|
+
itemsReturned: items,
|
|
62
|
+
itemCount: items.length,
|
|
63
|
+
context: "Initial load (eager mode)",
|
|
64
|
+
timestamp: Date.now(),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (config.interceptor?.onOperation) {
|
|
68
|
+
config.interceptor.onOperation({
|
|
69
|
+
type: "write",
|
|
70
|
+
tableName: config.tableName,
|
|
71
|
+
itemsWritten: items,
|
|
72
|
+
writeCount: items.length,
|
|
73
|
+
context: "Initial load (eager mode)",
|
|
74
|
+
timestamp: Date.now(),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return items as unknown as InferSchemaOutput<SelectSchema<TTable>>[];
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
loadSubset: async (options) => {
|
|
82
|
+
let query = config.drizzle.select().from(table).$dynamic();
|
|
83
|
+
|
|
84
|
+
let hasWhere = false;
|
|
85
|
+
if (options.where || options.cursor?.whereFrom) {
|
|
86
|
+
let drizzleWhere: SQL | undefined;
|
|
87
|
+
|
|
88
|
+
if (options.where && options.cursor?.whereFrom) {
|
|
89
|
+
const mainWhere = convertBasicExpressionToDrizzle(
|
|
90
|
+
options.where,
|
|
91
|
+
table,
|
|
92
|
+
);
|
|
93
|
+
const cursorWhere = convertBasicExpressionToDrizzle(
|
|
94
|
+
options.cursor.whereFrom,
|
|
95
|
+
table,
|
|
96
|
+
);
|
|
97
|
+
drizzleWhere = and(mainWhere, cursorWhere);
|
|
98
|
+
} else if (options.where) {
|
|
99
|
+
drizzleWhere = convertBasicExpressionToDrizzle(options.where, table);
|
|
100
|
+
} else if (options.cursor?.whereFrom) {
|
|
101
|
+
drizzleWhere = convertBasicExpressionToDrizzle(
|
|
102
|
+
options.cursor.whereFrom,
|
|
103
|
+
table,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (drizzleWhere) {
|
|
108
|
+
query = query.where(drizzleWhere);
|
|
109
|
+
hasWhere = true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (options.orderBy) {
|
|
114
|
+
const drizzleOrderBy = convertOrderByToDrizzle(options.orderBy, table);
|
|
115
|
+
query = query.orderBy(...drizzleOrderBy);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (options.limit !== undefined) {
|
|
119
|
+
query = query.limit(options.limit);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (options.offset !== undefined && options.offset > 0) {
|
|
123
|
+
query = query.offset(options.offset);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const items = (await query) as unknown as InferSchemaOutput<
|
|
127
|
+
SelectSchema<TTable>
|
|
128
|
+
>[];
|
|
129
|
+
|
|
130
|
+
if (config.interceptor?.onOperation) {
|
|
131
|
+
const contextParts: string[] = ["On-demand load"];
|
|
132
|
+
if (options.orderBy) contextParts.push("with sorting");
|
|
133
|
+
if (options.limit !== undefined)
|
|
134
|
+
contextParts.push(`limit ${options.limit}`);
|
|
135
|
+
if (options.offset !== undefined && options.offset > 0)
|
|
136
|
+
contextParts.push(`offset ${options.offset}`);
|
|
137
|
+
if (options.cursor) contextParts.push("with cursor pagination");
|
|
138
|
+
|
|
139
|
+
if (hasWhere) {
|
|
140
|
+
config.interceptor.onOperation({
|
|
141
|
+
type: "select-where",
|
|
142
|
+
tableName: config.tableName,
|
|
143
|
+
whereClause: "WHERE clause applied",
|
|
144
|
+
itemsReturned: items,
|
|
145
|
+
itemCount: items.length,
|
|
146
|
+
context: contextParts.join(", "),
|
|
147
|
+
timestamp: Date.now(),
|
|
148
|
+
});
|
|
149
|
+
} else {
|
|
150
|
+
config.interceptor.onOperation({
|
|
151
|
+
type: "select-all",
|
|
152
|
+
tableName: config.tableName,
|
|
153
|
+
itemsReturned: items,
|
|
154
|
+
itemCount: items.length,
|
|
155
|
+
context: contextParts.join(", "),
|
|
156
|
+
timestamp: Date.now(),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (config.interceptor?.onOperation) {
|
|
162
|
+
const contextParts: string[] = ["On-demand load"];
|
|
163
|
+
if (hasWhere) contextParts.push("with WHERE clause");
|
|
164
|
+
if (options.orderBy) contextParts.push("with sorting");
|
|
165
|
+
if (options.limit !== undefined)
|
|
166
|
+
contextParts.push(`limit ${options.limit}`);
|
|
167
|
+
if (options.offset !== undefined && options.offset > 0)
|
|
168
|
+
contextParts.push(`offset ${options.offset}`);
|
|
169
|
+
|
|
170
|
+
config.interceptor.onOperation({
|
|
171
|
+
type: "write",
|
|
172
|
+
tableName: config.tableName,
|
|
173
|
+
itemsWritten: items,
|
|
174
|
+
writeCount: items.length,
|
|
175
|
+
context: contextParts.join(", "),
|
|
176
|
+
timestamp: Date.now(),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return items as unknown as InferSchemaOutput<SelectSchema<TTable>>[];
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
handleInsert: async (items) => {
|
|
184
|
+
const results: Array<InferSchemaOutput<SelectSchema<TTable>>> = [];
|
|
185
|
+
|
|
186
|
+
await queueTransaction(async () => {
|
|
187
|
+
if (driverMode === "sync") {
|
|
188
|
+
config.drizzle.transaction((tx: typeof config.drizzle) => {
|
|
189
|
+
for (const itemToInsert of items) {
|
|
190
|
+
if (config.debug) {
|
|
191
|
+
console.log(
|
|
192
|
+
`[${new Date().toISOString()}] insertListener inserting`,
|
|
193
|
+
itemToInsert,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
const result = tx
|
|
197
|
+
.insert(table)
|
|
198
|
+
.values(
|
|
199
|
+
itemToInsert as unknown as SQLiteInsertValue<typeof table>,
|
|
200
|
+
)
|
|
201
|
+
.returning()
|
|
202
|
+
.all() as Array<InferSchemaOutput<SelectSchema<TTable>>>;
|
|
203
|
+
if (config.debug) {
|
|
204
|
+
console.log(
|
|
205
|
+
`[${new Date().toISOString()}] insertListener result`,
|
|
206
|
+
result,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
if (result.length > 0) {
|
|
210
|
+
results.push(result[0]);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
} else {
|
|
215
|
+
await config.drizzle.transaction(
|
|
216
|
+
async (tx: typeof config.drizzle) => {
|
|
217
|
+
for (const itemToInsert of items) {
|
|
218
|
+
if (config.debug) {
|
|
219
|
+
console.log(
|
|
220
|
+
`[${new Date().toISOString()}] insertListener inserting`,
|
|
221
|
+
itemToInsert,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
const result = (await tx
|
|
225
|
+
.insert(table)
|
|
226
|
+
.values(
|
|
227
|
+
itemToInsert as unknown as SQLiteInsertValue<typeof table>,
|
|
228
|
+
)
|
|
229
|
+
.returning()) as Array<
|
|
230
|
+
InferSchemaOutput<SelectSchema<TTable>>
|
|
231
|
+
>;
|
|
232
|
+
if (config.debug) {
|
|
233
|
+
console.log(
|
|
234
|
+
`[${new Date().toISOString()}] insertListener result`,
|
|
235
|
+
result,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
if (result.length > 0) {
|
|
239
|
+
results.push(result[0]);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (config.checkpoint) {
|
|
247
|
+
await config.checkpoint();
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
return results;
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
handleUpdate: async (mutations) => {
|
|
255
|
+
const results: Array<InferSchemaOutput<SelectSchema<TTable>>> = [];
|
|
256
|
+
|
|
257
|
+
await queueTransaction(async () => {
|
|
258
|
+
if (driverMode === "sync") {
|
|
259
|
+
config.drizzle.transaction((tx: typeof config.drizzle) => {
|
|
260
|
+
for (const mutation of mutations) {
|
|
261
|
+
if (config.debug) {
|
|
262
|
+
console.log(
|
|
263
|
+
`[${new Date().toISOString()}] updateListener updating`,
|
|
264
|
+
mutation,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
const updateTime = new Date();
|
|
268
|
+
const result = tx
|
|
269
|
+
.update(table)
|
|
270
|
+
.set({
|
|
271
|
+
...mutation.changes,
|
|
272
|
+
updatedAt: updateTime,
|
|
273
|
+
} as SQLiteUpdateSetSource<typeof table>)
|
|
274
|
+
// biome-ignore lint/suspicious/noExplicitAny: branded id key
|
|
275
|
+
.where(eq(table.id, mutation.key as any))
|
|
276
|
+
.returning()
|
|
277
|
+
.all() as Array<InferSchemaOutput<SelectSchema<TTable>>>;
|
|
278
|
+
if (config.debug) {
|
|
279
|
+
console.log(
|
|
280
|
+
`[${new Date().toISOString()}] updateListener result`,
|
|
281
|
+
result,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
results.push(...result);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
} else {
|
|
288
|
+
await config.drizzle.transaction(
|
|
289
|
+
async (tx: typeof config.drizzle) => {
|
|
290
|
+
for (const mutation of mutations) {
|
|
291
|
+
if (config.debug) {
|
|
292
|
+
console.log(
|
|
293
|
+
`[${new Date().toISOString()}] updateListener updating`,
|
|
294
|
+
mutation,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
const updateTime = new Date();
|
|
298
|
+
const result = (await tx
|
|
299
|
+
.update(table)
|
|
300
|
+
.set({
|
|
301
|
+
...mutation.changes,
|
|
302
|
+
updatedAt: updateTime,
|
|
303
|
+
} as SQLiteUpdateSetSource<typeof table>)
|
|
304
|
+
// biome-ignore lint/suspicious/noExplicitAny: branded id key
|
|
305
|
+
.where(eq(table.id, mutation.key as any))
|
|
306
|
+
.returning()) as Array<
|
|
307
|
+
InferSchemaOutput<SelectSchema<TTable>>
|
|
308
|
+
>;
|
|
309
|
+
if (config.debug) {
|
|
310
|
+
console.log(
|
|
311
|
+
`[${new Date().toISOString()}] updateListener result`,
|
|
312
|
+
result,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
results.push(...result);
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (config.checkpoint) {
|
|
322
|
+
await config.checkpoint();
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
return results;
|
|
327
|
+
},
|
|
328
|
+
|
|
329
|
+
handleDelete: async (mutations) => {
|
|
330
|
+
await queueTransaction(async () => {
|
|
331
|
+
if (driverMode === "sync") {
|
|
332
|
+
config.drizzle.transaction((tx: typeof config.drizzle) => {
|
|
333
|
+
for (const mutation of mutations) {
|
|
334
|
+
tx.delete(table)
|
|
335
|
+
// biome-ignore lint/suspicious/noExplicitAny: branded id key
|
|
336
|
+
.where(eq(table.id, mutation.key as any))
|
|
337
|
+
.run();
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
} else {
|
|
341
|
+
await config.drizzle.transaction(
|
|
342
|
+
async (tx: typeof config.drizzle) => {
|
|
343
|
+
for (const mutation of mutations) {
|
|
344
|
+
await tx
|
|
345
|
+
.delete(table)
|
|
346
|
+
// biome-ignore lint/suspicious/noExplicitAny: branded id key
|
|
347
|
+
.where(eq(table.id, mutation.key as any));
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (config.checkpoint) {
|
|
354
|
+
await config.checkpoint();
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
return backend;
|
|
361
|
+
}
|
|
@@ -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
|
+
}
|