@firtoz/drizzle-indexeddb 0.2.0 → 0.3.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 +32 -26
- package/README.md +112 -51
- package/package.json +13 -5
- package/src/bin/generate-migrations.ts +288 -0
- package/src/collections/indexeddb-collection.ts +95 -243
- package/src/context/useDrizzleIndexedDB.ts +2 -1
- package/src/function-migrator.ts +190 -170
- package/src/index.ts +16 -7
- package/src/utils.ts +517 -7
- package/src/snapshot-migrator.ts +0 -420
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @firtoz/drizzle-indexeddb
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`5e854a6`](https://github.com/firtoz/fullstack-toolkit/commit/5e854a62236a811918a47037a59df23329856614) Thanks [@firtoz](https://github.com/firtoz)! - ### Breaking Changes
|
|
8
|
+
|
|
9
|
+
- Removed `migrateIndexedDB` and `IndexedDBMigrationConfig` exports - use `migrateIndexedDBWithFunctions` instead
|
|
10
|
+
- Removed snapshot-based migration system in favor of function-based migrations
|
|
11
|
+
|
|
12
|
+
### New Features
|
|
13
|
+
|
|
14
|
+
- Added `drizzle-indexeddb-generate` CLI tool to generate IndexedDB migration functions from Drizzle snapshots
|
|
15
|
+
- Added `generateIndexedDBMigrations` export for programmatic migration generation
|
|
16
|
+
- Added `./generate` export path
|
|
17
|
+
|
|
18
|
+
### Migration Guide
|
|
19
|
+
|
|
20
|
+
Instead of importing snapshots directly and using `migrateIndexedDB`, you now:
|
|
21
|
+
|
|
22
|
+
1. Run `bun drizzle-indexeddb-generate` (or `npx drizzle-indexeddb-generate`) after `drizzle-kit generate`
|
|
23
|
+
2. Import the generated migrations and use `migrateIndexedDBWithFunctions`
|
|
24
|
+
|
|
3
25
|
## 0.2.0
|
|
4
26
|
|
|
5
27
|
### Minor Changes
|
|
@@ -41,18 +63,6 @@
|
|
|
41
63
|
- Sync configuration for real-time updates
|
|
42
64
|
- Works seamlessly with React hooks
|
|
43
65
|
|
|
44
|
-
### Snapshot-Based Migration
|
|
45
|
-
|
|
46
|
-
**`migrateIndexedDB(dbName, config, debug?)`** - Automatically migrates IndexedDB databases using Drizzle snapshot files:
|
|
47
|
-
|
|
48
|
-
- Reads Drizzle journal and snapshot files
|
|
49
|
-
- Tracks applied migrations in `__drizzle_migrations` store
|
|
50
|
-
- Creates/updates object stores and indexes based on schema changes
|
|
51
|
-
- Handles table deletion, index changes, and schema evolution
|
|
52
|
-
- Incremental migrations - only applies pending changes
|
|
53
|
-
- Validates primary key structure changes (requires manual migration if keys change)
|
|
54
|
-
- Performance logging when debug mode is enabled
|
|
55
|
-
|
|
56
66
|
### Function-Based Migration
|
|
57
67
|
|
|
58
68
|
**`migrateIndexedDBWithFunctions(dbName, migrations, debug?)`** - Run migrations using custom migration functions:
|
|
@@ -87,19 +97,15 @@
|
|
|
87
97
|
## Example
|
|
88
98
|
|
|
89
99
|
```typescript
|
|
90
|
-
import {
|
|
91
|
-
import
|
|
92
|
-
import * as snapshots from "./drizzle/snapshots";
|
|
100
|
+
import { migrateIndexedDBWithFunctions } from "@firtoz/drizzle-indexeddb";
|
|
101
|
+
import migrations from "./drizzle/indexeddb-migrations";
|
|
93
102
|
|
|
94
|
-
// Migrate database using
|
|
95
|
-
const db = await
|
|
103
|
+
// Migrate database using function-based migrations
|
|
104
|
+
const db = await migrateIndexedDBWithFunctions(
|
|
96
105
|
"my-app-db",
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
},
|
|
101
|
-
true
|
|
102
|
-
); // debug mode
|
|
106
|
+
migrations,
|
|
107
|
+
true // debug mode
|
|
108
|
+
);
|
|
103
109
|
|
|
104
110
|
// Use with TanStack DB
|
|
105
111
|
import { createCollection } from "@tanstack/db";
|
|
@@ -136,9 +142,9 @@
|
|
|
136
142
|
|
|
137
143
|
## Migration Workflow
|
|
138
144
|
|
|
139
|
-
1. Generate Drizzle
|
|
140
|
-
2.
|
|
141
|
-
3.
|
|
145
|
+
1. Generate Drizzle migrations: `drizzle-kit generate`
|
|
146
|
+
2. Generate IndexedDB migrations: `bun drizzle-indexeddb-generate`
|
|
147
|
+
3. Import migrations and call `migrateIndexedDBWithFunctions()` on app startup
|
|
142
148
|
4. Database automatically updates to latest schema
|
|
143
149
|
|
|
144
150
|
## Dependencies
|
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# @firtoz/drizzle-indexeddb
|
|
2
2
|
|
|
3
|
-
TanStack DB collections backed by IndexedDB with automatic migrations powered by Drizzle ORM
|
|
3
|
+
TanStack DB collections backed by IndexedDB with automatic migrations powered by Drizzle ORM. Build reactive, type-safe IndexedDB applications with the power of Drizzle's schema management.
|
|
4
4
|
|
|
5
5
|
> **⚠️ Early WIP Notice:** This package is in very early development and is **not production-ready**. It is TypeScript-only and may have breaking changes. While I (the maintainer) have limited time, I'm open to PRs for features, bug fixes, or additional support (like JS builds). Please feel free to try it out and contribute! See [CONTRIBUTING.md](../../CONTRIBUTING.md) for details.
|
|
6
6
|
|
|
7
|
-
> **Note:** This package currently builds on top of Drizzle's SQLite integration (using `drizzle-orm/sqlite-core` types
|
|
7
|
+
> **Note:** This package currently builds on top of Drizzle's SQLite integration (using `drizzle-orm/sqlite-core` types) until Drizzle adds native IndexedDB support. The migration system uses function-based migrations generated from Drizzle's SQLite migrations to create IndexedDB object stores and indexes.
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
@@ -19,8 +19,7 @@ npm install @firtoz/drizzle-indexeddb @firtoz/drizzle-utils drizzle-orm @tanstac
|
|
|
19
19
|
- 🔍 **Query optimization** - Leverage IndexedDB indexes for fast queries
|
|
20
20
|
- 📦 **Soft deletes** - Built-in support for `deletedAt` column
|
|
21
21
|
- ⚛️ **React hooks** - Provider and hooks for easy React integration
|
|
22
|
-
-
|
|
23
|
-
- 📝 **Function-based migrations** - Write custom migration functions for complex changes
|
|
22
|
+
- 📝 **Function-based migrations** - Generated migration functions from Drizzle schema changes
|
|
24
23
|
|
|
25
24
|
## Quick Start
|
|
26
25
|
|
|
@@ -40,22 +39,25 @@ export const todoTable = syncableTable("todos", {
|
|
|
40
39
|
### 2. Generate Migrations
|
|
41
40
|
|
|
42
41
|
```bash
|
|
43
|
-
# Generate Drizzle
|
|
42
|
+
# Generate Drizzle migrations
|
|
44
43
|
drizzle-kit generate
|
|
44
|
+
|
|
45
|
+
# Generate IndexedDB migration functions
|
|
46
|
+
bun drizzle-indexeddb-generate
|
|
45
47
|
```
|
|
46
48
|
|
|
47
49
|
### 3. Migrate IndexedDB
|
|
48
50
|
|
|
49
51
|
```typescript
|
|
50
52
|
// db.ts
|
|
51
|
-
import {
|
|
52
|
-
import
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
import { migrateIndexedDBWithFunctions } from "@firtoz/drizzle-indexeddb";
|
|
54
|
+
import migrations from "./drizzle/indexeddb-migrations";
|
|
55
|
+
|
|
56
|
+
export const db = await migrateIndexedDBWithFunctions(
|
|
57
|
+
"my-app",
|
|
58
|
+
migrations,
|
|
59
|
+
true // Enable debug logging
|
|
60
|
+
);
|
|
59
61
|
```
|
|
60
62
|
|
|
61
63
|
### 4. Use with React
|
|
@@ -181,19 +183,19 @@ collection.find({
|
|
|
181
183
|
|
|
182
184
|
## Migration Methods
|
|
183
185
|
|
|
184
|
-
###
|
|
186
|
+
### Function-Based Migration
|
|
185
187
|
|
|
186
|
-
Use
|
|
188
|
+
Use generated migration functions to migrate your IndexedDB schema:
|
|
187
189
|
|
|
188
190
|
```typescript
|
|
189
|
-
import {
|
|
190
|
-
import
|
|
191
|
-
import * as snapshots from "./drizzle/snapshots";
|
|
191
|
+
import { migrateIndexedDBWithFunctions } from "@firtoz/drizzle-indexeddb";
|
|
192
|
+
import migrations from "./drizzle/indexeddb-migrations";
|
|
192
193
|
|
|
193
|
-
const db = await
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
194
|
+
const db = await migrateIndexedDBWithFunctions(
|
|
195
|
+
"my-app-db",
|
|
196
|
+
migrations,
|
|
197
|
+
true // debug flag
|
|
198
|
+
);
|
|
197
199
|
|
|
198
200
|
console.log("Database migrated successfully!");
|
|
199
201
|
```
|
|
@@ -219,40 +221,49 @@ interface MigrationRecord {
|
|
|
219
221
|
}
|
|
220
222
|
```
|
|
221
223
|
|
|
222
|
-
###
|
|
224
|
+
### Custom Migration Functions
|
|
223
225
|
|
|
224
|
-
For complex migrations that require custom logic:
|
|
226
|
+
For complex migrations that require custom logic, you can write migration functions directly:
|
|
225
227
|
|
|
226
228
|
```typescript
|
|
227
229
|
import { migrateIndexedDBWithFunctions } from "@firtoz/drizzle-indexeddb";
|
|
228
230
|
|
|
229
231
|
const migrations = [
|
|
230
232
|
// Migration 0: Initial schema
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
233
|
+
{
|
|
234
|
+
tag: "0000_initial",
|
|
235
|
+
migrate: async (db: IDBDatabase, transaction: IDBTransaction) => {
|
|
236
|
+
const store = db.createObjectStore("todos", { keyPath: "id" });
|
|
237
|
+
store.createIndex("title", "title", { unique: false });
|
|
238
|
+
},
|
|
234
239
|
},
|
|
235
240
|
|
|
236
241
|
// Migration 1: Add completed index
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
242
|
+
{
|
|
243
|
+
tag: "0001_add_completed",
|
|
244
|
+
migrate: async (db: IDBDatabase, transaction: IDBTransaction) => {
|
|
245
|
+
const store = transaction.objectStore("todos");
|
|
246
|
+
store.createIndex("completed", "completed", { unique: false });
|
|
247
|
+
},
|
|
240
248
|
},
|
|
241
249
|
|
|
242
250
|
// Migration 2: Transform data
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
251
|
+
{
|
|
252
|
+
tag: "0002_add_priority",
|
|
253
|
+
migrate: async (db: IDBDatabase, transaction: IDBTransaction) => {
|
|
254
|
+
const store = transaction.objectStore("todos");
|
|
255
|
+
const todos = await new Promise<any[]>((resolve, reject) => {
|
|
256
|
+
const req = store.getAll();
|
|
257
|
+
req.onsuccess = () => resolve(req.result);
|
|
258
|
+
req.onerror = () => reject(req.error);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Transform data
|
|
262
|
+
for (const todo of todos) {
|
|
263
|
+
todo.priority = todo.priority || "medium";
|
|
264
|
+
store.put(todo);
|
|
265
|
+
}
|
|
266
|
+
},
|
|
256
267
|
},
|
|
257
268
|
];
|
|
258
269
|
|
|
@@ -338,6 +349,56 @@ Useful for:
|
|
|
338
349
|
- Clearing user data on logout
|
|
339
350
|
- Testing scenarios
|
|
340
351
|
|
|
352
|
+
### generateIndexedDBMigrations
|
|
353
|
+
|
|
354
|
+
Generate IndexedDB migration files from Drizzle snapshots programmatically:
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
import { generateIndexedDBMigrations } from "@firtoz/drizzle-indexeddb";
|
|
358
|
+
|
|
359
|
+
generateIndexedDBMigrations({
|
|
360
|
+
drizzleDir: "./drizzle", // Path to Drizzle directory (default: ./drizzle)
|
|
361
|
+
outputDir: "./drizzle/indexeddb-migrations", // Output directory (default: ./drizzle/indexeddb-migrations)
|
|
362
|
+
});
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
## CLI
|
|
366
|
+
|
|
367
|
+
The package includes a CLI tool to generate IndexedDB migrations from Drizzle schema snapshots.
|
|
368
|
+
|
|
369
|
+
### Usage
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
# Generate migrations (run after drizzle-kit generate)
|
|
373
|
+
bun drizzle-indexeddb-generate
|
|
374
|
+
|
|
375
|
+
# With custom paths
|
|
376
|
+
bun drizzle-indexeddb-generate --drizzle-dir ./db/drizzle
|
|
377
|
+
bun drizzle-indexeddb-generate --output-dir ./src/migrations
|
|
378
|
+
|
|
379
|
+
# Show help
|
|
380
|
+
bun drizzle-indexeddb-generate --help
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### Options
|
|
384
|
+
|
|
385
|
+
| Option | Description | Default |
|
|
386
|
+
|--------|-------------|---------|
|
|
387
|
+
| `--drizzle-dir <path>` | Path to Drizzle directory | `./drizzle` |
|
|
388
|
+
| `--output-dir <path>` | Path to output directory | `./drizzle/indexeddb-migrations` |
|
|
389
|
+
|
|
390
|
+
### npm scripts
|
|
391
|
+
|
|
392
|
+
Add to your `package.json`:
|
|
393
|
+
|
|
394
|
+
```json
|
|
395
|
+
{
|
|
396
|
+
"scripts": {
|
|
397
|
+
"db:generate": "bun drizzle-kit generate && bun drizzle-indexeddb-generate"
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
341
402
|
## Advanced Usage
|
|
342
403
|
|
|
343
404
|
### Custom Sync Configuration
|
|
@@ -359,13 +420,13 @@ const collection = createCollection(
|
|
|
359
420
|
|
|
360
421
|
```typescript
|
|
361
422
|
try {
|
|
362
|
-
const db = await
|
|
423
|
+
const db = await migrateIndexedDBWithFunctions("my-app", migrations, true);
|
|
363
424
|
} catch (error) {
|
|
364
425
|
console.error("Migration failed:", error);
|
|
365
426
|
|
|
366
427
|
// Option 1: Delete and start fresh
|
|
367
428
|
await deleteIndexedDB("my-app");
|
|
368
|
-
const db = await
|
|
429
|
+
const db = await migrateIndexedDBWithFunctions("my-app", migrations, true);
|
|
369
430
|
|
|
370
431
|
// Option 2: Handle specific errors
|
|
371
432
|
if (error.message.includes("Primary key structure changed")) {
|
|
@@ -378,10 +439,10 @@ try {
|
|
|
378
439
|
|
|
379
440
|
```typescript
|
|
380
441
|
// Enable debug mode to see performance metrics
|
|
381
|
-
const db = await
|
|
442
|
+
const db = await migrateIndexedDBWithFunctions("my-app", migrations, true);
|
|
382
443
|
|
|
383
444
|
// Output shows:
|
|
384
|
-
// [PERF] IndexedDB
|
|
445
|
+
// [PERF] IndexedDB function migrator start for my-app
|
|
385
446
|
// [PERF] Latest applied migration index: 5 (checked 5 migrations)
|
|
386
447
|
// [PERF] Found 2 pending migrations to apply: ["add_priority", "add_category"]
|
|
387
448
|
// [PERF] Upgrade started: v5 → v7
|
|
@@ -432,9 +493,9 @@ const todoTable = syncableTable("todos", {
|
|
|
432
493
|
|
|
433
494
|
### Renaming a Column
|
|
434
495
|
|
|
435
|
-
Drizzle
|
|
496
|
+
Drizzle migrations don't track renames directly, but you can:
|
|
436
497
|
|
|
437
|
-
1.
|
|
498
|
+
1. Modify the generated migration function to handle data transformation
|
|
438
499
|
2. Or: Add new column, copy data, delete old column (3 separate migrations)
|
|
439
500
|
|
|
440
501
|
### Deleting a Table
|
|
@@ -455,8 +516,8 @@ This happens when you change the primary key of a table. IndexedDB doesn't suppo
|
|
|
455
516
|
|
|
456
517
|
### Migrations Not Applying
|
|
457
518
|
|
|
458
|
-
- Check that
|
|
459
|
-
- Verify the
|
|
519
|
+
- Check that migrations are correctly imported from `drizzle/indexeddb-migrations/`
|
|
520
|
+
- Verify the migration files exist - run `bun drizzle-indexeddb-generate` to regenerate
|
|
460
521
|
- Enable debug mode to see what's happening
|
|
461
522
|
- Check browser DevTools → Application → IndexedDB
|
|
462
523
|
|
package/package.json
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firtoz/drizzle-indexeddb",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "IndexedDB migrations powered by Drizzle ORM
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "IndexedDB migrations powered by Drizzle ORM",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"module": "./src/index.ts",
|
|
7
7
|
"types": "./src/index.ts",
|
|
8
8
|
"type": "module",
|
|
9
|
+
"bin": {
|
|
10
|
+
"drizzle-indexeddb-generate": "./src/bin/generate-migrations.ts"
|
|
11
|
+
},
|
|
9
12
|
"exports": {
|
|
10
13
|
".": {
|
|
11
14
|
"types": "./src/index.ts",
|
|
12
15
|
"import": "./src/index.ts",
|
|
13
16
|
"require": "./src/index.ts"
|
|
14
17
|
},
|
|
18
|
+
"./generate": {
|
|
19
|
+
"types": "./src/bin/generate-migrations.ts",
|
|
20
|
+
"import": "./src/bin/generate-migrations.ts",
|
|
21
|
+
"require": "./src/bin/generate-migrations.ts"
|
|
22
|
+
},
|
|
15
23
|
"./*": {
|
|
16
24
|
"types": "./src/*.ts",
|
|
17
25
|
"import": "./src/*.ts",
|
|
@@ -57,15 +65,15 @@
|
|
|
57
65
|
},
|
|
58
66
|
"dependencies": {
|
|
59
67
|
"@firtoz/drizzle-utils": "^0.2.0",
|
|
60
|
-
"@tanstack/db": "^0.5.
|
|
68
|
+
"@tanstack/db": "^0.5.10",
|
|
61
69
|
"drizzle-orm": "^0.44.7",
|
|
62
70
|
"drizzle-valibot": "^0.4.2",
|
|
63
|
-
"valibot": "^1.
|
|
71
|
+
"valibot": "^1.2.0"
|
|
64
72
|
},
|
|
65
73
|
"peerDependencies": {
|
|
66
74
|
"react": "^19.2.0"
|
|
67
75
|
},
|
|
68
76
|
"devDependencies": {
|
|
69
|
-
"@types/react": "^19.2.
|
|
77
|
+
"@types/react": "^19.2.7"
|
|
70
78
|
}
|
|
71
79
|
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI tool to generate IndexedDB migrations from Drizzle schema snapshots
|
|
4
|
+
* Run after `drizzle-kit generate` to create executable migration files
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* bun drizzle-indexeddb-generate
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
11
|
+
import { join, resolve } from "node:path";
|
|
12
|
+
import type {
|
|
13
|
+
JournalEntry,
|
|
14
|
+
Journal,
|
|
15
|
+
Snapshot,
|
|
16
|
+
TableDefinition,
|
|
17
|
+
ColumnDefinition,
|
|
18
|
+
IndexDefinition,
|
|
19
|
+
} from "@firtoz/drizzle-utils";
|
|
20
|
+
import type { Migration, MigrationOperation } from "../function-migrator";
|
|
21
|
+
|
|
22
|
+
interface GenerateOptions {
|
|
23
|
+
drizzleDir?: string;
|
|
24
|
+
outputDir?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function generateMigration(
|
|
28
|
+
_entry: JournalEntry,
|
|
29
|
+
snapshot: Snapshot,
|
|
30
|
+
prevSnapshot: Snapshot | null,
|
|
31
|
+
): Migration {
|
|
32
|
+
const operations: MigrationOperation[] = [];
|
|
33
|
+
|
|
34
|
+
const currentTables: Record<string, TableDefinition> = snapshot.tables || {};
|
|
35
|
+
const previousTables: Record<string, TableDefinition> =
|
|
36
|
+
prevSnapshot?.tables || {};
|
|
37
|
+
|
|
38
|
+
// Find new tables
|
|
39
|
+
for (const [tableName, tableDef] of Object.entries(currentTables)) {
|
|
40
|
+
if (!previousTables[tableName]) {
|
|
41
|
+
// Find primary key
|
|
42
|
+
const pkColumn = Object.values(
|
|
43
|
+
tableDef.columns as Record<string, ColumnDefinition>,
|
|
44
|
+
).find((col) => col.primaryKey);
|
|
45
|
+
|
|
46
|
+
const indexes: Array<{
|
|
47
|
+
name: string;
|
|
48
|
+
keyPath: string | string[];
|
|
49
|
+
unique?: boolean;
|
|
50
|
+
}> = [];
|
|
51
|
+
|
|
52
|
+
// Collect indexes
|
|
53
|
+
for (const [indexName, indexDef] of Object.entries(
|
|
54
|
+
tableDef.indexes || {},
|
|
55
|
+
)) {
|
|
56
|
+
indexes.push({
|
|
57
|
+
name: indexName,
|
|
58
|
+
keyPath:
|
|
59
|
+
indexDef.columns.length === 1
|
|
60
|
+
? indexDef.columns[0]
|
|
61
|
+
: indexDef.columns,
|
|
62
|
+
unique: indexDef.isUnique,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
operations.push({
|
|
67
|
+
type: "createTable",
|
|
68
|
+
name: tableName,
|
|
69
|
+
keyPath: pkColumn?.name,
|
|
70
|
+
autoIncrement: pkColumn?.autoincrement ?? false,
|
|
71
|
+
indexes: indexes.length > 0 ? indexes : undefined,
|
|
72
|
+
});
|
|
73
|
+
} else {
|
|
74
|
+
// Table exists, check for index changes
|
|
75
|
+
const prevTableDef: TableDefinition = previousTables[tableName];
|
|
76
|
+
const newIndexes: Record<string, IndexDefinition> =
|
|
77
|
+
tableDef.indexes || {};
|
|
78
|
+
const oldIndexes: Record<string, IndexDefinition> =
|
|
79
|
+
prevTableDef.indexes || {};
|
|
80
|
+
|
|
81
|
+
// Add new indexes
|
|
82
|
+
for (const [indexName, indexDef] of Object.entries(newIndexes)) {
|
|
83
|
+
if (!oldIndexes[indexName]) {
|
|
84
|
+
operations.push({
|
|
85
|
+
type: "createIndex",
|
|
86
|
+
tableName,
|
|
87
|
+
indexName,
|
|
88
|
+
keyPath:
|
|
89
|
+
indexDef.columns.length === 1
|
|
90
|
+
? indexDef.columns[0]
|
|
91
|
+
: indexDef.columns,
|
|
92
|
+
unique: indexDef.isUnique,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Delete removed indexes
|
|
98
|
+
for (const indexName of Object.keys(oldIndexes)) {
|
|
99
|
+
if (!newIndexes[indexName]) {
|
|
100
|
+
operations.push({
|
|
101
|
+
type: "deleteIndex",
|
|
102
|
+
tableName,
|
|
103
|
+
indexName,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Find deleted tables
|
|
111
|
+
for (const tableName of Object.keys(previousTables)) {
|
|
112
|
+
if (!currentTables[tableName]) {
|
|
113
|
+
operations.push({
|
|
114
|
+
type: "deleteTable",
|
|
115
|
+
name: tableName,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return operations;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function migrationToCode(
|
|
124
|
+
migration: Migration,
|
|
125
|
+
idx: number,
|
|
126
|
+
tag: string,
|
|
127
|
+
): string {
|
|
128
|
+
const migrationName = tag.replace(/^\d+_/, "").replace(/_/g, " ");
|
|
129
|
+
const lines: string[] = [];
|
|
130
|
+
|
|
131
|
+
lines.push(
|
|
132
|
+
`import type { Migration } from "@firtoz/drizzle-indexeddb";`,
|
|
133
|
+
``,
|
|
134
|
+
`/**`,
|
|
135
|
+
` * Migration: ${migrationName}`,
|
|
136
|
+
` * Generated from: ${tag}`,
|
|
137
|
+
` */`,
|
|
138
|
+
`export const migrate_${idx.toString().padStart(4, "0")}: Migration = ${JSON.stringify(migration, null, "\t")};`,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
return lines.join("\n");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function generateIndexedDBMigrations(
|
|
145
|
+
options: GenerateOptions = {},
|
|
146
|
+
): void {
|
|
147
|
+
const cwd = process.cwd();
|
|
148
|
+
const drizzleDir = resolve(cwd, options.drizzleDir || "./drizzle");
|
|
149
|
+
const metaDir = join(drizzleDir, "meta");
|
|
150
|
+
const journalPath = join(metaDir, "_journal.json");
|
|
151
|
+
const outputDir = resolve(
|
|
152
|
+
cwd,
|
|
153
|
+
options.outputDir || join(drizzleDir, "indexeddb-migrations"),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const startTime = performance.now();
|
|
157
|
+
console.log(`[drizzle-indexeddb] Starting migration generation...`);
|
|
158
|
+
|
|
159
|
+
// Read the journal
|
|
160
|
+
if (!existsSync(journalPath)) {
|
|
161
|
+
console.error(
|
|
162
|
+
`[drizzle-indexeddb] Error: Journal not found at ${journalPath}`,
|
|
163
|
+
);
|
|
164
|
+
console.error(
|
|
165
|
+
`[drizzle-indexeddb] Make sure to run 'drizzle-kit generate' first`,
|
|
166
|
+
);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const journalContent = readFileSync(journalPath, "utf-8");
|
|
171
|
+
const journal: Journal = JSON.parse(journalContent);
|
|
172
|
+
|
|
173
|
+
console.log(`[drizzle-indexeddb] Found ${journal.entries.length} migrations`);
|
|
174
|
+
|
|
175
|
+
// Create output directory
|
|
176
|
+
if (!existsSync(outputDir)) {
|
|
177
|
+
mkdirSync(outputDir, { recursive: true });
|
|
178
|
+
console.log(`[drizzle-indexeddb] Created output directory: ${outputDir}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const migrationImports: string[] = [];
|
|
182
|
+
const migrationNames: string[] = [];
|
|
183
|
+
|
|
184
|
+
// Load all snapshots and generate migrations
|
|
185
|
+
const snapshots: Snapshot[] = [];
|
|
186
|
+
|
|
187
|
+
for (const entry of journal.entries) {
|
|
188
|
+
const fileName = `${entry.idx.toString().padStart(4, "0")}_snapshot.json`;
|
|
189
|
+
const snapshotPath = join(metaDir, fileName);
|
|
190
|
+
|
|
191
|
+
// Load snapshot
|
|
192
|
+
if (!existsSync(snapshotPath)) {
|
|
193
|
+
console.error(
|
|
194
|
+
`[drizzle-indexeddb] Error: Snapshot not found at ${snapshotPath}`,
|
|
195
|
+
);
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const snapshotContent = readFileSync(snapshotPath, "utf-8");
|
|
200
|
+
const snapshot: Snapshot = JSON.parse(snapshotContent);
|
|
201
|
+
snapshots.push(snapshot);
|
|
202
|
+
|
|
203
|
+
// Generate migration
|
|
204
|
+
const prevSnapshot = entry.idx > 0 ? snapshots[entry.idx - 1] : null;
|
|
205
|
+
const migration = generateMigration(entry, snapshot, prevSnapshot);
|
|
206
|
+
const migrationCode = migrationToCode(migration, entry.idx, entry.tag);
|
|
207
|
+
|
|
208
|
+
const migrationFileName = `${entry.tag}.ts`;
|
|
209
|
+
const migrationPath = join(outputDir, migrationFileName);
|
|
210
|
+
writeFileSync(migrationPath, migrationCode, "utf-8");
|
|
211
|
+
|
|
212
|
+
// Add to index imports
|
|
213
|
+
const migrationName = `migrate_${entry.idx.toString().padStart(4, "0")}`;
|
|
214
|
+
migrationImports.push(`import { ${migrationName} } from './${entry.tag}';`);
|
|
215
|
+
migrationNames.push(migrationName);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Generate index.ts for migrations
|
|
219
|
+
const indexContent = `import type { Migration } from "@firtoz/drizzle-indexeddb";
|
|
220
|
+
|
|
221
|
+
${migrationImports.join("\n")}
|
|
222
|
+
|
|
223
|
+
export const migrations: Migration[] = [
|
|
224
|
+
\t${migrationNames.join(",\n\t")}
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
export default migrations;
|
|
228
|
+
`;
|
|
229
|
+
|
|
230
|
+
writeFileSync(join(outputDir, "index.ts"), indexContent, "utf-8");
|
|
231
|
+
console.log(`[drizzle-indexeddb] ✓ Generated ${join(outputDir, "index.ts")}`);
|
|
232
|
+
|
|
233
|
+
const endTime = performance.now();
|
|
234
|
+
const totalTime = endTime - startTime;
|
|
235
|
+
|
|
236
|
+
console.log(`[drizzle-indexeddb] Migrations: ${migrationNames.join(", ")}`);
|
|
237
|
+
console.log(
|
|
238
|
+
`[drizzle-indexeddb] ✓ Complete! Generated ${journal.entries.length} migrations in ${totalTime.toFixed(2)}ms`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// CLI entry point
|
|
243
|
+
function main(): void {
|
|
244
|
+
const args = process.argv.slice(2);
|
|
245
|
+
const command = args[0];
|
|
246
|
+
|
|
247
|
+
if (command === "generate" || command === undefined) {
|
|
248
|
+
// Parse options
|
|
249
|
+
const options: GenerateOptions = {};
|
|
250
|
+
|
|
251
|
+
for (let i = 1; i < args.length; i++) {
|
|
252
|
+
const arg = args[i];
|
|
253
|
+
if (arg === "--drizzle-dir" && args[i + 1]) {
|
|
254
|
+
options.drizzleDir = args[++i];
|
|
255
|
+
} else if (arg === "--output-dir" && args[i + 1]) {
|
|
256
|
+
options.outputDir = args[++i];
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
generateIndexedDBMigrations(options);
|
|
261
|
+
} else if (command === "--help" || command === "-h") {
|
|
262
|
+
console.log(`
|
|
263
|
+
drizzle-indexeddb-generate - Generate IndexedDB migrations from Drizzle schema
|
|
264
|
+
|
|
265
|
+
Usage:
|
|
266
|
+
bun drizzle-indexeddb-generate [options]
|
|
267
|
+
|
|
268
|
+
Options:
|
|
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
|
+
}
|
|
284
|
+
|
|
285
|
+
// Only run CLI when executed directly (not when imported)
|
|
286
|
+
if (import.meta.main) {
|
|
287
|
+
main();
|
|
288
|
+
}
|