@firtoz/drizzle-sqlite-wasm 0.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 +230 -0
- package/README.md +602 -0
- package/package.json +89 -0
- package/src/collections/sqlite-collection.ts +532 -0
- package/src/collections/websocket-collection.ts +271 -0
- package/src/context/useDrizzleSqlite.ts +35 -0
- package/src/drizzle/direct.ts +27 -0
- package/src/drizzle/handle-callback.ts +113 -0
- package/src/drizzle/worker.ts +24 -0
- package/src/hooks/useDrizzleSqliteDb.ts +139 -0
- package/src/index.ts +32 -0
- package/src/migration/migrator.ts +148 -0
- package/src/worker/client.ts +11 -0
- package/src/worker/global-manager.ts +78 -0
- package/src/worker/manager.ts +339 -0
- package/src/worker/schema.ts +111 -0
- package/src/worker/sqlite.worker.ts +253 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
// const selectSchema = createSelectSchema(todoTable);
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
CollectionConfig,
|
|
5
|
+
InferSchemaOutput,
|
|
6
|
+
LoadSubsetOptions,
|
|
7
|
+
SyncConfig,
|
|
8
|
+
SyncConfigRes,
|
|
9
|
+
SyncMode,
|
|
10
|
+
} from "@tanstack/db";
|
|
11
|
+
import type { IR } from "@tanstack/db";
|
|
12
|
+
import { DeduplicatedLoadSubset } from "@tanstack/db";
|
|
13
|
+
import {
|
|
14
|
+
eq,
|
|
15
|
+
sql,
|
|
16
|
+
type Table,
|
|
17
|
+
gt,
|
|
18
|
+
gte,
|
|
19
|
+
lt,
|
|
20
|
+
lte,
|
|
21
|
+
ne,
|
|
22
|
+
and,
|
|
23
|
+
or,
|
|
24
|
+
not,
|
|
25
|
+
isNull,
|
|
26
|
+
isNotNull,
|
|
27
|
+
like,
|
|
28
|
+
inArray,
|
|
29
|
+
asc,
|
|
30
|
+
desc,
|
|
31
|
+
type SQL,
|
|
32
|
+
getTableColumns,
|
|
33
|
+
} from "drizzle-orm";
|
|
34
|
+
import type {
|
|
35
|
+
SQLiteUpdateSetSource,
|
|
36
|
+
BaseSQLiteDatabase,
|
|
37
|
+
SQLiteInsertValue,
|
|
38
|
+
} from "drizzle-orm/sqlite-core";
|
|
39
|
+
import { createInsertSchema } from "drizzle-valibot";
|
|
40
|
+
import * as v from "valibot";
|
|
41
|
+
import type {
|
|
42
|
+
SelectSchema,
|
|
43
|
+
TableWithRequiredFields,
|
|
44
|
+
} from "@firtoz/drizzle-utils";
|
|
45
|
+
|
|
46
|
+
// WORKAROUND: DeduplicatedLoadSubset has a bug where toggling queries (e.g., isNull/isNotNull)
|
|
47
|
+
// creates invalid expressions like not(or(isNull(...), not(isNull(...))))
|
|
48
|
+
// See: https://github.com/TanStack/db/issues/828
|
|
49
|
+
// TODO: Re-enable once the bug is fixed
|
|
50
|
+
const useDedupe = false as boolean;
|
|
51
|
+
|
|
52
|
+
export type AnyDrizzleDatabase = BaseSQLiteDatabase<
|
|
53
|
+
"async",
|
|
54
|
+
// biome-ignore lint/suspicious/noExplicitAny: We really want to use any here.
|
|
55
|
+
any,
|
|
56
|
+
Record<string, unknown>
|
|
57
|
+
>;
|
|
58
|
+
|
|
59
|
+
export type DrizzleSchema<TDrizzle extends AnyDrizzleDatabase> =
|
|
60
|
+
TDrizzle["_"]["fullSchema"];
|
|
61
|
+
|
|
62
|
+
export interface DrizzleCollectionConfig<
|
|
63
|
+
TDrizzle extends AnyDrizzleDatabase,
|
|
64
|
+
TTableName extends ValidTableNames<DrizzleSchema<TDrizzle>>,
|
|
65
|
+
> {
|
|
66
|
+
drizzle: TDrizzle;
|
|
67
|
+
tableName: ValidTableNames<DrizzleSchema<TDrizzle>> extends never
|
|
68
|
+
? {
|
|
69
|
+
$error: "The schema needs to include at least one table that uses the syncableTable function.";
|
|
70
|
+
}
|
|
71
|
+
: TTableName;
|
|
72
|
+
readyPromise: Promise<void>;
|
|
73
|
+
syncMode?: SyncMode;
|
|
74
|
+
/**
|
|
75
|
+
* Enable debug logging for query execution and mutations
|
|
76
|
+
*/
|
|
77
|
+
debug?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Optional callback to checkpoint the database after mutations
|
|
80
|
+
* This ensures WAL is flushed to the main database file for OPFS persistence
|
|
81
|
+
*/
|
|
82
|
+
checkpoint?: () => Promise<void>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type ValidTableNames<TSchema extends Record<string, unknown>> = {
|
|
86
|
+
[K in keyof TSchema]: TSchema[K] extends TableWithRequiredFields ? K : never;
|
|
87
|
+
}[keyof TSchema];
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Converts TanStack DB IR BasicExpression to Drizzle SQL expression
|
|
91
|
+
*/
|
|
92
|
+
function convertBasicExpressionToDrizzle<TTable extends Table>(
|
|
93
|
+
expression: IR.BasicExpression,
|
|
94
|
+
table: TTable,
|
|
95
|
+
): SQL {
|
|
96
|
+
if (expression.type === "ref") {
|
|
97
|
+
// PropRef - reference to a column
|
|
98
|
+
const propRef = expression as IR.PropRef;
|
|
99
|
+
const columnName = propRef.path[propRef.path.length - 1];
|
|
100
|
+
const column = table[columnName as keyof typeof table];
|
|
101
|
+
|
|
102
|
+
if (!column || typeof column !== "object" || !("_" in column)) {
|
|
103
|
+
throw new Error(`Column ${String(columnName)} not found in table`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Drizzle columns can be used directly in expressions
|
|
107
|
+
return column as unknown as SQL;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (expression.type === "val") {
|
|
111
|
+
// Value - literal value
|
|
112
|
+
const value = expression as IR.Value;
|
|
113
|
+
return sql`${value.value}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (expression.type === "func") {
|
|
117
|
+
// Func - function call like eq, gt, lt, etc.
|
|
118
|
+
const func = expression as IR.Func;
|
|
119
|
+
const args = func.args.map((arg) =>
|
|
120
|
+
convertBasicExpressionToDrizzle(arg, table),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
switch (func.name) {
|
|
124
|
+
case "eq":
|
|
125
|
+
return eq(args[0], args[1]);
|
|
126
|
+
case "ne":
|
|
127
|
+
return ne(args[0], args[1]);
|
|
128
|
+
case "gt":
|
|
129
|
+
return gt(args[0], args[1]);
|
|
130
|
+
case "gte":
|
|
131
|
+
return gte(args[0], args[1]);
|
|
132
|
+
case "lt":
|
|
133
|
+
return lt(args[0], args[1]);
|
|
134
|
+
case "lte":
|
|
135
|
+
return lte(args[0], args[1]);
|
|
136
|
+
case "and": {
|
|
137
|
+
const result = and(...args);
|
|
138
|
+
if (!result) {
|
|
139
|
+
throw new Error("Invalid 'and' expression - no arguments provided");
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
case "or": {
|
|
144
|
+
const result = or(...args);
|
|
145
|
+
if (!result) {
|
|
146
|
+
throw new Error("Invalid 'or' expression - no arguments provided");
|
|
147
|
+
}
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
case "not":
|
|
151
|
+
return not(args[0]);
|
|
152
|
+
case "isNull":
|
|
153
|
+
return isNull(args[0]);
|
|
154
|
+
case "isNotNull":
|
|
155
|
+
return isNotNull(args[0]);
|
|
156
|
+
case "like":
|
|
157
|
+
return like(args[0], args[1]);
|
|
158
|
+
case "in":
|
|
159
|
+
return inArray(args[0], args[1]);
|
|
160
|
+
case "isUndefined":
|
|
161
|
+
// isUndefined is same as isNull in SQLite
|
|
162
|
+
return isNull(args[0]);
|
|
163
|
+
default:
|
|
164
|
+
throw new Error(`Unsupported function: ${func.name}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
throw new Error(
|
|
169
|
+
`Unsupported expression type: ${(expression as { type: string }).type}`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Converts TanStack DB OrderBy to Drizzle orderBy
|
|
175
|
+
*/
|
|
176
|
+
function convertOrderByToDrizzle<TTable extends Table>(
|
|
177
|
+
orderBy: IR.OrderBy,
|
|
178
|
+
table: TTable,
|
|
179
|
+
): SQL[] {
|
|
180
|
+
return orderBy.map((clause) => {
|
|
181
|
+
const expression = convertBasicExpressionToDrizzle(
|
|
182
|
+
clause.expression,
|
|
183
|
+
table,
|
|
184
|
+
);
|
|
185
|
+
const direction = clause.compareOptions.direction || "asc";
|
|
186
|
+
|
|
187
|
+
return direction === "asc" ? asc(expression) : desc(expression);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function sqliteCollectionOptions<
|
|
192
|
+
const TDrizzle extends AnyDrizzleDatabase,
|
|
193
|
+
const TTableName extends string & ValidTableNames<DrizzleSchema<TDrizzle>>,
|
|
194
|
+
TTable extends DrizzleSchema<TDrizzle>[TTableName] & TableWithRequiredFields,
|
|
195
|
+
>(config: DrizzleCollectionConfig<TDrizzle, TTableName>) {
|
|
196
|
+
type CollectionType = CollectionConfig<
|
|
197
|
+
InferSchemaOutput<SelectSchema<TTable>>,
|
|
198
|
+
string,
|
|
199
|
+
// biome-ignore lint/suspicious/noExplicitAny: Schema type parameter needs to be flexible
|
|
200
|
+
any
|
|
201
|
+
>;
|
|
202
|
+
|
|
203
|
+
const tableName = config.tableName as string &
|
|
204
|
+
ValidTableNames<DrizzleSchema<TDrizzle>>;
|
|
205
|
+
|
|
206
|
+
const table = config.drizzle?._.fullSchema[tableName] as TTable;
|
|
207
|
+
|
|
208
|
+
let insertListener: CollectionType["onInsert"] | null = null;
|
|
209
|
+
let updateListener: CollectionType["onUpdate"] | null = null;
|
|
210
|
+
let deleteListener: CollectionType["onDelete"] | null = null;
|
|
211
|
+
|
|
212
|
+
// Transaction queue to serialize SQLite transactions (SQLite only supports one transaction at a time)
|
|
213
|
+
// The queue ensures transactions run sequentially and continues even if one fails
|
|
214
|
+
let transactionQueue = Promise.resolve();
|
|
215
|
+
const queueTransaction = <T>(fn: () => Promise<T>): Promise<T> => {
|
|
216
|
+
// Chain this transaction after the previous one (whether it succeeded or failed)
|
|
217
|
+
const result = transactionQueue.then(fn, fn);
|
|
218
|
+
// Update the queue to continue after this transaction completes (success or failure)
|
|
219
|
+
// This ensures the queue doesn't get stuck if a transaction fails
|
|
220
|
+
transactionQueue = result.then(
|
|
221
|
+
() => {}, // Success handler - return undefined to reset queue
|
|
222
|
+
() => {}, // Error handler - return undefined to reset queue (queue continues)
|
|
223
|
+
);
|
|
224
|
+
// Return the actual result so errors propagate to the caller
|
|
225
|
+
return result;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const sync: SyncConfig<
|
|
229
|
+
InferSchemaOutput<SelectSchema<TTable>>,
|
|
230
|
+
string
|
|
231
|
+
>["sync"] = (params) => {
|
|
232
|
+
const { begin, write, commit, markReady } = params;
|
|
233
|
+
|
|
234
|
+
const initialSync = async () => {
|
|
235
|
+
await config.readyPromise;
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
begin();
|
|
239
|
+
|
|
240
|
+
const items = (await config.drizzle
|
|
241
|
+
.select()
|
|
242
|
+
.from(table)) as unknown as InferSchemaOutput<SelectSchema<TTable>>[];
|
|
243
|
+
|
|
244
|
+
for (const item of items) {
|
|
245
|
+
write({
|
|
246
|
+
type: "insert",
|
|
247
|
+
value: item,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
commit();
|
|
252
|
+
} finally {
|
|
253
|
+
markReady();
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
if (config.syncMode === "eager" || !config.syncMode) {
|
|
258
|
+
initialSync();
|
|
259
|
+
} else {
|
|
260
|
+
markReady();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
insertListener = async (params) => {
|
|
264
|
+
// Store results to write after transaction succeeds
|
|
265
|
+
const results: Array<InferSchemaOutput<SelectSchema<TTable>>> = [];
|
|
266
|
+
|
|
267
|
+
// Queue the transaction to serialize SQLite operations
|
|
268
|
+
await queueTransaction(async () => {
|
|
269
|
+
await config.drizzle.transaction(async (tx) => {
|
|
270
|
+
for (const item of params.transaction.mutations) {
|
|
271
|
+
// TanStack DB applies schema transform (including ID default) before calling this listener
|
|
272
|
+
// So item.modified already has the ID from insertSchemaWithIdDefault
|
|
273
|
+
const itemToInsert = item.modified;
|
|
274
|
+
|
|
275
|
+
if (config.debug) {
|
|
276
|
+
console.log(
|
|
277
|
+
`[${new Date().toISOString()}] insertListener inserting`,
|
|
278
|
+
itemToInsert,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const result: Array<InferSchemaOutput<SelectSchema<TTable>>> =
|
|
283
|
+
(await tx
|
|
284
|
+
.insert(table)
|
|
285
|
+
.values(
|
|
286
|
+
itemToInsert as unknown as SQLiteInsertValue<typeof table>,
|
|
287
|
+
)
|
|
288
|
+
.returning()) as Array<InferSchemaOutput<SelectSchema<TTable>>>;
|
|
289
|
+
|
|
290
|
+
if (config.debug) {
|
|
291
|
+
console.log(
|
|
292
|
+
`[${new Date().toISOString()}] insertListener result`,
|
|
293
|
+
result,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (result.length > 0) {
|
|
298
|
+
results.push(result[0]);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Checkpoint to ensure WAL is flushed to main DB file
|
|
304
|
+
if (config.checkpoint) {
|
|
305
|
+
await config.checkpoint();
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// Only update reactive store after transaction succeeds
|
|
310
|
+
begin();
|
|
311
|
+
for (const result of results) {
|
|
312
|
+
write({
|
|
313
|
+
type: "insert",
|
|
314
|
+
value: result as unknown as InferSchemaOutput<SelectSchema<TTable>>,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
commit();
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
updateListener = async (params) => {
|
|
321
|
+
// Queue the transaction to serialize SQLite operations
|
|
322
|
+
const results: Array<InferSchemaOutput<SelectSchema<TTable>>> = [];
|
|
323
|
+
await queueTransaction(async () => {
|
|
324
|
+
await config.drizzle.transaction(async (tx) => {
|
|
325
|
+
for (const item of params.transaction.mutations) {
|
|
326
|
+
if (config.debug) {
|
|
327
|
+
console.log(
|
|
328
|
+
`[${new Date().toISOString()}] updateListener updating`,
|
|
329
|
+
item,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const updateTime = new Date();
|
|
334
|
+
const result: Array<InferSchemaOutput<SelectSchema<TTable>>> =
|
|
335
|
+
(await tx
|
|
336
|
+
.update(table)
|
|
337
|
+
.set({
|
|
338
|
+
...item.changes,
|
|
339
|
+
updatedAt: updateTime,
|
|
340
|
+
} as SQLiteUpdateSetSource<typeof table>)
|
|
341
|
+
.where(eq(table.id, item.key))
|
|
342
|
+
.returning()) as Array<InferSchemaOutput<SelectSchema<TTable>>>;
|
|
343
|
+
|
|
344
|
+
if (config.debug) {
|
|
345
|
+
console.log(
|
|
346
|
+
`[${new Date().toISOString()}] updateListener result`,
|
|
347
|
+
result,
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
results.push(...result);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// Checkpoint to ensure WAL is flushed to main DB file BEFORE UI updates
|
|
356
|
+
// This ensures persistence before the updateListener completes
|
|
357
|
+
if (config.checkpoint) {
|
|
358
|
+
await config.checkpoint();
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// Update the reactive store with actual database results
|
|
363
|
+
// This happens after checkpoint completes
|
|
364
|
+
begin();
|
|
365
|
+
for (const result of results) {
|
|
366
|
+
write({
|
|
367
|
+
type: "update",
|
|
368
|
+
value: result,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
commit();
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
deleteListener = async (params) => {
|
|
375
|
+
// Queue the transaction to serialize SQLite operations
|
|
376
|
+
await queueTransaction(async () => {
|
|
377
|
+
await config.drizzle.transaction(async (tx) => {
|
|
378
|
+
for (const item of params.transaction.mutations) {
|
|
379
|
+
await tx.delete(table).where(eq(table.id, item.key));
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// Checkpoint to ensure WAL is flushed to main DB file
|
|
384
|
+
if (config.checkpoint) {
|
|
385
|
+
await config.checkpoint();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
begin();
|
|
389
|
+
for (const item of params.transaction.mutations) {
|
|
390
|
+
if (config.debug) {
|
|
391
|
+
console.log(
|
|
392
|
+
`[${new Date().toISOString()}] deleteListener write`,
|
|
393
|
+
item,
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
write({
|
|
397
|
+
type: "delete",
|
|
398
|
+
value: item.modified,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
commit();
|
|
402
|
+
});
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const loadSubset = async (options: LoadSubsetOptions) => {
|
|
406
|
+
await config.readyPromise;
|
|
407
|
+
|
|
408
|
+
begin();
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
// Build the query with optional where, orderBy, and limit
|
|
412
|
+
// Use $dynamic() to enable dynamic query building
|
|
413
|
+
let query = config.drizzle.select().from(table).$dynamic();
|
|
414
|
+
|
|
415
|
+
// Convert TanStack DB IR expressions to Drizzle expressions
|
|
416
|
+
if (options.where) {
|
|
417
|
+
const drizzleWhere = convertBasicExpressionToDrizzle(
|
|
418
|
+
options.where,
|
|
419
|
+
table,
|
|
420
|
+
);
|
|
421
|
+
query = query.where(drizzleWhere);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (options.orderBy) {
|
|
425
|
+
const drizzleOrderBy = convertOrderByToDrizzle(
|
|
426
|
+
options.orderBy,
|
|
427
|
+
table,
|
|
428
|
+
);
|
|
429
|
+
query = query.orderBy(...drizzleOrderBy);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (options.limit !== undefined) {
|
|
433
|
+
query = query.limit(options.limit);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const items = (await query) as unknown as InferSchemaOutput<
|
|
437
|
+
SelectSchema<TTable>
|
|
438
|
+
>[];
|
|
439
|
+
|
|
440
|
+
for (const item of items) {
|
|
441
|
+
write({
|
|
442
|
+
type: "insert",
|
|
443
|
+
value: item,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
commit();
|
|
448
|
+
} catch (error) {
|
|
449
|
+
// If there's an error, we should still commit to maintain consistency
|
|
450
|
+
commit();
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
// Create deduplicated loadSubset wrapper to avoid redundant queries
|
|
456
|
+
let loadSubsetDedupe: DeduplicatedLoadSubset | null = null;
|
|
457
|
+
if (useDedupe) {
|
|
458
|
+
loadSubsetDedupe = new DeduplicatedLoadSubset({
|
|
459
|
+
loadSubset,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
cleanup: () => {
|
|
465
|
+
insertListener = null;
|
|
466
|
+
updateListener = null;
|
|
467
|
+
deleteListener = null;
|
|
468
|
+
loadSubsetDedupe?.reset();
|
|
469
|
+
},
|
|
470
|
+
loadSubset: loadSubsetDedupe?.loadSubset ?? loadSubset,
|
|
471
|
+
} satisfies SyncConfigRes;
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
// Create insert schema and augment it to apply ID default
|
|
475
|
+
// (Other defaults like createdAt/updatedAt are handled by SQLite)
|
|
476
|
+
const insertSchema = createInsertSchema(table);
|
|
477
|
+
const columns = getTableColumns(table);
|
|
478
|
+
const idColumn = columns.id;
|
|
479
|
+
|
|
480
|
+
const insertSchemaWithIdDefault = v.pipe(
|
|
481
|
+
insertSchema,
|
|
482
|
+
v.transform((input) => {
|
|
483
|
+
const result = { ...input } as Record<string, unknown>;
|
|
484
|
+
|
|
485
|
+
// Apply ID default if missing
|
|
486
|
+
if (result.id === undefined && idColumn?.defaultFn) {
|
|
487
|
+
result.id = idColumn.defaultFn();
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return result as typeof input;
|
|
491
|
+
}),
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
return {
|
|
495
|
+
schema: insertSchemaWithIdDefault,
|
|
496
|
+
getKey: (item: InferSchemaOutput<SelectSchema<TTable>>) => {
|
|
497
|
+
const id = (item as { id: string }).id;
|
|
498
|
+
return id;
|
|
499
|
+
},
|
|
500
|
+
sync: {
|
|
501
|
+
sync,
|
|
502
|
+
},
|
|
503
|
+
onInsert: async (
|
|
504
|
+
params: Parameters<NonNullable<CollectionType["onInsert"]>>[0],
|
|
505
|
+
) => {
|
|
506
|
+
if (config.debug) {
|
|
507
|
+
console.log("onInsert", params);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
await insertListener?.(params);
|
|
511
|
+
},
|
|
512
|
+
onUpdate: async (
|
|
513
|
+
params: Parameters<NonNullable<CollectionType["onUpdate"]>>[0],
|
|
514
|
+
) => {
|
|
515
|
+
if (config.debug) {
|
|
516
|
+
console.log("onUpdate", params);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
await updateListener?.(params);
|
|
520
|
+
},
|
|
521
|
+
onDelete: async (
|
|
522
|
+
params: Parameters<NonNullable<CollectionType["onDelete"]>>[0],
|
|
523
|
+
) => {
|
|
524
|
+
if (config.debug) {
|
|
525
|
+
console.log("onDelete", params);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
await deleteListener?.(params);
|
|
529
|
+
},
|
|
530
|
+
syncMode: config.syncMode,
|
|
531
|
+
} satisfies CollectionType;
|
|
532
|
+
}
|