@firtoz/drizzle-indexeddb 0.4.3 → 0.5.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 CHANGED
@@ -1,5 +1,28 @@
1
1
  # @firtoz/drizzle-indexeddb
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`9e532bb`](https://github.com/firtoz/fullstack-toolkit/commit/9e532bbd83bc671c62fd1333ae25fd9829112464) Thanks [@firtoz](https://github.com/firtoz)! - Add `createStandaloneCollection` utility for using IndexedDB collections outside of React context.
8
+
9
+ Features:
10
+
11
+ - Simple API for standalone usage without React providers
12
+ - Async mutation methods (`insert`, `update`, `delete`, `truncate`) that return Promises
13
+ - Sync accessors (`getAll`, `get`, `isReady`)
14
+ - Full access to collection utils (`truncate`, `pushExternalSync`)
15
+ - Automatic database initialization with migration support
16
+
17
+ Also:
18
+
19
+ - Update `IndexedDbCollection` type to use `CollectionUtils` instead of generic `UtilsRecord` for proper typing of `truncate` and `pushExternalSync`
20
+ - Export `IndexedDbCollection` type from package
21
+
22
+ ### Patch Changes
23
+
24
+ - [`c772c2c`](https://github.com/firtoz/fullstack-toolkit/commit/c772c2cf74af560dc04080933591ccd3014f85a1) Thanks [@firtoz](https://github.com/firtoz)! - Improve types returned and simplify internal logic
25
+
3
26
  ## 0.4.3
4
27
 
5
28
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firtoz/drizzle-indexeddb",
3
- "version": "0.4.3",
3
+ "version": "0.5.0",
4
4
  "description": "IndexedDB migrations powered by Drizzle ORM",
5
5
  "main": "./src/index.ts",
6
6
  "module": "./src/index.ts",
@@ -35,10 +35,6 @@ export interface IndexedDBCollectionConfig<TTable extends Table> {
35
35
  * Ref to the IndexedDB database instance
36
36
  */
37
37
  indexedDBRef: React.RefObject<IDBDatabaseLike | null>;
38
- /**
39
- * The database name (for perf markers)
40
- */
41
- dbName: string;
42
38
  /**
43
39
  * The Drizzle table definition (used for schema and type inference only)
44
40
  */
@@ -10,7 +10,6 @@ import {
10
10
  import {
11
11
  createCollection,
12
12
  type InferSchemaInput,
13
- type UtilsRecord,
14
13
  type Collection,
15
14
  type InferSchemaOutput,
16
15
  type SyncMode,
@@ -26,7 +25,7 @@ import type {
26
25
  SelectSchema,
27
26
  GetTableFromSchema,
28
27
  InferCollectionFromTable,
29
- ExternalSyncHandler,
28
+ CollectionUtils,
30
29
  } from "@firtoz/drizzle-utils";
31
30
  import {
32
31
  type Migration,
@@ -38,21 +37,21 @@ import type { IDBProxySyncMessage } from "../proxy/idb-proxy-types";
38
37
 
39
38
  interface CollectionCacheEntry {
40
39
  // biome-ignore lint/suspicious/noExplicitAny: Cache needs to store collections of various types
41
- collection: Collection<any, string>;
40
+ collection: Collection<any, string, CollectionUtils<any>, any, any>;
42
41
  refCount: number;
43
- // biome-ignore lint/suspicious/noExplicitAny: External sync needs to accept any item type
44
- pushExternalSync: ExternalSyncHandler<any>;
45
42
  }
46
43
 
47
44
  // Type for migration functions (generated by Drizzle)
48
45
 
49
- type IndexedDbCollection<
46
+ export type IndexedDbCollection<
50
47
  TSchema extends Record<string, unknown>,
51
48
  TTableName extends keyof TSchema & string,
52
49
  > = Collection<
53
50
  InferSchemaOutput<SelectSchema<GetTableFromSchema<TSchema, TTableName>>>,
54
51
  IdOf<GetTableFromSchema<TSchema, TTableName>>,
55
- UtilsRecord,
52
+ CollectionUtils<
53
+ InferSchemaOutput<SelectSchema<GetTableFromSchema<TSchema, TTableName>>>
54
+ >,
56
55
  SelectSchema<GetTableFromSchema<TSchema, TTableName>>,
57
56
  InferSchemaInput<InsertSchema<GetTableFromSchema<TSchema, TTableName>>>
58
57
  >;
@@ -195,7 +194,6 @@ export function DrizzleIndexedDBProvider<
195
194
  // Create collection options
196
195
  const collectionConfig = indexedDBCollectionOptions({
197
196
  indexedDBRef,
198
- dbName,
199
197
  table,
200
198
  storeName: actualTableName,
201
199
  readyPromise: readyPromise.promise,
@@ -205,12 +203,14 @@ export function DrizzleIndexedDBProvider<
205
203
 
206
204
  // Create new collection and cache it with ref count 0
207
205
  // The collection will wait for readyPromise before accessing the database
208
- const collection = createCollection(collectionConfig);
206
+ // Cast is safe: our collectionConfig provides CollectionUtils, but createCollection types it as UtilsRecord
207
+ const collection = createCollection(
208
+ collectionConfig,
209
+ ) as CollectionCacheEntry["collection"];
209
210
 
210
211
  collections.set(cacheKey, {
211
212
  collection,
212
213
  refCount: 0,
213
- pushExternalSync: collectionConfig.utils.pushExternalSync,
214
214
  });
215
215
  }
216
216
 
@@ -218,7 +218,7 @@ export function DrizzleIndexedDBProvider<
218
218
  return collections.get(cacheKey)!
219
219
  .collection as unknown as IndexedDbCollection<TSchema, TTableName>;
220
220
  },
221
- [collections, schema, readyPromise.promise, debug, dbName, syncMode],
221
+ [collections, schema, readyPromise.promise, debug, syncMode],
222
222
  );
223
223
 
224
224
  const incrementRefCount: DrizzleIndexedDBContextValue<TSchema>["incrementRefCount"] =
@@ -257,30 +257,31 @@ export function DrizzleIndexedDBProvider<
257
257
  const actualStoreName = getTableName(table as Table);
258
258
  if (actualStoreName === message.storeName) {
259
259
  const entry = collections.get(tableName);
260
- if (entry?.pushExternalSync) {
260
+ if (entry) {
261
+ const { pushExternalSync } = entry.collection.utils;
261
262
  // Route sync message to collection
262
263
  switch (message.type) {
263
264
  case "sync:add":
264
- entry.pushExternalSync({
265
+ pushExternalSync({
265
266
  type: "insert",
266
267
  items: message.items,
267
268
  });
268
269
  break;
269
270
  case "sync:put":
270
- entry.pushExternalSync({
271
+ pushExternalSync({
271
272
  type: "update",
272
273
  items: message.items,
273
274
  });
274
275
  break;
275
276
  case "sync:delete":
276
277
  // For delete, construct items with id
277
- entry.pushExternalSync({
278
+ pushExternalSync({
278
279
  type: "delete",
279
280
  items: message.keys.map((key) => ({ id: key })),
280
281
  });
281
282
  break;
282
283
  case "sync:clear":
283
- entry.pushExternalSync({
284
+ pushExternalSync({
284
285
  type: "truncate",
285
286
  });
286
287
  break;
package/src/index.ts CHANGED
@@ -39,12 +39,20 @@ export {
39
39
  type IndexedDBSyncItem,
40
40
  } from "./collections/indexeddb-collection";
41
41
 
42
+ // Standalone Collection (for use outside React)
43
+ export {
44
+ createStandaloneCollection,
45
+ type StandaloneCollection,
46
+ type StandaloneCollectionConfig,
47
+ } from "./standalone-collection";
48
+
42
49
  // IndexedDB Provider
43
50
  export {
44
51
  DrizzleIndexedDBProvider,
45
52
  DrizzleIndexedDBContext,
46
53
  useIndexedDBCollection,
47
54
  type DrizzleIndexedDBContextValue,
55
+ type IndexedDbCollection,
48
56
  } from "./context/DrizzleIndexedDBProvider";
49
57
 
50
58
  export {
@@ -0,0 +1,372 @@
1
+ import {
2
+ createCollection,
3
+ type Collection,
4
+ type InferSchemaInput,
5
+ type InferSchemaOutput,
6
+ type SyncMode,
7
+ type Transaction,
8
+ type WritableDeep,
9
+ } from "@tanstack/db";
10
+ import type { Table } from "drizzle-orm";
11
+ import type {
12
+ IdOf,
13
+ InsertSchema,
14
+ SelectSchema,
15
+ CollectionUtils,
16
+ } from "@firtoz/drizzle-utils";
17
+ import {
18
+ indexedDBCollectionOptions,
19
+ type IndexedDBCollectionConfig,
20
+ } from "./collections/indexeddb-collection";
21
+ import {
22
+ migrateIndexedDBWithFunctions,
23
+ type Migration,
24
+ } from "./function-migrator";
25
+ import { openIndexedDb } from "./idb-operations";
26
+ import type { IDBCreator, IDBDatabaseLike } from "./idb-types";
27
+
28
+ /**
29
+ * Configuration for creating a standalone IndexedDB collection
30
+ */
31
+ export interface StandaloneCollectionConfig<TTable extends Table> {
32
+ /**
33
+ * Name of the IndexedDB database
34
+ */
35
+ dbName: string;
36
+ /**
37
+ * The Drizzle table definition
38
+ */
39
+ table: TTable;
40
+ /**
41
+ * The name of the IndexedDB object store (defaults to table name)
42
+ */
43
+ storeName?: string;
44
+ /**
45
+ * Migrations to apply (optional)
46
+ */
47
+ migrations?: Migration[];
48
+ /**
49
+ * Custom database creator (for testing/mocking)
50
+ */
51
+ dbCreator?: IDBCreator;
52
+ /**
53
+ * Enable debug logging
54
+ */
55
+ debug?: boolean;
56
+ /**
57
+ * Sync mode: 'eager' (immediate) or 'lazy' (on-demand)
58
+ */
59
+ syncMode?: SyncMode;
60
+ }
61
+
62
+ /**
63
+ * Type for the underlying collection
64
+ */
65
+ type InternalCollection<TTable extends Table> = Collection<
66
+ InferSchemaOutput<SelectSchema<TTable>>,
67
+ IdOf<TTable>,
68
+ CollectionUtils<InferSchemaOutput<SelectSchema<TTable>>>,
69
+ SelectSchema<TTable>,
70
+ InferSchemaInput<InsertSchema<TTable>>
71
+ >;
72
+
73
+ /**
74
+ * Transaction type for mutations
75
+ */
76
+ type MutationTransaction<TTable extends Table> = Transaction<
77
+ InferSchemaOutput<SelectSchema<TTable>>
78
+ >;
79
+
80
+ /**
81
+ * Insert input type (what you pass to insert)
82
+ */
83
+ type InsertInput<TTable extends Table> = InferSchemaInput<InsertSchema<TTable>>;
84
+
85
+ /**
86
+ * Item type (what you get back from getAll, etc.)
87
+ */
88
+ type ItemType<TTable extends Table> = InferSchemaOutput<SelectSchema<TTable>>;
89
+
90
+ /**
91
+ * Writable draft type for update callbacks
92
+ */
93
+ type DraftType<TTable extends Table> = WritableDeep<InsertInput<TTable>>;
94
+
95
+ /**
96
+ * Standalone IndexedDB collection API
97
+ */
98
+ export interface StandaloneCollection<TTable extends Table> {
99
+ /**
100
+ * Promise that resolves when the collection is ready
101
+ */
102
+ ready: Promise<void>;
103
+
104
+ /**
105
+ * Check if the collection is ready (sync)
106
+ */
107
+ isReady(): boolean;
108
+
109
+ /**
110
+ * Get all items (sync - returns current state)
111
+ */
112
+ getAll(): ItemType<TTable>[];
113
+
114
+ /**
115
+ * Get an item by key (sync)
116
+ */
117
+ get(key: IdOf<TTable>): ItemType<TTable> | undefined;
118
+
119
+ /**
120
+ * Insert item(s)
121
+ * @returns Promise that resolves when persisted
122
+ */
123
+ insert(
124
+ data: InsertInput<TTable> | InsertInput<TTable>[],
125
+ callback?: (transaction: MutationTransaction<TTable>) => void,
126
+ ): Promise<MutationTransaction<TTable>>;
127
+
128
+ /**
129
+ * Update an item by key using a callback that receives a draft
130
+ * @returns Promise that resolves when persisted
131
+ */
132
+ update(
133
+ key: IdOf<TTable>,
134
+ updater: (draft: DraftType<TTable>) => void,
135
+ callback?: (transaction: MutationTransaction<TTable>) => void,
136
+ ): Promise<MutationTransaction<TTable>>;
137
+
138
+ /**
139
+ * Delete item(s) by key
140
+ * @returns Promise that resolves when persisted
141
+ */
142
+ delete(
143
+ key: IdOf<TTable> | IdOf<TTable>[],
144
+ callback?: (transaction: MutationTransaction<TTable>) => void,
145
+ ): Promise<MutationTransaction<TTable>>;
146
+
147
+ /**
148
+ * Clear all items from the store
149
+ * @returns Promise that resolves when truncate is complete
150
+ */
151
+ truncate(): Promise<void>;
152
+
153
+ /**
154
+ * Access to collection utils (truncate, pushExternalSync)
155
+ */
156
+ utils: CollectionUtils<ItemType<TTable>>;
157
+
158
+ /**
159
+ * The underlying TanStack DB collection (for advanced usage)
160
+ */
161
+ collection: InternalCollection<TTable>;
162
+
163
+ /**
164
+ * The IndexedDB database instance (available after ready)
165
+ */
166
+ db: IDBDatabaseLike | null;
167
+
168
+ /**
169
+ * Close the database connection
170
+ */
171
+ close(): void;
172
+ }
173
+
174
+ /**
175
+ * Create a standalone IndexedDB collection for use outside of React.
176
+ *
177
+ * @example
178
+ * ```ts
179
+ * const db = await createStandaloneCollection({
180
+ * dbName: "myapp.db",
181
+ * table: schema.todos,
182
+ * migrations,
183
+ * });
184
+ *
185
+ * // Wait for ready
186
+ * await db.ready;
187
+ *
188
+ * // Get all items
189
+ * const items = db.getAll();
190
+ *
191
+ * // Insert
192
+ * await db.insert({ title: "New todo" });
193
+ *
194
+ * // Update
195
+ * await db.update(itemId, { title: "Updated" });
196
+ *
197
+ * // Delete
198
+ * await db.delete(itemId);
199
+ *
200
+ * // Truncate
201
+ * await db.truncate();
202
+ *
203
+ * // Clean up
204
+ * db.close();
205
+ * ```
206
+ */
207
+ export function createStandaloneCollection<TTable extends Table>(
208
+ config: StandaloneCollectionConfig<TTable>,
209
+ ): StandaloneCollection<TTable> {
210
+ const {
211
+ dbName,
212
+ table,
213
+ storeName = (table as unknown as { _: { name: string } })._.name,
214
+ migrations = [],
215
+ dbCreator,
216
+ debug = false,
217
+ syncMode = "eager",
218
+ } = config;
219
+
220
+ // Create ready promise
221
+ let resolveReady: () => void;
222
+ const readyPromise = new Promise<void>((resolve) => {
223
+ resolveReady = resolve;
224
+ });
225
+
226
+ // Database ref
227
+ const indexedDBRef: { current: IDBDatabaseLike | null } = { current: null };
228
+
229
+ // Initialize database
230
+ const initDB = async () => {
231
+ try {
232
+ if (migrations.length === 0) {
233
+ if (debug) {
234
+ console.log(
235
+ `[StandaloneCollection] Opening database "${dbName}" directly`,
236
+ );
237
+ }
238
+ indexedDBRef.current = await openIndexedDb(dbName, dbCreator);
239
+ } else {
240
+ if (debug) {
241
+ console.log(`[StandaloneCollection] Migrating database "${dbName}"`);
242
+ }
243
+ indexedDBRef.current = await migrateIndexedDBWithFunctions(
244
+ dbName,
245
+ migrations,
246
+ debug,
247
+ dbCreator,
248
+ );
249
+ }
250
+
251
+ if (debug) {
252
+ console.log(`[StandaloneCollection] Database "${dbName}" initialized`);
253
+ }
254
+ // biome-ignore lint/style/noNonNullAssertion: resolveReady is set in promise constructor
255
+ resolveReady!();
256
+ } catch (error) {
257
+ console.error(
258
+ `[StandaloneCollection] Failed to initialize database "${dbName}":`,
259
+ error,
260
+ );
261
+ throw error;
262
+ }
263
+ };
264
+
265
+ // Start initialization
266
+ initDB();
267
+
268
+ // Create collection config
269
+ const collectionConfig = indexedDBCollectionOptions({
270
+ indexedDBRef,
271
+ table,
272
+ storeName,
273
+ readyPromise,
274
+ debug,
275
+ syncMode,
276
+ } as IndexedDBCollectionConfig<TTable>);
277
+
278
+ // Create the collection
279
+ const collection = createCollection(
280
+ collectionConfig,
281
+ ) as unknown as InternalCollection<TTable>;
282
+
283
+ // Wait for collection to be ready
284
+ const collectionReady = new Promise<void>((resolve) => {
285
+ if (collection.isReady()) {
286
+ resolve();
287
+ return;
288
+ }
289
+ collection.preload();
290
+ collection.onFirstReady(() => resolve());
291
+ });
292
+
293
+ // Combined ready promise
294
+ const ready = Promise.all([readyPromise, collectionReady]).then(() => {});
295
+
296
+ // Helper to wait for transaction to persist
297
+ const waitForPersist = (
298
+ // biome-ignore lint/suspicious/noExplicitAny: Transaction types are complex, runtime is correct
299
+ transaction: any,
300
+ // biome-ignore lint/suspicious/noExplicitAny: Transaction types are complex, runtime is correct
301
+ callback?: (transaction: any) => void,
302
+ ): Promise<MutationTransaction<TTable>> => {
303
+ if (callback) {
304
+ callback(transaction);
305
+ }
306
+ return transaction.isPersisted.promise.then(() => transaction);
307
+ };
308
+
309
+ return {
310
+ ready,
311
+
312
+ isReady(): boolean {
313
+ return collection.isReady();
314
+ },
315
+
316
+ getAll(): ItemType<TTable>[] {
317
+ return collection.toArray;
318
+ },
319
+
320
+ get(key: IdOf<TTable>): ItemType<TTable> | undefined {
321
+ return collection.state.get(key);
322
+ },
323
+
324
+ insert(
325
+ data: InsertInput<TTable> | InsertInput<TTable>[],
326
+ callback?: (transaction: MutationTransaction<TTable>) => void,
327
+ ): Promise<MutationTransaction<TTable>> {
328
+ const items = Array.isArray(data) ? data : [data];
329
+ // @ts-expect-error - Type inference is complex here but runtime is correct
330
+ const transaction = collection.insert(...items);
331
+ return waitForPersist(transaction, callback);
332
+ },
333
+
334
+ update(
335
+ key: IdOf<TTable>,
336
+ updater: (draft: DraftType<TTable>) => void,
337
+ callback?: (transaction: MutationTransaction<TTable>) => void,
338
+ ): Promise<MutationTransaction<TTable>> {
339
+ const transaction = collection.update(
340
+ key,
341
+ updater as (draft: any) => void,
342
+ );
343
+ return waitForPersist(transaction, callback);
344
+ },
345
+
346
+ delete(
347
+ key: IdOf<TTable> | IdOf<TTable>[],
348
+ callback?: (transaction: MutationTransaction<TTable>) => void,
349
+ ): Promise<MutationTransaction<TTable>> {
350
+ const keys = Array.isArray(key) ? key : [key];
351
+ const transaction = collection.delete(keys);
352
+ return waitForPersist(transaction, callback);
353
+ },
354
+
355
+ truncate(): Promise<void> {
356
+ return collection.utils.truncate();
357
+ },
358
+
359
+ utils: collection.utils,
360
+
361
+ collection,
362
+
363
+ get db(): IDBDatabaseLike | null {
364
+ return indexedDBRef.current;
365
+ },
366
+
367
+ close(): void {
368
+ indexedDBRef.current?.close();
369
+ indexedDBRef.current = null;
370
+ },
371
+ };
372
+ }