@firtoz/drizzle-sqlite-wasm 0.2.14 → 0.2.16
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 +16 -0
- package/package.json +3 -3
- package/src/collections/sqlite-collection.ts +23 -513
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @firtoz/drizzle-sqlite-wasm
|
|
2
2
|
|
|
3
|
+
## 0.2.16
|
|
4
|
+
|
|
5
|
+
### Patch 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
|
+
- Updated dependencies [[`b714ebb`](https://github.com/firtoz/fullstack-toolkit/commit/b714ebbb62ec0e3c3aa56c4105e7499fac11d1e5)]:
|
|
10
|
+
- @firtoz/drizzle-utils@1.1.0
|
|
11
|
+
|
|
12
|
+
## 0.2.15
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- Updated dependencies [[`f887a36`](https://github.com/firtoz/fullstack-toolkit/commit/f887a3683bfc1e3db3db0e399c1494755af4008c)]:
|
|
17
|
+
- @firtoz/worker-helper@1.5.1
|
|
18
|
+
|
|
3
19
|
## 0.2.14
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firtoz/drizzle-sqlite-wasm",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.16",
|
|
4
4
|
"description": "Drizzle SQLite WASM bindings",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"module": "./src/index.ts",
|
|
@@ -70,9 +70,9 @@
|
|
|
70
70
|
},
|
|
71
71
|
"dependencies": {
|
|
72
72
|
"@firtoz/db-helpers": "^2.0.0",
|
|
73
|
-
"@firtoz/drizzle-utils": "^1.0
|
|
73
|
+
"@firtoz/drizzle-utils": "^1.1.0",
|
|
74
74
|
"@firtoz/maybe-error": "^1.5.2",
|
|
75
|
-
"@firtoz/worker-helper": "^1.5.
|
|
75
|
+
"@firtoz/worker-helper": "^1.5.1",
|
|
76
76
|
"@sqlite.org/sqlite-wasm": "^3.51.2-build8",
|
|
77
77
|
"@tanstack/db": "^0.5.33",
|
|
78
78
|
"drizzle-orm": "^0.45.1",
|
|
@@ -3,48 +3,25 @@ import type {
|
|
|
3
3
|
SyncMode,
|
|
4
4
|
CollectionConfig,
|
|
5
5
|
} from "@tanstack/db";
|
|
6
|
-
import type {
|
|
7
|
-
import {
|
|
8
|
-
eq,
|
|
9
|
-
sql,
|
|
10
|
-
type Table,
|
|
11
|
-
gt,
|
|
12
|
-
gte,
|
|
13
|
-
lt,
|
|
14
|
-
lte,
|
|
15
|
-
ne,
|
|
16
|
-
and,
|
|
17
|
-
or,
|
|
18
|
-
not,
|
|
19
|
-
isNull,
|
|
20
|
-
isNotNull,
|
|
21
|
-
like,
|
|
22
|
-
inArray,
|
|
23
|
-
asc,
|
|
24
|
-
desc,
|
|
25
|
-
type SQL,
|
|
26
|
-
} from "drizzle-orm";
|
|
27
|
-
import {
|
|
28
|
-
type SQLiteUpdateSetSource,
|
|
29
|
-
type BaseSQLiteDatabase,
|
|
30
|
-
type SQLiteInsertValue,
|
|
31
|
-
SQLiteColumn,
|
|
32
|
-
} from "drizzle-orm/sqlite-core";
|
|
6
|
+
import type { Table } from "drizzle-orm";
|
|
7
|
+
import type { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core";
|
|
33
8
|
import type { CollectionUtils } from "@firtoz/db-helpers";
|
|
34
9
|
import type {
|
|
35
10
|
SelectSchema,
|
|
11
|
+
InsertToSelectSchema,
|
|
36
12
|
TableWithRequiredFields,
|
|
37
13
|
BaseSyncConfig,
|
|
38
|
-
SyncBackend,
|
|
39
14
|
} from "@firtoz/drizzle-utils";
|
|
40
15
|
import {
|
|
41
16
|
createSyncFunction,
|
|
42
17
|
createInsertSchemaWithIdDefault,
|
|
43
18
|
createGetKeyFunction,
|
|
44
19
|
createCollectionConfig,
|
|
20
|
+
createSqliteTableSyncBackend,
|
|
21
|
+
type SQLOperation,
|
|
22
|
+
type SQLInterceptor,
|
|
45
23
|
} from "@firtoz/drizzle-utils";
|
|
46
|
-
|
|
47
|
-
import type * as v from "valibot";
|
|
24
|
+
export type { SQLOperation, SQLInterceptor };
|
|
48
25
|
|
|
49
26
|
export type AnyDrizzleDatabase = BaseSQLiteDatabase<
|
|
50
27
|
"async",
|
|
@@ -56,81 +33,7 @@ export type AnyDrizzleDatabase = BaseSQLiteDatabase<
|
|
|
56
33
|
export type DrizzleSchema<TDrizzle extends AnyDrizzleDatabase> =
|
|
57
34
|
TDrizzle["_"]["fullSchema"];
|
|
58
35
|
|
|
59
|
-
|
|
60
|
-
* Operation tracking for SQLite queries
|
|
61
|
-
* Useful for testing and debugging to verify what operations are actually performed
|
|
62
|
-
*
|
|
63
|
-
* Uses discriminated unions for type safety - TypeScript can narrow the type based on the 'type' field
|
|
64
|
-
*/
|
|
65
|
-
export type SQLOperation =
|
|
66
|
-
| {
|
|
67
|
-
type: "select-all";
|
|
68
|
-
tableName: string;
|
|
69
|
-
itemsReturned: unknown[];
|
|
70
|
-
itemCount: number;
|
|
71
|
-
context: string;
|
|
72
|
-
sql?: string;
|
|
73
|
-
timestamp: number;
|
|
74
|
-
}
|
|
75
|
-
| {
|
|
76
|
-
type: "select-where";
|
|
77
|
-
tableName: string;
|
|
78
|
-
whereClause: string;
|
|
79
|
-
itemsReturned: unknown[];
|
|
80
|
-
itemCount: number;
|
|
81
|
-
context: string;
|
|
82
|
-
sql?: string;
|
|
83
|
-
timestamp: number;
|
|
84
|
-
}
|
|
85
|
-
| {
|
|
86
|
-
type: "write";
|
|
87
|
-
tableName: string;
|
|
88
|
-
itemsWritten: unknown[];
|
|
89
|
-
writeCount: number;
|
|
90
|
-
context: string;
|
|
91
|
-
timestamp: number;
|
|
92
|
-
}
|
|
93
|
-
| {
|
|
94
|
-
type: "insert";
|
|
95
|
-
tableName: string;
|
|
96
|
-
item: unknown;
|
|
97
|
-
sql?: string;
|
|
98
|
-
timestamp: number;
|
|
99
|
-
}
|
|
100
|
-
| {
|
|
101
|
-
type: "update";
|
|
102
|
-
tableName: string;
|
|
103
|
-
updates: unknown;
|
|
104
|
-
sql?: string;
|
|
105
|
-
timestamp: number;
|
|
106
|
-
}
|
|
107
|
-
| {
|
|
108
|
-
type: "delete";
|
|
109
|
-
tableName: string;
|
|
110
|
-
sql?: string;
|
|
111
|
-
timestamp: number;
|
|
112
|
-
}
|
|
113
|
-
| {
|
|
114
|
-
/** Raw SQL query executed directly via Drizzle (not through collection) */
|
|
115
|
-
type: "raw-query";
|
|
116
|
-
sql: string;
|
|
117
|
-
params?: unknown[];
|
|
118
|
-
method: string;
|
|
119
|
-
rowCount: number;
|
|
120
|
-
context: string;
|
|
121
|
-
timestamp: number;
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Interceptor interface for tracking SQLite operations
|
|
126
|
-
* Allows tests and debugging tools to observe what operations are performed
|
|
127
|
-
*/
|
|
128
|
-
export interface SQLInterceptor {
|
|
129
|
-
/** Called when any SQLite operation is performed */
|
|
130
|
-
onOperation?: (operation: SQLOperation) => void;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export interface DrizzleCollectionConfig<
|
|
36
|
+
export interface DrizzleSqliteCollectionConfig<
|
|
134
37
|
TDrizzle extends AnyDrizzleDatabase,
|
|
135
38
|
TTableName extends ValidTableNames<DrizzleSchema<TDrizzle>>,
|
|
136
39
|
> {
|
|
@@ -161,427 +64,40 @@ export type ValidTableNames<TSchema extends Record<string, unknown>> = {
|
|
|
161
64
|
[K in keyof TSchema]: TSchema[K] extends TableWithRequiredFields ? K : never;
|
|
162
65
|
}[keyof TSchema];
|
|
163
66
|
|
|
164
|
-
/**
|
|
165
|
-
* Return type for sqliteCollectionOptions - configuration object for creating SQLite collections
|
|
166
|
-
*
|
|
167
|
-
* Note: The third type parameter of CollectionConfig uses `any` to maintain compatibility with
|
|
168
|
-
* TanStack DB's type system, which expects different schema types in different contexts.
|
|
169
|
-
*/
|
|
170
67
|
export type SqliteCollectionConfig<TTable extends Table> = Omit<
|
|
171
68
|
CollectionConfig<
|
|
172
69
|
InferSchemaOutput<SelectSchema<TTable>>,
|
|
173
70
|
string,
|
|
174
|
-
|
|
175
|
-
any
|
|
71
|
+
InsertToSelectSchema<TTable>
|
|
176
72
|
>,
|
|
177
73
|
"utils"
|
|
178
74
|
> & {
|
|
179
|
-
schema:
|
|
75
|
+
schema: InsertToSelectSchema<TTable>;
|
|
180
76
|
utils: CollectionUtils<InferSchemaOutput<SelectSchema<TTable>>>;
|
|
181
77
|
};
|
|
182
78
|
|
|
183
|
-
/**
|
|
184
|
-
* Converts TanStack DB IR BasicExpression to Drizzle SQL expression
|
|
185
|
-
*
|
|
186
|
-
* Supported operators that TanStack DB pushes down to backend (SUPPORTED_COLLECTION_FUNCS):
|
|
187
|
-
* - eq, gt, lt, gte, lte, and, or, in, isNull, isUndefined, not
|
|
188
|
-
*
|
|
189
|
-
* Additional operators handled for completeness (won't be pushed down in on-demand mode):
|
|
190
|
-
* - ne, isNotNull, like
|
|
191
|
-
*/
|
|
192
|
-
function convertBasicExpressionToDrizzle<TTable extends Table>(
|
|
193
|
-
expression: IR.BasicExpression,
|
|
194
|
-
table: TTable,
|
|
195
|
-
): SQL {
|
|
196
|
-
switch (expression.type) {
|
|
197
|
-
case "ref": {
|
|
198
|
-
const propRef = expression;
|
|
199
|
-
const columnName = propRef.path[propRef.path.length - 1];
|
|
200
|
-
const column = table[columnName as keyof typeof table];
|
|
201
|
-
|
|
202
|
-
if (!column || !(column instanceof SQLiteColumn)) {
|
|
203
|
-
console.error("[SQLite Collection] Column lookup failed:", {
|
|
204
|
-
columnName,
|
|
205
|
-
column,
|
|
206
|
-
tableKeys: Object.keys(table),
|
|
207
|
-
hasColumn: columnName in table,
|
|
208
|
-
});
|
|
209
|
-
throw new Error(`Column ${String(columnName)} not found in table`);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return column as unknown as SQL;
|
|
213
|
-
}
|
|
214
|
-
case "val": {
|
|
215
|
-
const value = expression;
|
|
216
|
-
return sql`${value.value}`;
|
|
217
|
-
}
|
|
218
|
-
case "func": {
|
|
219
|
-
const func = expression;
|
|
220
|
-
const args = func.args.map((arg) =>
|
|
221
|
-
convertBasicExpressionToDrizzle(arg, table),
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
switch (func.name) {
|
|
225
|
-
case "eq":
|
|
226
|
-
return eq(args[0], args[1]);
|
|
227
|
-
case "ne":
|
|
228
|
-
return ne(args[0], args[1]);
|
|
229
|
-
case "gt":
|
|
230
|
-
return gt(args[0], args[1]);
|
|
231
|
-
case "gte":
|
|
232
|
-
return gte(args[0], args[1]);
|
|
233
|
-
case "lt":
|
|
234
|
-
return lt(args[0], args[1]);
|
|
235
|
-
case "lte":
|
|
236
|
-
return lte(args[0], args[1]);
|
|
237
|
-
case "and": {
|
|
238
|
-
const result = and(...args);
|
|
239
|
-
if (!result) {
|
|
240
|
-
throw new Error("Invalid 'and' expression - no arguments provided");
|
|
241
|
-
}
|
|
242
|
-
return result;
|
|
243
|
-
}
|
|
244
|
-
case "or": {
|
|
245
|
-
const result = or(...args);
|
|
246
|
-
if (!result) {
|
|
247
|
-
throw new Error("Invalid 'or' expression - no arguments provided");
|
|
248
|
-
}
|
|
249
|
-
return result;
|
|
250
|
-
}
|
|
251
|
-
case "not":
|
|
252
|
-
return not(args[0]);
|
|
253
|
-
case "isNull":
|
|
254
|
-
return isNull(args[0]);
|
|
255
|
-
case "isNotNull":
|
|
256
|
-
return isNotNull(args[0]);
|
|
257
|
-
case "like":
|
|
258
|
-
return like(args[0], args[1]);
|
|
259
|
-
case "in":
|
|
260
|
-
return inArray(args[0], args[1]);
|
|
261
|
-
case "isUndefined":
|
|
262
|
-
// isUndefined is same as isNull in SQLite
|
|
263
|
-
return isNull(args[0]);
|
|
264
|
-
default:
|
|
265
|
-
throw new Error(`Unsupported function: ${func.name}`);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
default:
|
|
269
|
-
exhaustiveGuard(expression);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* Converts TanStack DB OrderBy to Drizzle orderBy
|
|
275
|
-
*/
|
|
276
|
-
function convertOrderByToDrizzle<TTable extends Table>(
|
|
277
|
-
orderBy: IR.OrderBy,
|
|
278
|
-
table: TTable,
|
|
279
|
-
): SQL[] {
|
|
280
|
-
return orderBy.map((clause) => {
|
|
281
|
-
const expression = convertBasicExpressionToDrizzle(
|
|
282
|
-
clause.expression,
|
|
283
|
-
table,
|
|
284
|
-
);
|
|
285
|
-
const direction = clause.compareOptions.direction || "asc";
|
|
286
|
-
|
|
287
|
-
return direction === "asc" ? asc(expression) : desc(expression);
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
|
|
291
79
|
export function sqliteCollectionOptions<
|
|
292
80
|
const TDrizzle extends AnyDrizzleDatabase,
|
|
293
81
|
const TTableName extends string & ValidTableNames<DrizzleSchema<TDrizzle>>,
|
|
294
82
|
TTable extends DrizzleSchema<TDrizzle>[TTableName] & TableWithRequiredFields,
|
|
295
83
|
>(
|
|
296
|
-
config:
|
|
84
|
+
config: DrizzleSqliteCollectionConfig<TDrizzle, TTableName>,
|
|
297
85
|
): SqliteCollectionConfig<TTable> {
|
|
298
86
|
const tableName = config.tableName as string &
|
|
299
87
|
ValidTableNames<DrizzleSchema<TDrizzle>>;
|
|
300
88
|
|
|
301
89
|
const table = config.drizzle?._.fullSchema[tableName] as TTable;
|
|
302
90
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
() => {}, // Success handler - return undefined to reset queue
|
|
313
|
-
() => {}, // Error handler - return undefined to reset queue (queue continues)
|
|
314
|
-
);
|
|
315
|
-
// Return the actual result so errors propagate to the caller
|
|
316
|
-
return result;
|
|
317
|
-
};
|
|
318
|
-
|
|
319
|
-
// Create backend-specific implementation
|
|
320
|
-
const backend: SyncBackend<TTable> = {
|
|
321
|
-
initialLoad: async () => {
|
|
322
|
-
const items = (await config.drizzle
|
|
323
|
-
.select()
|
|
324
|
-
.from(table)) as unknown as InferSchemaOutput<SelectSchema<TTable>>[];
|
|
325
|
-
|
|
326
|
-
// Log SQL operation
|
|
327
|
-
if (config.interceptor?.onOperation) {
|
|
328
|
-
config.interceptor.onOperation({
|
|
329
|
-
type: "select-all",
|
|
330
|
-
tableName: config.tableName as string,
|
|
331
|
-
itemsReturned: items,
|
|
332
|
-
itemCount: items.length,
|
|
333
|
-
context: "Initial load (eager mode)",
|
|
334
|
-
timestamp: Date.now(),
|
|
335
|
-
});
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// Log write operation
|
|
339
|
-
if (config.interceptor?.onOperation) {
|
|
340
|
-
config.interceptor.onOperation({
|
|
341
|
-
type: "write",
|
|
342
|
-
tableName: config.tableName as string,
|
|
343
|
-
itemsWritten: items,
|
|
344
|
-
writeCount: items.length,
|
|
345
|
-
context: "Initial load (eager mode)",
|
|
346
|
-
timestamp: Date.now(),
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
return items as unknown as InferSchemaOutput<SelectSchema<TTable>>[];
|
|
351
|
-
},
|
|
352
|
-
|
|
353
|
-
loadSubset: async (options) => {
|
|
354
|
-
// Build the query with optional where, orderBy, limit, and offset
|
|
355
|
-
// Use $dynamic() to enable dynamic query building
|
|
356
|
-
let query = config.drizzle.select().from(table).$dynamic();
|
|
357
|
-
|
|
358
|
-
// Combine where with cursor expressions if present
|
|
359
|
-
// The cursor.whereFrom gives us rows after the cursor position
|
|
360
|
-
let hasWhere = false;
|
|
361
|
-
if (options.where || options.cursor?.whereFrom) {
|
|
362
|
-
let drizzleWhere: SQL | undefined;
|
|
363
|
-
|
|
364
|
-
if (options.where && options.cursor?.whereFrom) {
|
|
365
|
-
// Combine main where with cursor expression using AND
|
|
366
|
-
const mainWhere = convertBasicExpressionToDrizzle(
|
|
367
|
-
options.where,
|
|
368
|
-
table,
|
|
369
|
-
);
|
|
370
|
-
const cursorWhere = convertBasicExpressionToDrizzle(
|
|
371
|
-
options.cursor.whereFrom,
|
|
372
|
-
table,
|
|
373
|
-
);
|
|
374
|
-
drizzleWhere = and(mainWhere, cursorWhere);
|
|
375
|
-
} else if (options.where) {
|
|
376
|
-
drizzleWhere = convertBasicExpressionToDrizzle(options.where, table);
|
|
377
|
-
} else if (options.cursor?.whereFrom) {
|
|
378
|
-
drizzleWhere = convertBasicExpressionToDrizzle(
|
|
379
|
-
options.cursor.whereFrom,
|
|
380
|
-
table,
|
|
381
|
-
);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
if (drizzleWhere) {
|
|
385
|
-
query = query.where(drizzleWhere);
|
|
386
|
-
hasWhere = true;
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
if (options.orderBy) {
|
|
391
|
-
const drizzleOrderBy = convertOrderByToDrizzle(options.orderBy, table);
|
|
392
|
-
query = query.orderBy(...drizzleOrderBy);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
if (options.limit !== undefined) {
|
|
396
|
-
query = query.limit(options.limit);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// Apply offset for offset-based pagination
|
|
400
|
-
if (options.offset !== undefined && options.offset > 0) {
|
|
401
|
-
query = query.offset(options.offset);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
const items = (await query) as unknown as InferSchemaOutput<
|
|
405
|
-
SelectSchema<TTable>
|
|
406
|
-
>[];
|
|
407
|
-
|
|
408
|
-
// Log SQL operation
|
|
409
|
-
if (config.interceptor?.onOperation) {
|
|
410
|
-
const contextParts: string[] = ["On-demand load"];
|
|
411
|
-
if (options.orderBy) {
|
|
412
|
-
contextParts.push("with sorting");
|
|
413
|
-
}
|
|
414
|
-
if (options.limit !== undefined) {
|
|
415
|
-
contextParts.push(`limit ${options.limit}`);
|
|
416
|
-
}
|
|
417
|
-
if (options.offset !== undefined && options.offset > 0) {
|
|
418
|
-
contextParts.push(`offset ${options.offset}`);
|
|
419
|
-
}
|
|
420
|
-
if (options.cursor) {
|
|
421
|
-
contextParts.push("with cursor pagination");
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
if (hasWhere) {
|
|
425
|
-
config.interceptor.onOperation({
|
|
426
|
-
type: "select-where",
|
|
427
|
-
tableName: config.tableName as string,
|
|
428
|
-
whereClause: "WHERE clause applied",
|
|
429
|
-
itemsReturned: items,
|
|
430
|
-
itemCount: items.length,
|
|
431
|
-
context: contextParts.join(", "),
|
|
432
|
-
timestamp: Date.now(),
|
|
433
|
-
});
|
|
434
|
-
} else {
|
|
435
|
-
config.interceptor.onOperation({
|
|
436
|
-
type: "select-all",
|
|
437
|
-
tableName: config.tableName as string,
|
|
438
|
-
itemsReturned: items,
|
|
439
|
-
itemCount: items.length,
|
|
440
|
-
context: contextParts.join(", "),
|
|
441
|
-
timestamp: Date.now(),
|
|
442
|
-
});
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// Log write operation
|
|
447
|
-
if (config.interceptor?.onOperation) {
|
|
448
|
-
const contextParts: string[] = ["On-demand load"];
|
|
449
|
-
if (hasWhere) {
|
|
450
|
-
contextParts.push("with WHERE clause");
|
|
451
|
-
}
|
|
452
|
-
if (options.orderBy) {
|
|
453
|
-
contextParts.push("with sorting");
|
|
454
|
-
}
|
|
455
|
-
if (options.limit !== undefined) {
|
|
456
|
-
contextParts.push(`limit ${options.limit}`);
|
|
457
|
-
}
|
|
458
|
-
if (options.offset !== undefined && options.offset > 0) {
|
|
459
|
-
contextParts.push(`offset ${options.offset}`);
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
config.interceptor.onOperation({
|
|
463
|
-
type: "write",
|
|
464
|
-
tableName: config.tableName as string,
|
|
465
|
-
itemsWritten: items,
|
|
466
|
-
writeCount: items.length,
|
|
467
|
-
context: contextParts.join(", "),
|
|
468
|
-
timestamp: Date.now(),
|
|
469
|
-
});
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
return items as unknown as InferSchemaOutput<SelectSchema<TTable>>[];
|
|
473
|
-
},
|
|
474
|
-
|
|
475
|
-
handleInsert: async (items) => {
|
|
476
|
-
const results: Array<InferSchemaOutput<SelectSchema<TTable>>> = [];
|
|
477
|
-
|
|
478
|
-
// Queue the transaction to serialize SQLite operations
|
|
479
|
-
await queueTransaction(async () => {
|
|
480
|
-
await config.drizzle.transaction(async (tx) => {
|
|
481
|
-
for (const itemToInsert of items) {
|
|
482
|
-
if (config.debug) {
|
|
483
|
-
console.log(
|
|
484
|
-
`[${new Date().toISOString()}] insertListener inserting`,
|
|
485
|
-
itemToInsert,
|
|
486
|
-
);
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
const result: Array<InferSchemaOutput<SelectSchema<TTable>>> =
|
|
490
|
-
(await tx
|
|
491
|
-
.insert(table)
|
|
492
|
-
.values(
|
|
493
|
-
itemToInsert as unknown as SQLiteInsertValue<typeof table>,
|
|
494
|
-
)
|
|
495
|
-
.returning()) as Array<InferSchemaOutput<SelectSchema<TTable>>>;
|
|
496
|
-
|
|
497
|
-
if (config.debug) {
|
|
498
|
-
console.log(
|
|
499
|
-
`[${new Date().toISOString()}] insertListener result`,
|
|
500
|
-
result,
|
|
501
|
-
);
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
if (result.length > 0) {
|
|
505
|
-
results.push(result[0]);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
// Checkpoint to ensure WAL is flushed to main DB file
|
|
511
|
-
if (config.checkpoint) {
|
|
512
|
-
await config.checkpoint();
|
|
513
|
-
}
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
return results;
|
|
517
|
-
},
|
|
518
|
-
|
|
519
|
-
handleUpdate: async (mutations) => {
|
|
520
|
-
const results: Array<InferSchemaOutput<SelectSchema<TTable>>> = [];
|
|
521
|
-
|
|
522
|
-
// Queue the transaction to serialize SQLite operations
|
|
523
|
-
await queueTransaction(async () => {
|
|
524
|
-
await config.drizzle.transaction(async (tx) => {
|
|
525
|
-
for (const mutation of mutations) {
|
|
526
|
-
if (config.debug) {
|
|
527
|
-
console.log(
|
|
528
|
-
`[${new Date().toISOString()}] updateListener updating`,
|
|
529
|
-
mutation,
|
|
530
|
-
);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
const updateTime = new Date();
|
|
534
|
-
const result: Array<InferSchemaOutput<SelectSchema<TTable>>> =
|
|
535
|
-
(await tx
|
|
536
|
-
.update(table)
|
|
537
|
-
.set({
|
|
538
|
-
...mutation.changes,
|
|
539
|
-
updatedAt: updateTime,
|
|
540
|
-
} as SQLiteUpdateSetSource<typeof table>)
|
|
541
|
-
// biome-ignore lint/suspicious/noExplicitAny: Key is string but table.id is branded type
|
|
542
|
-
.where(eq(table.id, mutation.key as any))
|
|
543
|
-
.returning()) as Array<InferSchemaOutput<SelectSchema<TTable>>>;
|
|
544
|
-
|
|
545
|
-
if (config.debug) {
|
|
546
|
-
console.log(
|
|
547
|
-
`[${new Date().toISOString()}] updateListener result`,
|
|
548
|
-
result,
|
|
549
|
-
);
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
results.push(...result);
|
|
553
|
-
}
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
// Checkpoint to ensure WAL is flushed to main DB file BEFORE UI updates
|
|
557
|
-
// This ensures persistence before the updateListener completes
|
|
558
|
-
if (config.checkpoint) {
|
|
559
|
-
await config.checkpoint();
|
|
560
|
-
}
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
return results;
|
|
564
|
-
},
|
|
565
|
-
|
|
566
|
-
handleDelete: async (mutations) => {
|
|
567
|
-
// Queue the transaction to serialize SQLite operations
|
|
568
|
-
await queueTransaction(async () => {
|
|
569
|
-
await config.drizzle.transaction(async (tx) => {
|
|
570
|
-
for (const mutation of mutations) {
|
|
571
|
-
// biome-ignore lint/suspicious/noExplicitAny: Key is string but table.id is branded type
|
|
572
|
-
await tx.delete(table).where(eq(table.id, mutation.key as any));
|
|
573
|
-
}
|
|
574
|
-
});
|
|
575
|
-
|
|
576
|
-
// Checkpoint to ensure WAL is flushed to main DB file
|
|
577
|
-
if (config.checkpoint) {
|
|
578
|
-
await config.checkpoint();
|
|
579
|
-
}
|
|
580
|
-
});
|
|
581
|
-
},
|
|
582
|
-
};
|
|
91
|
+
const backend = createSqliteTableSyncBackend({
|
|
92
|
+
drizzle: config.drizzle,
|
|
93
|
+
table,
|
|
94
|
+
tableName: config.tableName as string,
|
|
95
|
+
debug: config.debug,
|
|
96
|
+
checkpoint: config.checkpoint,
|
|
97
|
+
interceptor: config.interceptor,
|
|
98
|
+
driverMode: "async",
|
|
99
|
+
});
|
|
583
100
|
|
|
584
|
-
// Create sync function using shared utilities
|
|
585
101
|
const baseSyncConfig: BaseSyncConfig<TTable> = {
|
|
586
102
|
table,
|
|
587
103
|
readyPromise: config.readyPromise,
|
|
@@ -591,11 +107,8 @@ export function sqliteCollectionOptions<
|
|
|
591
107
|
|
|
592
108
|
const syncResult = createSyncFunction(baseSyncConfig, backend);
|
|
593
109
|
|
|
594
|
-
// Create insert schema with ID default
|
|
595
|
-
// (Other defaults like createdAt/updatedAt are handled by SQLite)
|
|
596
110
|
const schema = createInsertSchemaWithIdDefault(table);
|
|
597
111
|
|
|
598
|
-
// Create collection config using shared utilities
|
|
599
112
|
const collectionConfig = createCollectionConfig({
|
|
600
113
|
schema,
|
|
601
114
|
getKey: createGetKeyFunction<TTable>(),
|
|
@@ -603,24 +116,21 @@ export function sqliteCollectionOptions<
|
|
|
603
116
|
onInsert: config.debug
|
|
604
117
|
? async (params) => {
|
|
605
118
|
console.log("onInsert", params);
|
|
606
|
-
//
|
|
607
|
-
// biome-ignore lint/style/noNonNullAssertion: onInsert is always defined in SyncFunctionResult
|
|
119
|
+
// biome-ignore lint/style/noNonNullAssertion: onInsert is always defined in createSyncFunction
|
|
608
120
|
await syncResult.onInsert!(params);
|
|
609
121
|
}
|
|
610
122
|
: undefined,
|
|
611
123
|
onUpdate: config.debug
|
|
612
124
|
? async (params) => {
|
|
613
125
|
console.log("onUpdate", params);
|
|
614
|
-
//
|
|
615
|
-
// biome-ignore lint/style/noNonNullAssertion: onUpdate is always defined in SyncFunctionResult
|
|
126
|
+
// biome-ignore lint/style/noNonNullAssertion: onUpdate is always defined in createSyncFunction
|
|
616
127
|
await syncResult.onUpdate!(params);
|
|
617
128
|
}
|
|
618
129
|
: undefined,
|
|
619
130
|
onDelete: config.debug
|
|
620
131
|
? async (params) => {
|
|
621
132
|
console.log("onDelete", params);
|
|
622
|
-
//
|
|
623
|
-
// biome-ignore lint/style/noNonNullAssertion: onDelete is always defined in SyncFunctionResult
|
|
133
|
+
// biome-ignore lint/style/noNonNullAssertion: onDelete is always defined in createSyncFunction
|
|
624
134
|
await syncResult.onDelete!(params);
|
|
625
135
|
}
|
|
626
136
|
: undefined,
|