@firtoz/drizzle-indexeddb 0.4.2 → 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 +29 -0
- package/README.md +37 -3
- package/package.json +4 -1
- package/src/bin/generate-migrations.ts +28 -42
- package/src/collections/indexeddb-collection.ts +0 -4
- package/src/context/DrizzleIndexedDBProvider.tsx +17 -16
- package/src/index.ts +8 -0
- package/src/standalone-collection.ts +372 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
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
|
+
|
|
26
|
+
## 0.4.3
|
|
27
|
+
|
|
28
|
+
### Patch Changes
|
|
29
|
+
|
|
30
|
+
- [`ed6dfce`](https://github.com/firtoz/fullstack-toolkit/commit/ed6dfce75be95d5349381ab43c6c22b25b164414) Thanks [@firtoz](https://github.com/firtoz)! - Enable drizzle and output dir configuration in drizzle-indexeddb-generate
|
|
31
|
+
|
|
3
32
|
## 0.4.2
|
|
4
33
|
|
|
5
34
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -385,8 +385,8 @@ bun drizzle-indexeddb-generate --help
|
|
|
385
385
|
|
|
386
386
|
| Option | Description | Default |
|
|
387
387
|
|--------|-------------|---------|
|
|
388
|
-
| `--drizzle-dir <path
|
|
389
|
-
| `--output-dir <path
|
|
388
|
+
| `--drizzle-dir <path>`, `-d` | Path to Drizzle directory | `./drizzle` |
|
|
389
|
+
| `--output-dir <path>`, `-o` | Path to output directory | `<drizzle-dir>/indexeddb-migrations` |
|
|
390
390
|
|
|
391
391
|
### npm scripts
|
|
392
392
|
|
|
@@ -395,11 +395,13 @@ Add to your `package.json`:
|
|
|
395
395
|
```json
|
|
396
396
|
{
|
|
397
397
|
"scripts": {
|
|
398
|
-
"db:generate": "bun drizzle-kit generate && bun drizzle-indexeddb-generate"
|
|
398
|
+
"db:generate": "bun --bun drizzle-kit generate && bun drizzle-indexeddb-generate"
|
|
399
399
|
}
|
|
400
400
|
}
|
|
401
401
|
```
|
|
402
402
|
|
|
403
|
+
> **Note:** The `--bun` flag forces bun's runtime instead of Node, which is needed because this package exports raw TypeScript. See [Troubleshooting](#err_unsupported_node_modules_type_stripping-error) if you encounter type stripping errors.
|
|
404
|
+
|
|
403
405
|
## Advanced Usage
|
|
404
406
|
|
|
405
407
|
### Custom Sync Configuration
|
|
@@ -666,6 +668,38 @@ Remove from schema and regenerate - the migrator will delete the object store.
|
|
|
666
668
|
|
|
667
669
|
## Troubleshooting
|
|
668
670
|
|
|
671
|
+
### "ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING" Error
|
|
672
|
+
|
|
673
|
+
If you see this error when running `drizzle-kit generate`:
|
|
674
|
+
|
|
675
|
+
```
|
|
676
|
+
Error [ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING]: Stripping types is currently unsupported for files under node_modules
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
This happens because this package exports raw TypeScript files, and Node's built-in type stripping doesn't work inside `node_modules`.
|
|
680
|
+
|
|
681
|
+
**Solution:** Use `bun --bun` to force bun's runtime instead of Node:
|
|
682
|
+
|
|
683
|
+
```bash
|
|
684
|
+
bun --bun drizzle-kit generate
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
Or in your `package.json`:
|
|
688
|
+
|
|
689
|
+
```json
|
|
690
|
+
{
|
|
691
|
+
"scripts": {
|
|
692
|
+
"db:generate": "bun --bun drizzle-kit generate && bun drizzle-indexeddb-generate"
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
**Alternative:** If you're not using bun, use `tsx`:
|
|
698
|
+
|
|
699
|
+
```bash
|
|
700
|
+
npx tsx node_modules/drizzle-kit/bin.cjs generate --config ./drizzle.config.ts
|
|
701
|
+
```
|
|
702
|
+
|
|
669
703
|
### "Primary key structure changed" Error
|
|
670
704
|
|
|
671
705
|
This happens when you change the primary key of a table. IndexedDB doesn't support changing keyPath after creation.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firtoz/drizzle-indexeddb",
|
|
3
|
-
"version": "0.
|
|
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",
|
|
@@ -83,5 +83,8 @@
|
|
|
83
83
|
"drizzle-valibot": "^0.4.2",
|
|
84
84
|
"react": "^19.2.1",
|
|
85
85
|
"valibot": "^1.2.0"
|
|
86
|
+
},
|
|
87
|
+
"dependencies": {
|
|
88
|
+
"citty": "^0.1.6"
|
|
86
89
|
}
|
|
87
90
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
11
11
|
import { join, resolve } from "node:path";
|
|
12
|
+
import { defineCommand, runMain } from "citty";
|
|
12
13
|
import type {
|
|
13
14
|
JournalEntry,
|
|
14
15
|
Journal,
|
|
@@ -240,49 +241,34 @@ export default migrations;
|
|
|
240
241
|
}
|
|
241
242
|
|
|
242
243
|
// CLI entry point
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
--drizzle-dir <path> Path to Drizzle directory (default: ./drizzle)
|
|
270
|
-
--output-dir <path> Path to output directory (default: ./drizzle/indexeddb-migrations)
|
|
271
|
-
-h, --help Show this help message
|
|
272
|
-
|
|
273
|
-
Examples:
|
|
274
|
-
bun drizzle-indexeddb-generate
|
|
275
|
-
bun drizzle-indexeddb-generate --drizzle-dir ./db/drizzle
|
|
276
|
-
bun drizzle-indexeddb-generate --output-dir ./src/migrations
|
|
277
|
-
`);
|
|
278
|
-
} else {
|
|
279
|
-
console.error(`Unknown command: ${command}`);
|
|
280
|
-
console.error(`Run 'bun drizzle-indexeddb-generate --help' for usage`);
|
|
281
|
-
process.exit(1);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
244
|
+
const main = defineCommand({
|
|
245
|
+
meta: {
|
|
246
|
+
name: "drizzle-indexeddb-generate",
|
|
247
|
+
description: "Generate IndexedDB migrations from Drizzle schema",
|
|
248
|
+
},
|
|
249
|
+
args: {
|
|
250
|
+
drizzleDir: {
|
|
251
|
+
type: "string",
|
|
252
|
+
alias: "d",
|
|
253
|
+
default: "./drizzle",
|
|
254
|
+
description: "Path to Drizzle directory",
|
|
255
|
+
},
|
|
256
|
+
outputDir: {
|
|
257
|
+
type: "string",
|
|
258
|
+
alias: "o",
|
|
259
|
+
description:
|
|
260
|
+
"Path to output directory (default: <drizzle-dir>/indexeddb-migrations)",
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
run({ args }) {
|
|
264
|
+
generateIndexedDBMigrations({
|
|
265
|
+
drizzleDir: args.drizzleDir,
|
|
266
|
+
outputDir: args.outputDir,
|
|
267
|
+
});
|
|
268
|
+
},
|
|
269
|
+
});
|
|
284
270
|
|
|
285
271
|
// Only run CLI when executed directly (not when imported)
|
|
286
272
|
if (import.meta.main) {
|
|
287
|
-
main
|
|
273
|
+
runMain(main);
|
|
288
274
|
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
265
|
+
pushExternalSync({
|
|
265
266
|
type: "insert",
|
|
266
267
|
items: message.items,
|
|
267
268
|
});
|
|
268
269
|
break;
|
|
269
270
|
case "sync:put":
|
|
270
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|