@firtoz/drizzle-utils 1.0.0 → 1.0.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 +15 -0
- package/package.json +6 -6
- package/src/collection-utils.ts +42 -336
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @firtoz/drizzle-utils
|
|
2
2
|
|
|
3
|
+
## 1.0.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`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`.
|
|
8
|
+
|
|
9
|
+
## 1.0.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [`5c667ec`](https://github.com/firtoz/fullstack-toolkit/commit/5c667ecfce1ed4f22ccf9686ad37f00e7a4ecee3) Thanks [@firtoz](https://github.com/firtoz)! - Refactor `SyncBackend`, `createSyncFunction`, and `createCollectionConfig` to delegate to generic (Drizzle-free) implementations from `@firtoz/db-helpers`. No public API changes.
|
|
14
|
+
|
|
15
|
+
- Updated dependencies [[`5c667ec`](https://github.com/firtoz/fullstack-toolkit/commit/5c667ecfce1ed4f22ccf9686ad37f00e7a4ecee3), [`5c667ec`](https://github.com/firtoz/fullstack-toolkit/commit/5c667ecfce1ed4f22ccf9686ad37f00e7a4ecee3)]:
|
|
16
|
+
- @firtoz/db-helpers@2.0.0
|
|
17
|
+
|
|
3
18
|
## 1.0.0
|
|
4
19
|
|
|
5
20
|
### Major Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firtoz/drizzle-utils",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Shared utilities and types for Drizzle-based packages",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"module": "./src/index.ts",
|
|
@@ -53,19 +53,19 @@
|
|
|
53
53
|
"access": "public"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@tanstack/db": ">=0.5.
|
|
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
|
-
"@tanstack/db": "^0.5.
|
|
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
|
-
"@firtoz/db-helpers": "^
|
|
68
|
+
"@firtoz/db-helpers": "^2.0.0",
|
|
69
69
|
"@firtoz/maybe-error": "^1.5.2"
|
|
70
70
|
}
|
|
71
71
|
}
|
package/src/collection-utils.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import type { CollectionUtils
|
|
2
|
-
import {
|
|
1
|
+
import type { CollectionUtils } from "@firtoz/db-helpers";
|
|
2
|
+
import {
|
|
3
|
+
type GenericBaseSyncConfig,
|
|
4
|
+
type GenericSyncBackend,
|
|
5
|
+
type GenericSyncFunctionResult,
|
|
6
|
+
createGenericSyncFunction,
|
|
7
|
+
createGenericCollectionConfig,
|
|
8
|
+
USE_DEDUPE as _USE_DEDUPE,
|
|
9
|
+
} from "@firtoz/db-helpers";
|
|
3
10
|
import { type Table, SQL, getTableColumns } from "drizzle-orm";
|
|
4
11
|
import type { BuildSchema } from "drizzle-valibot";
|
|
5
12
|
import { createInsertSchema } from "drizzle-valibot";
|
|
@@ -9,12 +16,8 @@ import type {
|
|
|
9
16
|
UtilsRecord,
|
|
10
17
|
CollectionConfig,
|
|
11
18
|
InferSchemaOutput,
|
|
12
|
-
SyncConfig,
|
|
13
|
-
SyncConfigRes,
|
|
14
19
|
SyncMode,
|
|
15
|
-
LoadSubsetOptions,
|
|
16
20
|
} from "@tanstack/db";
|
|
17
|
-
import { DeduplicatedLoadSubset } from "@tanstack/db";
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
23
|
* Utility type for branded IDs
|
|
@@ -89,339 +92,41 @@ export type InferCollectionFromTable<TTable extends Table> = Collection<
|
|
|
89
92
|
}
|
|
90
93
|
>;
|
|
91
94
|
|
|
92
|
-
|
|
93
|
-
// creates invalid expressions like not(or(isNull(...), not(isNull(...))))
|
|
94
|
-
// See: https://github.com/TanStack/db/issues/828
|
|
95
|
-
// TODO: Re-enable once the bug is fixed
|
|
96
|
-
export const USE_DEDUPE = false as boolean;
|
|
95
|
+
export const USE_DEDUPE = _USE_DEDUPE;
|
|
97
96
|
|
|
98
97
|
/**
|
|
99
|
-
* Base configuration for sync lifecycle management
|
|
98
|
+
* Base configuration for sync lifecycle management.
|
|
99
|
+
* Extends the generic (Drizzle-free) config with a Drizzle table reference.
|
|
100
100
|
*/
|
|
101
|
-
export interface BaseSyncConfig<TTable extends Table>
|
|
102
|
-
|
|
103
|
-
* The Drizzle table definition
|
|
104
|
-
*/
|
|
101
|
+
export interface BaseSyncConfig<TTable extends Table>
|
|
102
|
+
extends GenericBaseSyncConfig {
|
|
105
103
|
table: TTable;
|
|
106
|
-
/**
|
|
107
|
-
* Promise that resolves when the database is ready
|
|
108
|
-
*/
|
|
109
|
-
readyPromise: Promise<void>;
|
|
110
|
-
/**
|
|
111
|
-
* Sync mode: 'eager' (immediate) or 'lazy' (on-demand)
|
|
112
|
-
*/
|
|
113
|
-
syncMode?: SyncMode;
|
|
114
|
-
/**
|
|
115
|
-
* Enable debug logging
|
|
116
|
-
*/
|
|
117
|
-
debug?: boolean;
|
|
118
104
|
}
|
|
119
105
|
|
|
120
106
|
/**
|
|
121
|
-
* Backend-specific implementations required for sync
|
|
107
|
+
* Backend-specific implementations required for sync.
|
|
108
|
+
* Drizzle-typed alias for GenericSyncBackend.
|
|
122
109
|
*/
|
|
123
|
-
export
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
*/
|
|
127
|
-
initialLoad: () => Promise<Array<InferSchemaOutput<SelectSchema<TTable>>>>;
|
|
128
|
-
/**
|
|
129
|
-
* Load a subset of data based on query options
|
|
130
|
-
*/
|
|
131
|
-
loadSubset: (
|
|
132
|
-
options: LoadSubsetOptions,
|
|
133
|
-
) => Promise<Array<InferSchemaOutput<SelectSchema<TTable>>>>;
|
|
134
|
-
/**
|
|
135
|
-
* Handle insert mutations
|
|
136
|
-
*/
|
|
137
|
-
handleInsert: (
|
|
138
|
-
items: Array<InferSchemaOutput<SelectSchema<TTable>>>,
|
|
139
|
-
) => Promise<Array<InferSchemaOutput<SelectSchema<TTable>>>>;
|
|
140
|
-
/**
|
|
141
|
-
* Handle update mutations
|
|
142
|
-
*/
|
|
143
|
-
handleUpdate: (
|
|
144
|
-
mutations: Array<{
|
|
145
|
-
key: string;
|
|
146
|
-
changes: Partial<InferSchemaOutput<SelectSchema<TTable>>>;
|
|
147
|
-
original: InferSchemaOutput<SelectSchema<TTable>>;
|
|
148
|
-
}>,
|
|
149
|
-
) => Promise<Array<InferSchemaOutput<SelectSchema<TTable>>>>;
|
|
150
|
-
/**
|
|
151
|
-
* Handle delete mutations
|
|
152
|
-
*/
|
|
153
|
-
handleDelete: (
|
|
154
|
-
mutations: Array<{
|
|
155
|
-
key: string;
|
|
156
|
-
modified: InferSchemaOutput<SelectSchema<TTable>>;
|
|
157
|
-
original: InferSchemaOutput<SelectSchema<TTable>>;
|
|
158
|
-
}>,
|
|
159
|
-
) => Promise<void>;
|
|
160
|
-
/**
|
|
161
|
-
* Handle truncate (clear all data from store)
|
|
162
|
-
* Optional - if not provided, truncate util won't be available
|
|
163
|
-
*/
|
|
164
|
-
handleTruncate?: () => Promise<void>;
|
|
165
|
-
}
|
|
110
|
+
export type SyncBackend<TTable extends Table> = GenericSyncBackend<
|
|
111
|
+
InferSchemaOutput<SelectSchema<TTable>>
|
|
112
|
+
>;
|
|
166
113
|
|
|
167
114
|
/**
|
|
168
|
-
* Return type for createSyncFunction
|
|
115
|
+
* Return type for createSyncFunction.
|
|
116
|
+
* Drizzle-typed alias for GenericSyncFunctionResult.
|
|
169
117
|
*/
|
|
170
|
-
export type SyncFunctionResult<TTable extends Table> =
|
|
171
|
-
|
|
172
|
-
onInsert: CollectionConfig<
|
|
173
|
-
InferSchemaOutput<SelectSchema<TTable>>,
|
|
174
|
-
string,
|
|
175
|
-
// biome-ignore lint/suspicious/noExplicitAny: Schema type parameter needs to be flexible
|
|
176
|
-
any
|
|
177
|
-
>["onInsert"];
|
|
178
|
-
onUpdate: CollectionConfig<
|
|
179
|
-
InferSchemaOutput<SelectSchema<TTable>>,
|
|
180
|
-
string,
|
|
181
|
-
// biome-ignore lint/suspicious/noExplicitAny: Schema type parameter needs to be flexible
|
|
182
|
-
any
|
|
183
|
-
>["onUpdate"];
|
|
184
|
-
onDelete: CollectionConfig<
|
|
185
|
-
InferSchemaOutput<SelectSchema<TTable>>,
|
|
186
|
-
string,
|
|
187
|
-
// biome-ignore lint/suspicious/noExplicitAny: Schema type parameter needs to be flexible
|
|
188
|
-
any
|
|
189
|
-
>["onDelete"];
|
|
190
|
-
/**
|
|
191
|
-
* Collection utilities including truncate and external sync
|
|
192
|
-
*/
|
|
193
|
-
utils: CollectionUtils<InferSchemaOutput<SelectSchema<TTable>>>;
|
|
194
|
-
};
|
|
118
|
+
export type SyncFunctionResult<TTable extends Table> =
|
|
119
|
+
GenericSyncFunctionResult<InferSchemaOutput<SelectSchema<TTable>>>;
|
|
195
120
|
|
|
196
121
|
/**
|
|
197
|
-
* Creates the sync function with common lifecycle management
|
|
122
|
+
* Creates the sync function with common lifecycle management.
|
|
123
|
+
* Delegates to the generic (Drizzle-free) implementation in @firtoz/db-helpers.
|
|
198
124
|
*/
|
|
199
125
|
export function createSyncFunction<TTable extends Table>(
|
|
200
126
|
config: BaseSyncConfig<TTable>,
|
|
201
127
|
backend: SyncBackend<TTable>,
|
|
202
128
|
): SyncFunctionResult<TTable> {
|
|
203
|
-
|
|
204
|
-
type CollectionType = CollectionConfig<
|
|
205
|
-
ItemType,
|
|
206
|
-
string,
|
|
207
|
-
// biome-ignore lint/suspicious/noExplicitAny: Schema type parameter needs to be flexible
|
|
208
|
-
any
|
|
209
|
-
>;
|
|
210
|
-
|
|
211
|
-
let insertListener: CollectionType["onInsert"];
|
|
212
|
-
let updateListener: CollectionType["onUpdate"];
|
|
213
|
-
let deleteListener: CollectionType["onDelete"];
|
|
214
|
-
|
|
215
|
-
// Captured sync functions for external sync
|
|
216
|
-
let syncBegin: (() => void) | null = null;
|
|
217
|
-
let syncWrite:
|
|
218
|
-
| ((op: { type: "insert" | "update" | "delete"; value: ItemType }) => void)
|
|
219
|
-
| null = null;
|
|
220
|
-
let syncCommit: (() => void) | null = null;
|
|
221
|
-
let syncTruncate: (() => void) | null = null;
|
|
222
|
-
|
|
223
|
-
const syncFn: SyncConfig<
|
|
224
|
-
InferSchemaOutput<SelectSchema<TTable>>,
|
|
225
|
-
string
|
|
226
|
-
>["sync"] = (params) => {
|
|
227
|
-
const { begin, write, commit, markReady, truncate } = params;
|
|
228
|
-
|
|
229
|
-
// Capture sync functions for external use
|
|
230
|
-
syncBegin = begin;
|
|
231
|
-
syncWrite = write;
|
|
232
|
-
syncCommit = commit;
|
|
233
|
-
syncTruncate = truncate;
|
|
234
|
-
|
|
235
|
-
const initialSync = async () => {
|
|
236
|
-
await config.readyPromise;
|
|
237
|
-
|
|
238
|
-
try {
|
|
239
|
-
const items = await backend.initialLoad();
|
|
240
|
-
|
|
241
|
-
begin();
|
|
242
|
-
|
|
243
|
-
for (const item of items) {
|
|
244
|
-
write({
|
|
245
|
-
type: "insert",
|
|
246
|
-
value: item,
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
commit();
|
|
251
|
-
} finally {
|
|
252
|
-
markReady();
|
|
253
|
-
}
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
if (config.syncMode === "eager" || !config.syncMode) {
|
|
257
|
-
initialSync();
|
|
258
|
-
} else {
|
|
259
|
-
markReady();
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
insertListener = async (params) => {
|
|
263
|
-
const results = await backend.handleInsert(
|
|
264
|
-
params.transaction.mutations.map((m) => m.modified),
|
|
265
|
-
);
|
|
266
|
-
|
|
267
|
-
begin();
|
|
268
|
-
for (const result of results) {
|
|
269
|
-
write({
|
|
270
|
-
type: "insert",
|
|
271
|
-
value: result,
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
commit();
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
updateListener = async (params) => {
|
|
278
|
-
const results = await backend.handleUpdate(params.transaction.mutations);
|
|
279
|
-
|
|
280
|
-
begin();
|
|
281
|
-
for (const result of results) {
|
|
282
|
-
write({
|
|
283
|
-
type: "update",
|
|
284
|
-
value: result,
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
commit();
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
deleteListener = async (params) => {
|
|
291
|
-
await backend.handleDelete(params.transaction.mutations);
|
|
292
|
-
|
|
293
|
-
begin();
|
|
294
|
-
for (const item of params.transaction.mutations) {
|
|
295
|
-
write({
|
|
296
|
-
type: "delete",
|
|
297
|
-
value: item.modified,
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
commit();
|
|
301
|
-
};
|
|
302
|
-
|
|
303
|
-
const loadSubset = async (options: LoadSubsetOptions) => {
|
|
304
|
-
await config.readyPromise;
|
|
305
|
-
|
|
306
|
-
const items = await backend.loadSubset(options);
|
|
307
|
-
|
|
308
|
-
begin();
|
|
309
|
-
|
|
310
|
-
for (const item of items) {
|
|
311
|
-
write({
|
|
312
|
-
type: "insert",
|
|
313
|
-
value: item,
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
commit();
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
// Create deduplicated loadSubset wrapper to avoid redundant queries
|
|
321
|
-
let loadSubsetDedupe: DeduplicatedLoadSubset | null = null;
|
|
322
|
-
if (USE_DEDUPE) {
|
|
323
|
-
loadSubsetDedupe = new DeduplicatedLoadSubset({
|
|
324
|
-
loadSubset,
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
return {
|
|
329
|
-
cleanup: () => {
|
|
330
|
-
insertListener = undefined;
|
|
331
|
-
updateListener = undefined;
|
|
332
|
-
deleteListener = undefined;
|
|
333
|
-
loadSubsetDedupe?.reset();
|
|
334
|
-
},
|
|
335
|
-
loadSubset: loadSubsetDedupe?.loadSubset ?? loadSubset,
|
|
336
|
-
} satisfies SyncConfigRes;
|
|
337
|
-
};
|
|
338
|
-
|
|
339
|
-
// Apply SyncMessage[] to the sync layer (canonical receive API)
|
|
340
|
-
const receiveSync = async (messages: SyncMessage<ItemType>[]) => {
|
|
341
|
-
if (messages.length === 0) return;
|
|
342
|
-
if (!syncBegin || !syncWrite || !syncCommit || !syncTruncate) {
|
|
343
|
-
if (config.debug) {
|
|
344
|
-
console.warn(
|
|
345
|
-
"[receiveSync] Sync functions not initialized yet - messages will be dropped",
|
|
346
|
-
messages.length,
|
|
347
|
-
);
|
|
348
|
-
}
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
syncBegin();
|
|
352
|
-
for (const msg of messages) {
|
|
353
|
-
switch (msg.type) {
|
|
354
|
-
case "insert":
|
|
355
|
-
syncWrite({ type: "insert", value: msg.value });
|
|
356
|
-
break;
|
|
357
|
-
case "update":
|
|
358
|
-
syncWrite({ type: "update", value: msg.value });
|
|
359
|
-
break;
|
|
360
|
-
case "delete":
|
|
361
|
-
syncWrite({
|
|
362
|
-
type: "delete",
|
|
363
|
-
value: { id: msg.key } as ItemType,
|
|
364
|
-
});
|
|
365
|
-
break;
|
|
366
|
-
case "truncate":
|
|
367
|
-
syncTruncate();
|
|
368
|
-
break;
|
|
369
|
-
default:
|
|
370
|
-
exhaustiveGuard(msg);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
syncCommit();
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
// Create utils with truncate and receiveSync
|
|
377
|
-
const utils: CollectionUtils<ItemType> = {
|
|
378
|
-
truncate: async () => {
|
|
379
|
-
if (!backend.handleTruncate) {
|
|
380
|
-
throw new Error("Truncate not supported by this backend");
|
|
381
|
-
}
|
|
382
|
-
if (!syncBegin || !syncTruncate || !syncCommit) {
|
|
383
|
-
throw new Error(
|
|
384
|
-
"Sync functions not initialized - sync function may not have been called yet",
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
// Clear the backend store
|
|
388
|
-
await backend.handleTruncate();
|
|
389
|
-
// Update local reactive store (same pattern as insert/update/delete listeners)
|
|
390
|
-
syncBegin();
|
|
391
|
-
syncTruncate();
|
|
392
|
-
syncCommit();
|
|
393
|
-
},
|
|
394
|
-
receiveSync,
|
|
395
|
-
};
|
|
396
|
-
|
|
397
|
-
return {
|
|
398
|
-
sync: syncFn,
|
|
399
|
-
onInsert: async (params) => {
|
|
400
|
-
if (!insertListener) {
|
|
401
|
-
throw new Error(
|
|
402
|
-
"insertListener not initialized - sync function may not have been called yet",
|
|
403
|
-
);
|
|
404
|
-
}
|
|
405
|
-
return insertListener(params);
|
|
406
|
-
},
|
|
407
|
-
onUpdate: async (params) => {
|
|
408
|
-
if (!updateListener) {
|
|
409
|
-
throw new Error(
|
|
410
|
-
"updateListener not initialized - sync function may not have been called yet",
|
|
411
|
-
);
|
|
412
|
-
}
|
|
413
|
-
return updateListener(params);
|
|
414
|
-
},
|
|
415
|
-
onDelete: async (params) => {
|
|
416
|
-
if (!deleteListener) {
|
|
417
|
-
throw new Error(
|
|
418
|
-
"deleteListener not initialized - sync function may not have been called yet",
|
|
419
|
-
);
|
|
420
|
-
}
|
|
421
|
-
return deleteListener(params);
|
|
422
|
-
},
|
|
423
|
-
utils,
|
|
424
|
-
};
|
|
129
|
+
return createGenericSyncFunction(config, backend);
|
|
425
130
|
}
|
|
426
131
|
|
|
427
132
|
/**
|
|
@@ -529,8 +234,8 @@ export function createGetKeyFunction<TTable extends Table>() {
|
|
|
529
234
|
}
|
|
530
235
|
|
|
531
236
|
/**
|
|
532
|
-
* Base collection config factory
|
|
533
|
-
*
|
|
237
|
+
* Base collection config factory.
|
|
238
|
+
* Delegates to the generic (Drizzle-free) implementation in @firtoz/db-helpers.
|
|
534
239
|
*/
|
|
535
240
|
export function createCollectionConfig<
|
|
536
241
|
TTable extends Table,
|
|
@@ -570,18 +275,19 @@ export function createCollectionConfig<
|
|
|
570
275
|
schema: TSchema;
|
|
571
276
|
utils: CollectionUtils<InferSchemaOutput<SelectSchema<TTable>>>;
|
|
572
277
|
} {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
utils: config.syncResult.utils,
|
|
278
|
+
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
|
+
> & {
|
|
288
|
+
schema: TSchema;
|
|
289
|
+
utils: CollectionUtils<TItem>;
|
|
586
290
|
};
|
|
291
|
+
|
|
292
|
+
return createGenericCollectionConfig<TItem, TSchema>(config) as ReturnType;
|
|
587
293
|
}
|