@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 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.1",
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.0.0"
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.2.0"
65
+ "valibot": "^1.3.1"
66
66
  },
67
67
  "dependencies": {
68
68
  "@firtoz/db-helpers": "^2.0.0",
@@ -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
- SelectSchema<TTable>,
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
- ): v.GenericSchema<unknown> {
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 v.GenericSchema<unknown>;
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
- ): v.GenericSchema<unknown> {
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 v.GenericSchema<unknown>;
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
- // biome-ignore lint/suspicious/noExplicitAny: Schema type parameter needs to be flexible
251
- any
259
+ TSchema
252
260
  >["onInsert"];
253
261
  onUpdate?: CollectionConfig<
254
262
  InferSchemaOutput<SelectSchema<TTable>>,
255
263
  string,
256
- // biome-ignore lint/suspicious/noExplicitAny: Schema type parameter needs to be flexible
257
- any
264
+ TSchema
258
265
  >["onUpdate"];
259
266
  onDelete?: CollectionConfig<
260
267
  InferSchemaOutput<SelectSchema<TTable>>,
261
268
  string,
262
- // biome-ignore lint/suspicious/noExplicitAny: Schema type parameter needs to be flexible
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
+ }