@firtoz/drizzle-sqlite-wasm 0.2.1 → 0.2.2
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 +9 -11
- package/src/collections/sqlite-collection.ts +93 -13
- package/src/drizzle/worker.ts +140 -0
- package/src/hooks/useDrizzleSqliteDb.ts +61 -34
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @firtoz/drizzle-sqlite-wasm
|
|
2
2
|
|
|
3
|
+
## 0.2.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`8abab0a`](https://github.com/firtoz/fullstack-toolkit/commit/8abab0ae7a99320a4254cb128c0fd823726e58e0) Thanks [@firtoz](https://github.com/firtoz)! - Fix critical bug where debug mode prevented database operations from executing. Debug handlers now properly wrap and call the actual backend handlers instead of replacing them.
|
|
8
|
+
|
|
9
|
+
Add cursor-based and offset-based pagination support to `loadSubset` operations, enabling efficient navigation through large datasets.
|
|
10
|
+
|
|
11
|
+
Add `SQLInterceptor` support to log all SQL queries, including direct Drizzle queries, with the new `createInstrumentedDrizzle` function. This provides comprehensive query visibility for debugging and monitoring.
|
|
12
|
+
|
|
13
|
+
Add explicit return type `SqliteCollectionConfig<TTable>` to `sqliteCollectionOptions` function, improving type safety and eliminating the `any` cast at the return statement.
|
|
14
|
+
|
|
15
|
+
- Updated dependencies [[`8abab0a`](https://github.com/firtoz/fullstack-toolkit/commit/8abab0ae7a99320a4254cb128c0fd823726e58e0), [`8abab0a`](https://github.com/firtoz/fullstack-toolkit/commit/8abab0ae7a99320a4254cb128c0fd823726e58e0)]:
|
|
16
|
+
- @firtoz/maybe-error@1.5.2
|
|
17
|
+
- @firtoz/drizzle-utils@0.3.1
|
|
18
|
+
|
|
3
19
|
## 0.2.1
|
|
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.2",
|
|
4
4
|
"description": "Drizzle SQLite WASM bindings",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"module": "./src/index.ts",
|
|
@@ -43,9 +43,7 @@
|
|
|
43
43
|
"typecheck": "tsc --noEmit -p ./tsconfig.json",
|
|
44
44
|
"lint": "biome check --write src",
|
|
45
45
|
"lint:ci": "biome ci src",
|
|
46
|
-
"format": "biome format src --write"
|
|
47
|
-
"test": "bun test --pass-with-no-tests",
|
|
48
|
-
"test:watch": "bun test --watch"
|
|
46
|
+
"format": "biome format src --write"
|
|
49
47
|
},
|
|
50
48
|
"keywords": [
|
|
51
49
|
"typescript",
|
|
@@ -71,19 +69,19 @@
|
|
|
71
69
|
"access": "public"
|
|
72
70
|
},
|
|
73
71
|
"dependencies": {
|
|
74
|
-
"@firtoz/drizzle-utils": "^0.3.
|
|
75
|
-
"@firtoz/maybe-error": "^1.5.
|
|
72
|
+
"@firtoz/drizzle-utils": "^0.3.1",
|
|
73
|
+
"@firtoz/maybe-error": "^1.5.2",
|
|
76
74
|
"@firtoz/worker-helper": "^1.0.0",
|
|
77
75
|
"@sqlite.org/sqlite-wasm": "^3.51.1-build2",
|
|
78
|
-
"@tanstack/db": "^0.5.
|
|
79
|
-
"drizzle-orm": "^0.
|
|
76
|
+
"@tanstack/db": "^0.5.15",
|
|
77
|
+
"drizzle-orm": "^0.45.1",
|
|
80
78
|
"drizzle-valibot": "^0.4.2",
|
|
81
|
-
"react": "^19.2.
|
|
79
|
+
"react": "^19.2.3",
|
|
82
80
|
"valibot": "^1.2.0",
|
|
83
|
-
"zod": "^4.1
|
|
81
|
+
"zod": "^4.2.1"
|
|
84
82
|
},
|
|
85
83
|
"devDependencies": {
|
|
86
|
-
"@standard-schema/spec": "^1.
|
|
84
|
+
"@standard-schema/spec": "^1.1.0",
|
|
87
85
|
"@types/react": "^19.2.7"
|
|
88
86
|
}
|
|
89
87
|
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
InferSchemaOutput,
|
|
3
|
+
SyncMode,
|
|
4
|
+
CollectionConfig,
|
|
5
|
+
} from "@tanstack/db";
|
|
2
6
|
import type { IR } from "@tanstack/db";
|
|
3
7
|
import {
|
|
4
8
|
eq,
|
|
@@ -31,6 +35,7 @@ import type {
|
|
|
31
35
|
TableWithRequiredFields,
|
|
32
36
|
BaseSyncConfig,
|
|
33
37
|
SyncBackend,
|
|
38
|
+
CollectionUtils,
|
|
34
39
|
} from "@firtoz/drizzle-utils";
|
|
35
40
|
import {
|
|
36
41
|
createSyncFunction,
|
|
@@ -38,6 +43,7 @@ import {
|
|
|
38
43
|
createGetKeyFunction,
|
|
39
44
|
createCollectionConfig,
|
|
40
45
|
} from "@firtoz/drizzle-utils";
|
|
46
|
+
import type * as v from "valibot";
|
|
41
47
|
|
|
42
48
|
export type AnyDrizzleDatabase = BaseSQLiteDatabase<
|
|
43
49
|
"async",
|
|
@@ -102,6 +108,16 @@ export type SQLOperation =
|
|
|
102
108
|
tableName: string;
|
|
103
109
|
sql?: string;
|
|
104
110
|
timestamp: number;
|
|
111
|
+
}
|
|
112
|
+
| {
|
|
113
|
+
/** Raw SQL query executed directly via Drizzle (not through collection) */
|
|
114
|
+
type: "raw-query";
|
|
115
|
+
sql: string;
|
|
116
|
+
params?: unknown[];
|
|
117
|
+
method: string;
|
|
118
|
+
rowCount: number;
|
|
119
|
+
context: string;
|
|
120
|
+
timestamp: number;
|
|
105
121
|
};
|
|
106
122
|
|
|
107
123
|
/**
|
|
@@ -144,6 +160,25 @@ export type ValidTableNames<TSchema extends Record<string, unknown>> = {
|
|
|
144
160
|
[K in keyof TSchema]: TSchema[K] extends TableWithRequiredFields ? K : never;
|
|
145
161
|
}[keyof TSchema];
|
|
146
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Return type for sqliteCollectionOptions - configuration object for creating SQLite collections
|
|
165
|
+
*
|
|
166
|
+
* Note: The third type parameter of CollectionConfig uses `any` to maintain compatibility with
|
|
167
|
+
* TanStack DB's type system, which expects different schema types in different contexts.
|
|
168
|
+
*/
|
|
169
|
+
export type SqliteCollectionConfig<TTable extends Table> = Omit<
|
|
170
|
+
CollectionConfig<
|
|
171
|
+
InferSchemaOutput<SelectSchema<TTable>>,
|
|
172
|
+
string,
|
|
173
|
+
// biome-ignore lint/suspicious/noExplicitAny: Required for TanStack DB type compatibility
|
|
174
|
+
any
|
|
175
|
+
>,
|
|
176
|
+
"utils"
|
|
177
|
+
> & {
|
|
178
|
+
schema: v.GenericSchema<unknown>;
|
|
179
|
+
utils: CollectionUtils<InferSchemaOutput<SelectSchema<TTable>>>;
|
|
180
|
+
};
|
|
181
|
+
|
|
147
182
|
/**
|
|
148
183
|
* Converts TanStack DB IR BasicExpression to Drizzle SQL expression
|
|
149
184
|
*
|
|
@@ -262,7 +297,9 @@ export function sqliteCollectionOptions<
|
|
|
262
297
|
const TDrizzle extends AnyDrizzleDatabase,
|
|
263
298
|
const TTableName extends string & ValidTableNames<DrizzleSchema<TDrizzle>>,
|
|
264
299
|
TTable extends DrizzleSchema<TDrizzle>[TTableName] & TableWithRequiredFields,
|
|
265
|
-
>(
|
|
300
|
+
>(
|
|
301
|
+
config: DrizzleCollectionConfig<TDrizzle, TTableName>,
|
|
302
|
+
): SqliteCollectionConfig<TTable> {
|
|
266
303
|
const tableName = config.tableName as string &
|
|
267
304
|
ValidTableNames<DrizzleSchema<TDrizzle>>;
|
|
268
305
|
|
|
@@ -321,19 +358,40 @@ export function sqliteCollectionOptions<
|
|
|
321
358
|
},
|
|
322
359
|
|
|
323
360
|
loadSubset: async (options, write) => {
|
|
324
|
-
// Build the query with optional where, orderBy, and
|
|
361
|
+
// Build the query with optional where, orderBy, limit, and offset
|
|
325
362
|
// Use $dynamic() to enable dynamic query building
|
|
326
363
|
let query = config.drizzle.select().from(table).$dynamic();
|
|
327
364
|
|
|
328
|
-
//
|
|
365
|
+
// Combine where with cursor expressions if present
|
|
366
|
+
// The cursor.whereFrom gives us rows after the cursor position
|
|
329
367
|
let hasWhere = false;
|
|
330
|
-
if (options.where) {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
368
|
+
if (options.where || options.cursor?.whereFrom) {
|
|
369
|
+
let drizzleWhere: SQL | undefined;
|
|
370
|
+
|
|
371
|
+
if (options.where && options.cursor?.whereFrom) {
|
|
372
|
+
// Combine main where with cursor expression using AND
|
|
373
|
+
const mainWhere = convertBasicExpressionToDrizzle(
|
|
374
|
+
options.where,
|
|
375
|
+
table,
|
|
376
|
+
);
|
|
377
|
+
const cursorWhere = convertBasicExpressionToDrizzle(
|
|
378
|
+
options.cursor.whereFrom,
|
|
379
|
+
table,
|
|
380
|
+
);
|
|
381
|
+
drizzleWhere = and(mainWhere, cursorWhere);
|
|
382
|
+
} else if (options.where) {
|
|
383
|
+
drizzleWhere = convertBasicExpressionToDrizzle(options.where, table);
|
|
384
|
+
} else if (options.cursor?.whereFrom) {
|
|
385
|
+
drizzleWhere = convertBasicExpressionToDrizzle(
|
|
386
|
+
options.cursor.whereFrom,
|
|
387
|
+
table,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (drizzleWhere) {
|
|
392
|
+
query = query.where(drizzleWhere);
|
|
393
|
+
hasWhere = true;
|
|
394
|
+
}
|
|
337
395
|
}
|
|
338
396
|
|
|
339
397
|
if (options.orderBy) {
|
|
@@ -345,6 +403,11 @@ export function sqliteCollectionOptions<
|
|
|
345
403
|
query = query.limit(options.limit);
|
|
346
404
|
}
|
|
347
405
|
|
|
406
|
+
// Apply offset for offset-based pagination
|
|
407
|
+
if (options.offset !== undefined && options.offset > 0) {
|
|
408
|
+
query = query.offset(options.offset);
|
|
409
|
+
}
|
|
410
|
+
|
|
348
411
|
const items = (await query) as unknown as InferSchemaOutput<
|
|
349
412
|
SelectSchema<TTable>
|
|
350
413
|
>[];
|
|
@@ -358,6 +421,12 @@ export function sqliteCollectionOptions<
|
|
|
358
421
|
if (options.limit !== undefined) {
|
|
359
422
|
contextParts.push(`limit ${options.limit}`);
|
|
360
423
|
}
|
|
424
|
+
if (options.offset !== undefined && options.offset > 0) {
|
|
425
|
+
contextParts.push(`offset ${options.offset}`);
|
|
426
|
+
}
|
|
427
|
+
if (options.cursor) {
|
|
428
|
+
contextParts.push("with cursor pagination");
|
|
429
|
+
}
|
|
361
430
|
|
|
362
431
|
if (hasWhere) {
|
|
363
432
|
config.interceptor.onOperation({
|
|
@@ -393,6 +462,9 @@ export function sqliteCollectionOptions<
|
|
|
393
462
|
if (options.limit !== undefined) {
|
|
394
463
|
contextParts.push(`limit ${options.limit}`);
|
|
395
464
|
}
|
|
465
|
+
if (options.offset !== undefined && options.offset > 0) {
|
|
466
|
+
contextParts.push(`offset ${options.offset}`);
|
|
467
|
+
}
|
|
396
468
|
|
|
397
469
|
config.interceptor.onOperation({
|
|
398
470
|
type: "write",
|
|
@@ -544,21 +616,29 @@ export function sqliteCollectionOptions<
|
|
|
544
616
|
onInsert: config.debug
|
|
545
617
|
? async (params) => {
|
|
546
618
|
console.log("onInsert", params);
|
|
619
|
+
// Call the actual handler from syncResult (always defined in createSyncFunction)
|
|
620
|
+
// biome-ignore lint/style/noNonNullAssertion: onInsert is always defined in SyncFunctionResult
|
|
621
|
+
await syncResult.onInsert!(params);
|
|
547
622
|
}
|
|
548
623
|
: undefined,
|
|
549
624
|
onUpdate: config.debug
|
|
550
625
|
? async (params) => {
|
|
551
626
|
console.log("onUpdate", params);
|
|
627
|
+
// Call the actual handler from syncResult (always defined in createSyncFunction)
|
|
628
|
+
// biome-ignore lint/style/noNonNullAssertion: onUpdate is always defined in SyncFunctionResult
|
|
629
|
+
await syncResult.onUpdate!(params);
|
|
552
630
|
}
|
|
553
631
|
: undefined,
|
|
554
632
|
onDelete: config.debug
|
|
555
633
|
? async (params) => {
|
|
556
634
|
console.log("onDelete", params);
|
|
635
|
+
// Call the actual handler from syncResult (always defined in createSyncFunction)
|
|
636
|
+
// biome-ignore lint/style/noNonNullAssertion: onDelete is always defined in SyncFunctionResult
|
|
637
|
+
await syncResult.onDelete!(params);
|
|
557
638
|
}
|
|
558
639
|
: undefined,
|
|
559
640
|
syncMode: config.syncMode,
|
|
560
641
|
});
|
|
561
642
|
|
|
562
|
-
|
|
563
|
-
return collectionConfig as any;
|
|
643
|
+
return collectionConfig;
|
|
564
644
|
}
|
package/src/drizzle/worker.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { DrizzleConfig } from "drizzle-orm";
|
|
2
2
|
import { drizzle as drizzleSqliteProxy } from "drizzle-orm/sqlite-proxy";
|
|
3
3
|
import type { ISqliteWorkerClient } from "../worker/client";
|
|
4
|
+
import type {
|
|
5
|
+
SQLInterceptor,
|
|
6
|
+
SQLOperation,
|
|
7
|
+
} from "../collections/sqlite-collection";
|
|
4
8
|
|
|
5
9
|
export const drizzleSqliteWasmWorker = <
|
|
6
10
|
TSchema extends Record<string, unknown> = Record<string, never>,
|
|
@@ -22,3 +26,139 @@ export const drizzleSqliteWasmWorker = <
|
|
|
22
26
|
});
|
|
23
27
|
}, config);
|
|
24
28
|
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Creates an instrumented Drizzle instance that logs all SQL queries.
|
|
32
|
+
* This wraps the standard drizzleSqliteWasmWorker to intercept every query.
|
|
33
|
+
*/
|
|
34
|
+
export const createInstrumentedDrizzle = <
|
|
35
|
+
TSchema extends Record<string, unknown> = Record<string, never>,
|
|
36
|
+
>(
|
|
37
|
+
client: ISqliteWorkerClient,
|
|
38
|
+
config: DrizzleConfig<TSchema> = {},
|
|
39
|
+
interceptor?: SQLInterceptor,
|
|
40
|
+
) => {
|
|
41
|
+
return drizzleSqliteProxy<TSchema>(async (sql, params, method) => {
|
|
42
|
+
const startTime = Date.now();
|
|
43
|
+
|
|
44
|
+
const result = await new Promise<{ rows: unknown[] }>((resolve, reject) => {
|
|
45
|
+
client.performRemoteCallback(
|
|
46
|
+
{
|
|
47
|
+
sql,
|
|
48
|
+
params,
|
|
49
|
+
method,
|
|
50
|
+
},
|
|
51
|
+
resolve,
|
|
52
|
+
reject,
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Log the operation if interceptor is provided
|
|
57
|
+
if (interceptor?.onOperation) {
|
|
58
|
+
// Parse SQL to determine context
|
|
59
|
+
const sqlLower = sql.toLowerCase().trim();
|
|
60
|
+
let context = "Direct Drizzle query";
|
|
61
|
+
|
|
62
|
+
if (sqlLower.startsWith("select")) {
|
|
63
|
+
// Extract table name from SELECT query
|
|
64
|
+
const fromMatch = sql.match(/from\s+["']?(\w+)["']?/i);
|
|
65
|
+
const tableName = fromMatch?.[1] || "unknown";
|
|
66
|
+
|
|
67
|
+
// Check for LIMIT/OFFSET - handle both literal values and ? placeholders
|
|
68
|
+
// Drizzle uses parameterized queries, so we need to check for `limit ?` style
|
|
69
|
+
const hasLimit = /limit\s+(\d+|\?|\$\d+)/i.test(sql);
|
|
70
|
+
const hasOffset = /offset\s+(\d+|\?|\$\d+)/i.test(sql);
|
|
71
|
+
const hasOrderBy = /order\s+by/i.test(sql);
|
|
72
|
+
|
|
73
|
+
if (hasLimit || hasOffset) {
|
|
74
|
+
// Extract actual values from params if using placeholders
|
|
75
|
+
// Drizzle typically puts LIMIT as second-to-last param, OFFSET as last
|
|
76
|
+
let limitVal = "?";
|
|
77
|
+
let offsetVal = "0";
|
|
78
|
+
|
|
79
|
+
// Try to extract literal values first
|
|
80
|
+
const literalLimit = sql.match(/limit\s+(\d+)/i);
|
|
81
|
+
const literalOffset = sql.match(/offset\s+(\d+)/i);
|
|
82
|
+
|
|
83
|
+
if (literalLimit) {
|
|
84
|
+
limitVal = literalLimit[1];
|
|
85
|
+
} else if (params && params.length > 0) {
|
|
86
|
+
// For parameterized queries, try to infer from params
|
|
87
|
+
// LIMIT/OFFSET are usually at the end
|
|
88
|
+
if (hasLimit && hasOffset && params.length >= 2) {
|
|
89
|
+
limitVal = String(params[params.length - 2]);
|
|
90
|
+
offsetVal = String(params[params.length - 1]);
|
|
91
|
+
} else if (hasLimit && params.length >= 1) {
|
|
92
|
+
limitVal = String(params[params.length - 1]);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (literalOffset) {
|
|
97
|
+
offsetVal = literalOffset[1];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
context = `SELECT with LIMIT ${limitVal} OFFSET ${offsetVal}`;
|
|
101
|
+
} else if (hasOrderBy) {
|
|
102
|
+
context = `SELECT all from ${tableName} (ordered)`;
|
|
103
|
+
} else {
|
|
104
|
+
context = `SELECT all from ${tableName}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const operation: SQLOperation = {
|
|
108
|
+
type: "raw-query",
|
|
109
|
+
sql,
|
|
110
|
+
params: params as unknown[],
|
|
111
|
+
method,
|
|
112
|
+
rowCount: result.rows?.length ?? 0,
|
|
113
|
+
context,
|
|
114
|
+
timestamp: startTime,
|
|
115
|
+
};
|
|
116
|
+
interceptor.onOperation(operation);
|
|
117
|
+
} else if (sqlLower.startsWith("insert")) {
|
|
118
|
+
const intoMatch = sql.match(/into\s+["']?(\w+)["']?/i);
|
|
119
|
+
context = `INSERT into ${intoMatch?.[1] || "unknown"}`;
|
|
120
|
+
|
|
121
|
+
const operation: SQLOperation = {
|
|
122
|
+
type: "raw-query",
|
|
123
|
+
sql,
|
|
124
|
+
params: params as unknown[],
|
|
125
|
+
method,
|
|
126
|
+
rowCount: 0,
|
|
127
|
+
context,
|
|
128
|
+
timestamp: startTime,
|
|
129
|
+
};
|
|
130
|
+
interceptor.onOperation(operation);
|
|
131
|
+
} else if (sqlLower.startsWith("update")) {
|
|
132
|
+
const tableMatch = sql.match(/update\s+["']?(\w+)["']?/i);
|
|
133
|
+
context = `UPDATE ${tableMatch?.[1] || "unknown"}`;
|
|
134
|
+
|
|
135
|
+
const operation: SQLOperation = {
|
|
136
|
+
type: "raw-query",
|
|
137
|
+
sql,
|
|
138
|
+
params: params as unknown[],
|
|
139
|
+
method,
|
|
140
|
+
rowCount: 0,
|
|
141
|
+
context,
|
|
142
|
+
timestamp: startTime,
|
|
143
|
+
};
|
|
144
|
+
interceptor.onOperation(operation);
|
|
145
|
+
} else if (sqlLower.startsWith("delete")) {
|
|
146
|
+
const fromMatch = sql.match(/from\s+["']?(\w+)["']?/i);
|
|
147
|
+
context = `DELETE from ${fromMatch?.[1] || "unknown"}`;
|
|
148
|
+
|
|
149
|
+
const operation: SQLOperation = {
|
|
150
|
+
type: "raw-query",
|
|
151
|
+
sql,
|
|
152
|
+
params: params as unknown[],
|
|
153
|
+
method,
|
|
154
|
+
rowCount: 0,
|
|
155
|
+
context,
|
|
156
|
+
timestamp: startTime,
|
|
157
|
+
};
|
|
158
|
+
interceptor.onOperation(operation);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return result;
|
|
163
|
+
}, config);
|
|
164
|
+
};
|
|
@@ -3,12 +3,16 @@ import {
|
|
|
3
3
|
customSqliteMigrate,
|
|
4
4
|
type DurableSqliteMigrationConfig,
|
|
5
5
|
} from "@firtoz/drizzle-sqlite-wasm/sqlite-wasm-migrator";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
drizzleSqliteWasmWorker,
|
|
8
|
+
createInstrumentedDrizzle,
|
|
9
|
+
} from "@firtoz/drizzle-sqlite-wasm/drizzle-sqlite-wasm-worker";
|
|
7
10
|
import type { ISqliteWorkerClient } from "../worker/manager";
|
|
8
11
|
import {
|
|
9
12
|
initializeSqliteWorker,
|
|
10
13
|
isSqliteWorkerInitialized,
|
|
11
14
|
} from "../worker/global-manager";
|
|
15
|
+
import type { SQLInterceptor } from "../collections/sqlite-collection";
|
|
12
16
|
|
|
13
17
|
export const useDrizzleSqliteDb = <TSchema extends Record<string, unknown>>(
|
|
14
18
|
WorkerConstructor: new () => Worker,
|
|
@@ -16,6 +20,8 @@ export const useDrizzleSqliteDb = <TSchema extends Record<string, unknown>>(
|
|
|
16
20
|
schema: TSchema,
|
|
17
21
|
migrations: DurableSqliteMigrationConfig,
|
|
18
22
|
debug?: boolean,
|
|
23
|
+
/** Optional interceptor to log ALL SQL queries (including direct Drizzle queries) */
|
|
24
|
+
interceptor?: SQLInterceptor,
|
|
19
25
|
) => {
|
|
20
26
|
const resolveRef = useRef<null | (() => void)>(null);
|
|
21
27
|
const rejectRef = useRef<null | ((error: unknown) => void)>(null);
|
|
@@ -72,45 +78,66 @@ export const useDrizzleSqliteDb = <TSchema extends Record<string, unknown>>(
|
|
|
72
78
|
};
|
|
73
79
|
}, [dbName, WorkerConstructor]);
|
|
74
80
|
|
|
81
|
+
// Store interceptor in a ref to avoid recreating drizzle on interceptor changes
|
|
82
|
+
const interceptorRef = useRef(interceptor);
|
|
83
|
+
interceptorRef.current = interceptor;
|
|
84
|
+
|
|
75
85
|
// Create drizzle instance with a callback-based approach that waits for the client
|
|
86
|
+
// Use instrumented version if interceptor is provided to log ALL queries
|
|
76
87
|
const drizzle = useMemo(() => {
|
|
77
88
|
if (debug) {
|
|
78
89
|
console.log(`[DEBUG] ${dbName} - creating drizzle proxy wrapper`);
|
|
79
90
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
client.onStarted(callback);
|
|
106
|
-
},
|
|
107
|
-
terminate: () => {
|
|
108
|
-
sqliteClientRef.current?.terminate();
|
|
109
|
-
},
|
|
91
|
+
|
|
92
|
+
const client: ISqliteWorkerClient = {
|
|
93
|
+
performRemoteCallback: (data, resolve, reject) => {
|
|
94
|
+
const actualClient = sqliteClientRef.current;
|
|
95
|
+
if (!actualClient) {
|
|
96
|
+
console.error(
|
|
97
|
+
`[DEBUG] ${dbName} - performRemoteCallback called but no sqliteClient yet`,
|
|
98
|
+
);
|
|
99
|
+
reject(
|
|
100
|
+
new Error(`Database ${dbName} not ready yet - still initializing`),
|
|
101
|
+
);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
actualClient.performRemoteCallback(data, resolve, reject);
|
|
105
|
+
},
|
|
106
|
+
onStarted: (callback) => {
|
|
107
|
+
const actualClient = sqliteClientRef.current;
|
|
108
|
+
if (!actualClient) {
|
|
109
|
+
console.warn(
|
|
110
|
+
`[DEBUG] ${dbName} - onStarted called but no sqliteClient yet`,
|
|
111
|
+
);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
actualClient.onStarted(callback);
|
|
110
115
|
},
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
116
|
+
terminate: () => {
|
|
117
|
+
sqliteClientRef.current?.terminate();
|
|
118
|
+
},
|
|
119
|
+
checkpoint: () => {
|
|
120
|
+
return sqliteClientRef.current?.checkpoint() ?? Promise.resolve();
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Use instrumented version if interceptor is provided
|
|
125
|
+
// Use a wrapper that accesses the ref so interceptor changes don't recreate drizzle
|
|
126
|
+
const interceptorWrapper: SQLInterceptor = {
|
|
127
|
+
onOperation: (op) => interceptorRef.current?.onOperation?.(op),
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Always use instrumented if initial interceptor was provided
|
|
131
|
+
if (interceptor) {
|
|
132
|
+
return createInstrumentedDrizzle<TSchema>(
|
|
133
|
+
client,
|
|
134
|
+
{ schema },
|
|
135
|
+
interceptorWrapper,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return drizzleSqliteWasmWorker<TSchema>(client, { schema });
|
|
140
|
+
}, [schema, dbName, !!interceptor]); // Only recreate if interceptor presence changes, not on every render
|
|
114
141
|
|
|
115
142
|
useEffect(() => {
|
|
116
143
|
if (!sqliteClient) {
|