@firtoz/drizzle-utils 1.0.0 → 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,14 @@
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
+
3
12
  ## 1.0.0
4
13
 
5
14
  ### Major Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firtoz/drizzle-utils",
3
- "version": "1.0.0",
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,19 +53,19 @@
53
53
  "access": "public"
54
54
  },
55
55
  "peerDependencies": {
56
- "@tanstack/db": ">=0.5.26",
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.26",
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
66
  },
67
67
  "dependencies": {
68
- "@firtoz/db-helpers": "^1.0.0",
68
+ "@firtoz/db-helpers": "^2.0.0",
69
69
  "@firtoz/maybe-error": "^1.5.2"
70
70
  }
71
71
  }
@@ -1,5 +1,12 @@
1
- import type { CollectionUtils, SyncMessage } from "@firtoz/db-helpers";
2
- import { exhaustiveGuard } from "@firtoz/maybe-error";
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
- // WORKAROUND: DeduplicatedLoadSubset has a bug where toggling queries (e.g., isNull/isNotNull)
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 interface SyncBackend<TTable extends Table> {
124
- /**
125
- * Initial data load - should call write() for each item
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 that includes both sync config and listeners
115
+ * Return type for createSyncFunction.
116
+ * Drizzle-typed alias for GenericSyncFunctionResult.
169
117
  */
170
- export type SyncFunctionResult<TTable extends Table> = {
171
- sync: SyncConfig<InferSchemaOutput<SelectSchema<TTable>>, string>["sync"];
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
- type ItemType = InferSchemaOutput<SelectSchema<TTable>>;
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
- * 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.
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
- return {
574
- schema: config.schema,
575
- getKey: config.getKey,
576
- sync: {
577
- sync: config.syncResult.sync,
578
- },
579
- // Merge provided handlers with sync result handlers (provided handlers take precedence)
580
- onInsert: config.onInsert ?? config.syncResult.onInsert,
581
- onUpdate: config.onUpdate ?? config.syncResult.onUpdate,
582
- onDelete: config.onDelete ?? config.syncResult.onDelete,
583
- syncMode: config.syncMode,
584
- // Include utils with truncate and receiveSync
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
  }