@firtoz/drizzle-utils 0.3.3 → 1.0.1

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,29 @@
1
1
  # @firtoz/drizzle-utils
2
2
 
3
+ ## 1.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [`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.
8
+
9
+ - Updated dependencies [[`5c667ec`](https://github.com/firtoz/fullstack-toolkit/commit/5c667ecfce1ed4f22ccf9686ad37f00e7a4ecee3), [`5c667ec`](https://github.com/firtoz/fullstack-toolkit/commit/5c667ecfce1ed4f22ccf9686ad37f00e7a4ecee3)]:
10
+ - @firtoz/db-helpers@2.0.0
11
+
12
+ ## 1.0.0
13
+
14
+ ### Major Changes
15
+
16
+ - [`3c7ce1d`](https://github.com/firtoz/fullstack-toolkit/commit/3c7ce1dbca5c5396386db9927ae7f5e19a562cf6) Thanks [@firtoz](https://github.com/firtoz)! - **Unified collection sync**
17
+
18
+ `CollectionUtils` now exposes `receiveSync(messages: SyncMessage<T>[])` instead of `pushExternalSync`. Canonical sync message type is `SyncMessage<T, TKey>` (`insert` | `update` | `delete` | `truncate`). `ExternalSyncEvent` / `ExternalSyncHandler` are internal only.
19
+
20
+ **Upgrade:** Replace `utils.pushExternalSync(events)` with `utils.receiveSync(messages)`. Import `SyncMessage` from `@firtoz/drizzle-utils` (or from `@firtoz/db-helpers`) and send per-mutation messages: `{ type: "insert", value }`, `{ type: "update", value, previousValue }`, `{ type: "delete", key }`, or `{ type: "truncate" }`. Stop using `ExternalSyncEvent` / `ExternalSyncHandler` in your code; they are no longer part of the public API. Generic sync types (`SyncMessage`, `CollectionUtils`) are now defined in `@firtoz/db-helpers` and re-exported here for backward compatibility.
21
+
22
+ ### Patch Changes
23
+
24
+ - Updated dependencies [[`3c7ce1d`](https://github.com/firtoz/fullstack-toolkit/commit/3c7ce1dbca5c5396386db9927ae7f5e19a562cf6)]:
25
+ - @firtoz/db-helpers@1.0.0
26
+
3
27
  ## 0.3.3
4
28
 
5
29
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firtoz/drizzle-utils",
3
- "version": "0.3.3",
3
+ "version": "1.0.1",
4
4
  "description": "Shared utilities and types for Drizzle-based packages",
5
5
  "main": "./src/index.ts",
6
6
  "module": "./src/index.ts",
@@ -53,15 +53,19 @@
53
53
  "access": "public"
54
54
  },
55
55
  "peerDependencies": {
56
- "@tanstack/db": ">=0.5.25",
56
+ "@tanstack/db": ">=0.5.33",
57
57
  "drizzle-orm": ">=0.45.1",
58
58
  "drizzle-valibot": ">=0.4.0",
59
59
  "valibot": ">=1.0.0"
60
60
  },
61
61
  "devDependencies": {
62
- "@tanstack/db": "^0.5.25",
62
+ "@tanstack/db": "^0.5.33",
63
63
  "drizzle-orm": "^0.45.1",
64
64
  "drizzle-valibot": "^0.4.2",
65
65
  "valibot": "^1.2.0"
66
+ },
67
+ "dependencies": {
68
+ "@firtoz/db-helpers": "^2.0.0",
69
+ "@firtoz/maybe-error": "^1.5.2"
66
70
  }
67
71
  }
@@ -1,3 +1,12 @@
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";
1
10
  import { type Table, SQL, getTableColumns } from "drizzle-orm";
2
11
  import type { BuildSchema } from "drizzle-valibot";
3
12
  import { createInsertSchema } from "drizzle-valibot";
@@ -7,12 +16,8 @@ import type {
7
16
  UtilsRecord,
8
17
  CollectionConfig,
9
18
  InferSchemaOutput,
10
- SyncConfig,
11
- SyncConfigRes,
12
19
  SyncMode,
13
- LoadSubsetOptions,
14
20
  } from "@tanstack/db";
15
- import { DeduplicatedLoadSubset } from "@tanstack/db";
16
21
 
17
22
  /**
18
23
  * Utility type for branded IDs
@@ -87,374 +92,41 @@ export type InferCollectionFromTable<TTable extends Table> = Collection<
87
92
  }
88
93
  >;
89
94
 
90
- // WORKAROUND: DeduplicatedLoadSubset has a bug where toggling queries (e.g., isNull/isNotNull)
91
- // creates invalid expressions like not(or(isNull(...), not(isNull(...))))
92
- // See: https://github.com/TanStack/db/issues/828
93
- // TODO: Re-enable once the bug is fixed
94
- export const USE_DEDUPE = false as boolean;
95
+ export const USE_DEDUPE = _USE_DEDUPE;
95
96
 
96
97
  /**
97
- * 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.
98
100
  */
99
- export interface BaseSyncConfig<TTable extends Table> {
100
- /**
101
- * The Drizzle table definition
102
- */
101
+ export interface BaseSyncConfig<TTable extends Table>
102
+ extends GenericBaseSyncConfig {
103
103
  table: TTable;
104
- /**
105
- * Promise that resolves when the database is ready
106
- */
107
- readyPromise: Promise<void>;
108
- /**
109
- * Sync mode: 'eager' (immediate) or 'lazy' (on-demand)
110
- */
111
- syncMode?: SyncMode;
112
- /**
113
- * Enable debug logging
114
- */
115
- debug?: boolean;
116
- }
117
-
118
- /**
119
- * Backend-specific implementations required for sync
120
- */
121
- export interface SyncBackend<TTable extends Table> {
122
- /**
123
- * Initial data load - should call write() for each item
124
- */
125
- initialLoad: () => Promise<Array<InferSchemaOutput<SelectSchema<TTable>>>>;
126
- /**
127
- * Load a subset of data based on query options
128
- */
129
- loadSubset: (
130
- options: LoadSubsetOptions,
131
- ) => Promise<Array<InferSchemaOutput<SelectSchema<TTable>>>>;
132
- /**
133
- * Handle insert mutations
134
- */
135
- handleInsert: (
136
- items: Array<InferSchemaOutput<SelectSchema<TTable>>>,
137
- ) => Promise<Array<InferSchemaOutput<SelectSchema<TTable>>>>;
138
- /**
139
- * Handle update mutations
140
- */
141
- handleUpdate: (
142
- mutations: Array<{
143
- key: string;
144
- changes: Partial<InferSchemaOutput<SelectSchema<TTable>>>;
145
- original: InferSchemaOutput<SelectSchema<TTable>>;
146
- }>,
147
- ) => Promise<Array<InferSchemaOutput<SelectSchema<TTable>>>>;
148
- /**
149
- * Handle delete mutations
150
- */
151
- handleDelete: (
152
- mutations: Array<{
153
- key: string;
154
- modified: InferSchemaOutput<SelectSchema<TTable>>;
155
- original: InferSchemaOutput<SelectSchema<TTable>>;
156
- }>,
157
- ) => Promise<void>;
158
- /**
159
- * Handle truncate (clear all data from store)
160
- * Optional - if not provided, truncate util won't be available
161
- */
162
- handleTruncate?: () => Promise<void>;
163
104
  }
164
105
 
165
106
  /**
166
- * External sync event for pushing changes from outside (e.g., from a proxy server)
107
+ * Backend-specific implementations required for sync.
108
+ * Drizzle-typed alias for GenericSyncBackend.
167
109
  */
168
- export type ExternalSyncEvent<T> =
169
- | { type: "insert"; items: T[] }
170
- | { type: "update"; items: T[] }
171
- | { type: "delete"; items: T[] }
172
- | { type: "truncate" };
173
-
174
- /**
175
- * Handler for external sync events
176
- */
177
- export type ExternalSyncHandler<T> = (event: ExternalSyncEvent<T>) => void;
110
+ export type SyncBackend<TTable extends Table> = GenericSyncBackend<
111
+ InferSchemaOutput<SelectSchema<TTable>>
112
+ >;
178
113
 
179
114
  /**
180
- * Collection utils that include truncate and external sync functionality
115
+ * Return type for createSyncFunction.
116
+ * Drizzle-typed alias for GenericSyncFunctionResult.
181
117
  */
182
- export interface CollectionUtils<T = unknown> {
183
- /**
184
- * Clear all data from the store (truncate).
185
- * This clears the backend store and updates the local reactive store.
186
- */
187
- truncate: () => Promise<void>;
188
- /**
189
- * Push external sync events to the collection.
190
- * Use this when receiving sync messages from a proxy server or other external source.
191
- */
192
- pushExternalSync: ExternalSyncHandler<T>;
193
- }
118
+ export type SyncFunctionResult<TTable extends Table> =
119
+ GenericSyncFunctionResult<InferSchemaOutput<SelectSchema<TTable>>>;
194
120
 
195
121
  /**
196
- * Return type for createSyncFunction that includes both sync config and listeners
197
- */
198
- export type SyncFunctionResult<TTable extends Table> = {
199
- sync: SyncConfig<InferSchemaOutput<SelectSchema<TTable>>, string>["sync"];
200
- onInsert: CollectionConfig<
201
- InferSchemaOutput<SelectSchema<TTable>>,
202
- string,
203
- // biome-ignore lint/suspicious/noExplicitAny: Schema type parameter needs to be flexible
204
- any
205
- >["onInsert"];
206
- onUpdate: CollectionConfig<
207
- InferSchemaOutput<SelectSchema<TTable>>,
208
- string,
209
- // biome-ignore lint/suspicious/noExplicitAny: Schema type parameter needs to be flexible
210
- any
211
- >["onUpdate"];
212
- onDelete: CollectionConfig<
213
- InferSchemaOutput<SelectSchema<TTable>>,
214
- string,
215
- // biome-ignore lint/suspicious/noExplicitAny: Schema type parameter needs to be flexible
216
- any
217
- >["onDelete"];
218
- /**
219
- * Collection utilities including truncate and external sync
220
- */
221
- utils: CollectionUtils<InferSchemaOutput<SelectSchema<TTable>>>;
222
- };
223
-
224
- /**
225
- * 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.
226
124
  */
227
125
  export function createSyncFunction<TTable extends Table>(
228
126
  config: BaseSyncConfig<TTable>,
229
127
  backend: SyncBackend<TTable>,
230
128
  ): SyncFunctionResult<TTable> {
231
- type ItemType = InferSchemaOutput<SelectSchema<TTable>>;
232
- type CollectionType = CollectionConfig<
233
- ItemType,
234
- string,
235
- // biome-ignore lint/suspicious/noExplicitAny: Schema type parameter needs to be flexible
236
- any
237
- >;
238
-
239
- let insertListener: CollectionType["onInsert"];
240
- let updateListener: CollectionType["onUpdate"];
241
- let deleteListener: CollectionType["onDelete"];
242
-
243
- // Captured sync functions for external sync
244
- let syncBegin: (() => void) | null = null;
245
- let syncWrite:
246
- | ((op: { type: "insert" | "update" | "delete"; value: ItemType }) => void)
247
- | null = null;
248
- let syncCommit: (() => void) | null = null;
249
- let syncTruncate: (() => void) | null = null;
250
-
251
- const syncFn: SyncConfig<
252
- InferSchemaOutput<SelectSchema<TTable>>,
253
- string
254
- >["sync"] = (params) => {
255
- const { begin, write, commit, markReady, truncate } = params;
256
-
257
- // Capture sync functions for external use
258
- syncBegin = begin;
259
- syncWrite = write;
260
- syncCommit = commit;
261
- syncTruncate = truncate;
262
-
263
- const initialSync = async () => {
264
- await config.readyPromise;
265
-
266
- try {
267
- const items = await backend.initialLoad();
268
-
269
- begin();
270
-
271
- for (const item of items) {
272
- write({
273
- type: "insert",
274
- value: item,
275
- });
276
- }
277
-
278
- commit();
279
- } finally {
280
- markReady();
281
- }
282
- };
283
-
284
- if (config.syncMode === "eager" || !config.syncMode) {
285
- initialSync();
286
- } else {
287
- markReady();
288
- }
289
-
290
- insertListener = async (params) => {
291
- const results = await backend.handleInsert(
292
- params.transaction.mutations.map((m) => m.modified),
293
- );
294
-
295
- begin();
296
- for (const result of results) {
297
- write({
298
- type: "insert",
299
- value: result,
300
- });
301
- }
302
- commit();
303
- };
304
-
305
- updateListener = async (params) => {
306
- const results = await backend.handleUpdate(params.transaction.mutations);
307
-
308
- begin();
309
- for (const result of results) {
310
- write({
311
- type: "update",
312
- value: result,
313
- });
314
- }
315
- commit();
316
- };
317
-
318
- deleteListener = async (params) => {
319
- await backend.handleDelete(params.transaction.mutations);
320
-
321
- begin();
322
- for (const item of params.transaction.mutations) {
323
- write({
324
- type: "delete",
325
- value: item.modified,
326
- });
327
- }
328
- commit();
329
- };
330
-
331
- const loadSubset = async (options: LoadSubsetOptions) => {
332
- await config.readyPromise;
333
-
334
- const items = await backend.loadSubset(options);
335
-
336
- begin();
337
-
338
- for (const item of items) {
339
- write({
340
- type: "insert",
341
- value: item,
342
- });
343
- }
344
-
345
- commit();
346
- };
347
-
348
- // Create deduplicated loadSubset wrapper to avoid redundant queries
349
- let loadSubsetDedupe: DeduplicatedLoadSubset | null = null;
350
- if (USE_DEDUPE) {
351
- loadSubsetDedupe = new DeduplicatedLoadSubset({
352
- loadSubset,
353
- });
354
- }
355
-
356
- return {
357
- cleanup: () => {
358
- insertListener = undefined;
359
- updateListener = undefined;
360
- deleteListener = undefined;
361
- loadSubsetDedupe?.reset();
362
- },
363
- loadSubset: loadSubsetDedupe?.loadSubset ?? loadSubset,
364
- } satisfies SyncConfigRes;
365
- };
366
-
367
- // External sync handler - allows pushing sync events from outside (e.g., proxy server)
368
- const pushExternalSync: ExternalSyncHandler<ItemType> = (event) => {
369
- if (!syncBegin || !syncWrite || !syncCommit || !syncTruncate) {
370
- if (config.debug) {
371
- console.warn(
372
- "[pushExternalSync] Sync functions not initialized yet - event will be dropped",
373
- event,
374
- );
375
- }
376
- return;
377
- }
378
-
379
- switch (event.type) {
380
- case "insert":
381
- syncBegin();
382
- for (const item of event.items) {
383
- syncWrite({ type: "insert", value: item });
384
- }
385
- syncCommit();
386
- break;
387
- case "update":
388
- syncBegin();
389
- for (const item of event.items) {
390
- syncWrite({ type: "update", value: item });
391
- }
392
- syncCommit();
393
- break;
394
- case "delete":
395
- syncBegin();
396
- for (const item of event.items) {
397
- syncWrite({ type: "delete", value: item });
398
- }
399
- syncCommit();
400
- break;
401
- case "truncate":
402
- syncBegin();
403
- syncTruncate();
404
- syncCommit();
405
- break;
406
- }
407
- };
408
-
409
- // Create utils with truncate and external sync
410
- const utils: CollectionUtils<ItemType> = {
411
- truncate: async () => {
412
- if (!backend.handleTruncate) {
413
- throw new Error("Truncate not supported by this backend");
414
- }
415
- if (!syncBegin || !syncTruncate || !syncCommit) {
416
- throw new Error(
417
- "Sync functions not initialized - sync function may not have been called yet",
418
- );
419
- }
420
- // Clear the backend store
421
- await backend.handleTruncate();
422
- // Update local reactive store (same pattern as insert/update/delete listeners)
423
- syncBegin();
424
- syncTruncate();
425
- syncCommit();
426
- },
427
- pushExternalSync,
428
- };
429
-
430
- return {
431
- sync: syncFn,
432
- onInsert: async (params) => {
433
- if (!insertListener) {
434
- throw new Error(
435
- "insertListener not initialized - sync function may not have been called yet",
436
- );
437
- }
438
- return insertListener(params);
439
- },
440
- onUpdate: async (params) => {
441
- if (!updateListener) {
442
- throw new Error(
443
- "updateListener not initialized - sync function may not have been called yet",
444
- );
445
- }
446
- return updateListener(params);
447
- },
448
- onDelete: async (params) => {
449
- if (!deleteListener) {
450
- throw new Error(
451
- "deleteListener not initialized - sync function may not have been called yet",
452
- );
453
- }
454
- return deleteListener(params);
455
- },
456
- utils,
457
- };
129
+ return createGenericSyncFunction(config, backend);
458
130
  }
459
131
 
460
132
  /**
@@ -562,8 +234,8 @@ export function createGetKeyFunction<TTable extends Table>() {
562
234
  }
563
235
 
564
236
  /**
565
- * Base collection config factory
566
- * Combines schema, sync, and event handlers into a collection config
237
+ * Base collection config factory.
238
+ * Delegates to the generic (Drizzle-free) implementation in @firtoz/db-helpers.
567
239
  */
568
240
  export function createCollectionConfig<
569
241
  TTable extends Table,
@@ -603,18 +275,19 @@ export function createCollectionConfig<
603
275
  schema: TSchema;
604
276
  utils: CollectionUtils<InferSchemaOutput<SelectSchema<TTable>>>;
605
277
  } {
606
- return {
607
- schema: config.schema,
608
- getKey: config.getKey,
609
- sync: {
610
- sync: config.syncResult.sync,
611
- },
612
- // Merge provided handlers with sync result handlers (provided handlers take precedence)
613
- onInsert: config.onInsert ?? config.syncResult.onInsert,
614
- onUpdate: config.onUpdate ?? config.syncResult.onUpdate,
615
- onDelete: config.onDelete ?? config.syncResult.onDelete,
616
- syncMode: config.syncMode,
617
- // Include utils with truncate and pushExternalSync
618
- 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>;
619
290
  };
291
+
292
+ return createGenericCollectionConfig<TItem, TSchema>(config) as ReturnType;
620
293
  }
package/src/index.ts CHANGED
@@ -27,9 +27,6 @@ export type {
27
27
  BaseSyncConfig,
28
28
  SyncBackend,
29
29
  SyncFunctionResult,
30
- ExternalSyncEvent,
31
- ExternalSyncHandler,
32
- CollectionUtils,
33
30
  } from "./collection-utils";
34
31
 
35
32
  export {