@firtoz/drizzle-indexeddb 0.1.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 +139 -0
- package/README.md +477 -0
- package/package.json +71 -0
- package/src/collections/indexeddb-collection.ts +915 -0
- package/src/context/useDrizzleIndexedDB.ts +36 -0
- package/src/function-migrator.ts +234 -0
- package/src/index.ts +30 -0
- package/src/snapshot-migrator.ts +420 -0
- package/src/utils.ts +15 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# @firtoz/drizzle-indexeddb
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#22](https://github.com/firtoz/fullstack-toolkit/pull/22) [`05e88e7`](https://github.com/firtoz/fullstack-toolkit/commit/05e88e775f262488d1da2b579eadd560cee2eba9) Thanks [@firtoz](https://github.com/firtoz)! - Initial release of `@firtoz/drizzle-indexeddb` - TanStack DB collections backed by IndexedDB with automatic migrations powered by Drizzle ORM snapshots.
|
|
8
|
+
|
|
9
|
+
> **⚠️ 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.
|
|
10
|
+
|
|
11
|
+
**Note:** This package currently builds on top of Drizzle's SQLite integration (using `drizzle-orm/sqlite-core` types and snapshots) until Drizzle adds native IndexedDB support. The migration system reads Drizzle's SQLite snapshots and translates them into IndexedDB object stores and indexes.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
### TanStack DB Collections (Primary Feature)
|
|
16
|
+
|
|
17
|
+
**`indexedDBCollectionOptions(config)`** - The main feature: Create reactive TanStack DB collections backed by IndexedDB:
|
|
18
|
+
|
|
19
|
+
- Full CRUD operations with type safety
|
|
20
|
+
- Reactive subscriptions to data changes
|
|
21
|
+
- Soft delete support (respects `deletedAt` column)
|
|
22
|
+
- Automatic pagination and sorting
|
|
23
|
+
- Query optimization with IndexedDB indexes
|
|
24
|
+
- Sync configuration for real-time updates
|
|
25
|
+
- Works seamlessly with React hooks
|
|
26
|
+
|
|
27
|
+
### Snapshot-Based Migration
|
|
28
|
+
|
|
29
|
+
**`migrateIndexedDB(dbName, config, debug?)`** - Automatically migrates IndexedDB databases using Drizzle snapshot files:
|
|
30
|
+
|
|
31
|
+
- Reads Drizzle journal and snapshot files
|
|
32
|
+
- Tracks applied migrations in `__drizzle_migrations` store
|
|
33
|
+
- Creates/updates object stores and indexes based on schema changes
|
|
34
|
+
- Handles table deletion, index changes, and schema evolution
|
|
35
|
+
- Incremental migrations - only applies pending changes
|
|
36
|
+
- Validates primary key structure changes (requires manual migration if keys change)
|
|
37
|
+
- Performance logging when debug mode is enabled
|
|
38
|
+
|
|
39
|
+
### Function-Based Migration
|
|
40
|
+
|
|
41
|
+
**`migrateIndexedDBWithFunctions(dbName, migrations, debug?)`** - Run migrations using custom migration functions:
|
|
42
|
+
|
|
43
|
+
- Execute custom migration logic for complex schema changes
|
|
44
|
+
- Full control over IndexedDB transaction and database during migration
|
|
45
|
+
- Tracks applied migrations automatically
|
|
46
|
+
- Ideal for data transformations and complex schema changes
|
|
47
|
+
|
|
48
|
+
### React Context & Hooks
|
|
49
|
+
|
|
50
|
+
**`DrizzleIndexedDBProvider`** - React context provider for IndexedDB:
|
|
51
|
+
|
|
52
|
+
- Manages IndexedDB connection lifecycle
|
|
53
|
+
- Provides collection access with automatic caching
|
|
54
|
+
- Reference counting for memory management
|
|
55
|
+
|
|
56
|
+
**`useDrizzleIndexedDB()`** - React hook for accessing IndexedDB context:
|
|
57
|
+
|
|
58
|
+
- Get collection instances with type safety
|
|
59
|
+
- Automatic ref counting for cleanup
|
|
60
|
+
|
|
61
|
+
**`useIndexedDBCollection(tableName)`** - React hook for using specific collections:
|
|
62
|
+
|
|
63
|
+
- Automatic ref counting and cleanup
|
|
64
|
+
- Type-safe collection access
|
|
65
|
+
|
|
66
|
+
### Utilities
|
|
67
|
+
|
|
68
|
+
**`deleteIndexedDB(dbName)`** - Utility to completely delete an IndexedDB database
|
|
69
|
+
|
|
70
|
+
## Example
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { migrateIndexedDB } from "@firtoz/drizzle-indexeddb";
|
|
74
|
+
import journal from "./drizzle/journal.json";
|
|
75
|
+
import * as snapshots from "./drizzle/snapshots";
|
|
76
|
+
|
|
77
|
+
// Migrate database using Drizzle snapshots
|
|
78
|
+
const db = await migrateIndexedDB(
|
|
79
|
+
"my-app-db",
|
|
80
|
+
{
|
|
81
|
+
journal,
|
|
82
|
+
snapshots,
|
|
83
|
+
},
|
|
84
|
+
true
|
|
85
|
+
); // debug mode
|
|
86
|
+
|
|
87
|
+
// Use with TanStack DB
|
|
88
|
+
import { createCollection } from "@tanstack/db";
|
|
89
|
+
import { indexedDBCollectionOptions } from "@firtoz/drizzle-indexeddb";
|
|
90
|
+
|
|
91
|
+
const todosCollection = createCollection(
|
|
92
|
+
indexedDBCollectionOptions({
|
|
93
|
+
db,
|
|
94
|
+
tableName: "todos",
|
|
95
|
+
})
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// React integration
|
|
99
|
+
import {
|
|
100
|
+
DrizzleIndexedDBProvider,
|
|
101
|
+
useDrizzleIndexedDB,
|
|
102
|
+
} from "@firtoz/drizzle-indexeddb";
|
|
103
|
+
|
|
104
|
+
function App() {
|
|
105
|
+
return (
|
|
106
|
+
<DrizzleIndexedDBProvider db={db} schema={schema}>
|
|
107
|
+
<TodoList />
|
|
108
|
+
</DrizzleIndexedDBProvider>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function TodoList() {
|
|
113
|
+
const { getCollection } = useDrizzleIndexedDB();
|
|
114
|
+
const todos = getCollection("todos");
|
|
115
|
+
|
|
116
|
+
// Use collection with TanStack DB hooks...
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Migration Workflow
|
|
121
|
+
|
|
122
|
+
1. Generate Drizzle snapshots: `drizzle-kit generate`
|
|
123
|
+
2. Import journal and snapshots
|
|
124
|
+
3. Call `migrateIndexedDB()` on app startup
|
|
125
|
+
4. Database automatically updates to latest schema
|
|
126
|
+
|
|
127
|
+
## Dependencies
|
|
128
|
+
|
|
129
|
+
- `@firtoz/drizzle-utils` (workspace)
|
|
130
|
+
- `drizzle-orm`
|
|
131
|
+
- `drizzle-valibot`
|
|
132
|
+
- `@tanstack/db`
|
|
133
|
+
- `valibot`
|
|
134
|
+
- `react` (peer dependency)
|
|
135
|
+
|
|
136
|
+
### Patch Changes
|
|
137
|
+
|
|
138
|
+
- Updated dependencies [[`05e88e7`](https://github.com/firtoz/fullstack-toolkit/commit/05e88e775f262488d1da2b579eadd560cee2eba9)]:
|
|
139
|
+
- @firtoz/drizzle-utils@0.1.0
|
package/README.md
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
# @firtoz/drizzle-indexeddb
|
|
2
|
+
|
|
3
|
+
TanStack DB collections backed by IndexedDB with automatic migrations powered by Drizzle ORM snapshots. Build reactive, type-safe IndexedDB applications with the power of Drizzle's schema management.
|
|
4
|
+
|
|
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
|
+
|
|
7
|
+
> **Note:** This package currently builds on top of Drizzle's SQLite integration (using `drizzle-orm/sqlite-core` types and snapshots) until Drizzle adds native IndexedDB support. The migration system reads Drizzle's SQLite snapshots and translates them into IndexedDB object stores and indexes.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @firtoz/drizzle-indexeddb @firtoz/drizzle-utils drizzle-orm @tanstack/db
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- ⚡ **TanStack DB collections** - Reactive collections with type safety (primary feature)
|
|
18
|
+
- 🎯 **Type-safe** - Full TypeScript support with automatic type inference
|
|
19
|
+
- 🔍 **Query optimization** - Leverage IndexedDB indexes for fast queries
|
|
20
|
+
- 📦 **Soft deletes** - Built-in support for `deletedAt` column
|
|
21
|
+
- ⚛️ **React hooks** - Provider and hooks for easy React integration
|
|
22
|
+
- 🔄 **Snapshot-based migrations** - Use Drizzle's generated snapshots to migrate IndexedDB
|
|
23
|
+
- 📝 **Function-based migrations** - Write custom migration functions for complex changes
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
### 1. Setup Drizzle Schema
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
// schema.ts
|
|
31
|
+
import { syncableTable } from "@firtoz/drizzle-utils";
|
|
32
|
+
import { text, integer } from "drizzle-orm/sqlite-core";
|
|
33
|
+
|
|
34
|
+
export const todoTable = syncableTable("todos", {
|
|
35
|
+
title: text("title").notNull(),
|
|
36
|
+
completed: integer("completed", { mode: "boolean" }).notNull().default(false),
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 2. Generate Migrations
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Generate Drizzle snapshots
|
|
44
|
+
drizzle-kit generate
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 3. Migrate IndexedDB
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
// db.ts
|
|
51
|
+
import { migrateIndexedDB } from "@firtoz/drizzle-indexeddb";
|
|
52
|
+
import journal from "./drizzle/meta/_journal.json";
|
|
53
|
+
import * as snapshots from "./drizzle/snapshots";
|
|
54
|
+
|
|
55
|
+
export const db = await migrateIndexedDB("my-app", {
|
|
56
|
+
journal,
|
|
57
|
+
snapshots,
|
|
58
|
+
}, true); // Enable debug logging
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 4. Use with React
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// App.tsx
|
|
65
|
+
import { DrizzleIndexedDBProvider, useIndexedDBCollection } from "@firtoz/drizzle-indexeddb";
|
|
66
|
+
import { createCollection } from "@tanstack/db";
|
|
67
|
+
|
|
68
|
+
function App() {
|
|
69
|
+
return (
|
|
70
|
+
<DrizzleIndexedDBProvider db={db} schema={schema}>
|
|
71
|
+
<TodoList />
|
|
72
|
+
</DrizzleIndexedDBProvider>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function TodoList() {
|
|
77
|
+
const collection = useIndexedDBCollection("todos");
|
|
78
|
+
const [todos] = collection.useStore();
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<ul>
|
|
82
|
+
{todos.map(todo => (
|
|
83
|
+
<li key={todo.id}>{todo.title}</li>
|
|
84
|
+
))}
|
|
85
|
+
</ul>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## TanStack DB Collections
|
|
91
|
+
|
|
92
|
+
The primary feature of this package: Create reactive, type-safe collections backed by IndexedDB.
|
|
93
|
+
|
|
94
|
+
### Basic Usage
|
|
95
|
+
|
|
96
|
+
Create reactive collections backed by IndexedDB:
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import { createCollection } from "@tanstack/db";
|
|
100
|
+
import { indexedDBCollectionOptions } from "@firtoz/drizzle-indexeddb";
|
|
101
|
+
|
|
102
|
+
const todosCollection = createCollection(
|
|
103
|
+
indexedDBCollectionOptions({
|
|
104
|
+
db,
|
|
105
|
+
tableName: "todos",
|
|
106
|
+
syncMode: "on-demand", // or "realtime"
|
|
107
|
+
})
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// Subscribe to changes
|
|
111
|
+
const unsubscribe = todosCollection.subscribe((todos) => {
|
|
112
|
+
console.log("Todos updated:", todos);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// CRUD operations
|
|
116
|
+
await todosCollection.insert({
|
|
117
|
+
title: "Buy milk",
|
|
118
|
+
completed: false,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
await todosCollection.update(todoId, {
|
|
122
|
+
completed: true,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await todosCollection.delete(todoId); // Soft delete (sets deletedAt)
|
|
126
|
+
|
|
127
|
+
// Query with filters
|
|
128
|
+
const completedTodos = await todosCollection.find({
|
|
129
|
+
where: { completed: true },
|
|
130
|
+
orderBy: { createdAt: "desc" },
|
|
131
|
+
limit: 10,
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Collection Options
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
interface IndexedDBCollectionConfig {
|
|
139
|
+
db: IDBDatabase;
|
|
140
|
+
tableName: string;
|
|
141
|
+
syncMode?: "on-demand" | "realtime";
|
|
142
|
+
debug?: boolean;
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Supported Operations
|
|
147
|
+
|
|
148
|
+
- **Insert** - Add new records
|
|
149
|
+
- **Update** - Modify existing records
|
|
150
|
+
- **Delete** - Soft delete (sets `deletedAt`) or hard delete
|
|
151
|
+
- **Find** - Query with filters, sorting, pagination
|
|
152
|
+
- **Subscribe** - React to data changes
|
|
153
|
+
|
|
154
|
+
### Query Features
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
// Filtering
|
|
158
|
+
collection.find({
|
|
159
|
+
where: {
|
|
160
|
+
completed: false,
|
|
161
|
+
title: { contains: "urgent" },
|
|
162
|
+
priority: { in: ["high", "critical"] },
|
|
163
|
+
createdAt: { gt: yesterday },
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Sorting
|
|
168
|
+
collection.find({
|
|
169
|
+
orderBy: { createdAt: "desc" }
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Pagination
|
|
173
|
+
collection.find({
|
|
174
|
+
limit: 10,
|
|
175
|
+
offset: 20,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Soft delete filtering (automatic)
|
|
179
|
+
// By default, records with deletedAt !== null are excluded
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Migration Methods
|
|
183
|
+
|
|
184
|
+
### Snapshot-Based Migration
|
|
185
|
+
|
|
186
|
+
Use Drizzle's snapshot files to automatically migrate your IndexedDB schema:
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
import { migrateIndexedDB } from "@firtoz/drizzle-indexeddb";
|
|
190
|
+
import journal from "./drizzle/meta/_journal.json";
|
|
191
|
+
import * as snapshots from "./drizzle/snapshots";
|
|
192
|
+
|
|
193
|
+
const db = await migrateIndexedDB("my-app-db", {
|
|
194
|
+
journal,
|
|
195
|
+
snapshots,
|
|
196
|
+
}, true); // debug flag
|
|
197
|
+
|
|
198
|
+
console.log("Database migrated successfully!");
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Features:**
|
|
202
|
+
- Automatically creates/updates object stores
|
|
203
|
+
- Manages indexes based on Drizzle schema
|
|
204
|
+
- Handles table deletion
|
|
205
|
+
- Tracks applied migrations
|
|
206
|
+
- Validates primary key changes
|
|
207
|
+
- Incremental migrations (only applies pending changes)
|
|
208
|
+
|
|
209
|
+
**Migration Tracking:**
|
|
210
|
+
|
|
211
|
+
Migrations are tracked in the `__drizzle_migrations` object store:
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
interface MigrationRecord {
|
|
215
|
+
id: number; // Migration index
|
|
216
|
+
tag: string; // Migration name
|
|
217
|
+
when: number; // Migration timestamp
|
|
218
|
+
appliedAt: number; // When it was applied
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Function-Based Migration
|
|
223
|
+
|
|
224
|
+
For complex migrations that require custom logic:
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
import { migrateIndexedDBWithFunctions } from "@firtoz/drizzle-indexeddb";
|
|
228
|
+
|
|
229
|
+
const migrations = [
|
|
230
|
+
// Migration 0: Initial schema
|
|
231
|
+
async (db: IDBDatabase, transaction: IDBTransaction) => {
|
|
232
|
+
const store = db.createObjectStore("todos", { keyPath: "id" });
|
|
233
|
+
store.createIndex("title", "title", { unique: false });
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
// Migration 1: Add completed index
|
|
237
|
+
async (db: IDBDatabase, transaction: IDBTransaction) => {
|
|
238
|
+
const store = transaction.objectStore("todos");
|
|
239
|
+
store.createIndex("completed", "completed", { unique: false });
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
// Migration 2: Transform data
|
|
243
|
+
async (db: IDBDatabase, transaction: IDBTransaction) => {
|
|
244
|
+
const store = transaction.objectStore("todos");
|
|
245
|
+
const todos = await new Promise<any[]>((resolve, reject) => {
|
|
246
|
+
const req = store.getAll();
|
|
247
|
+
req.onsuccess = () => resolve(req.result);
|
|
248
|
+
req.onerror = () => reject(req.error);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Transform data
|
|
252
|
+
for (const todo of todos) {
|
|
253
|
+
todo.priority = todo.priority || "medium";
|
|
254
|
+
store.put(todo);
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
const db = await migrateIndexedDBWithFunctions("my-app-db", migrations, true);
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## React Integration
|
|
263
|
+
|
|
264
|
+
### DrizzleIndexedDBProvider
|
|
265
|
+
|
|
266
|
+
Wrap your app with the provider:
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
import { DrizzleIndexedDBProvider } from "@firtoz/drizzle-indexeddb";
|
|
270
|
+
|
|
271
|
+
function App() {
|
|
272
|
+
return (
|
|
273
|
+
<DrizzleIndexedDBProvider db={db} schema={schema}>
|
|
274
|
+
<YourApp />
|
|
275
|
+
</DrizzleIndexedDBProvider>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### useDrizzleIndexedDB
|
|
281
|
+
|
|
282
|
+
Access the context:
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
import { useDrizzleIndexedDB } from "@firtoz/drizzle-indexeddb";
|
|
286
|
+
|
|
287
|
+
function MyComponent() {
|
|
288
|
+
const { getCollection } = useDrizzleIndexedDB();
|
|
289
|
+
|
|
290
|
+
const todosCollection = getCollection("todos");
|
|
291
|
+
const usersCollection = getCollection("users");
|
|
292
|
+
|
|
293
|
+
// Use collections...
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**Features:**
|
|
298
|
+
- Collection caching (same collection instance for same table)
|
|
299
|
+
- Reference counting for memory management
|
|
300
|
+
- Type-safe collection access
|
|
301
|
+
|
|
302
|
+
### useIndexedDBCollection
|
|
303
|
+
|
|
304
|
+
Hook for a specific collection:
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
import { useIndexedDBCollection } from "@firtoz/drizzle-indexeddb";
|
|
308
|
+
|
|
309
|
+
function TodoList() {
|
|
310
|
+
const collection = useIndexedDBCollection("todos");
|
|
311
|
+
|
|
312
|
+
// Automatic ref counting and cleanup
|
|
313
|
+
useEffect(() => {
|
|
314
|
+
return () => {
|
|
315
|
+
// Collection automatically cleaned up when component unmounts
|
|
316
|
+
};
|
|
317
|
+
}, []);
|
|
318
|
+
|
|
319
|
+
// Use collection...
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
## Utilities
|
|
324
|
+
|
|
325
|
+
### deleteIndexedDB
|
|
326
|
+
|
|
327
|
+
Completely delete an IndexedDB database:
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
import { deleteIndexedDB } from "@firtoz/drizzle-indexeddb";
|
|
331
|
+
|
|
332
|
+
await deleteIndexedDB("my-app-db");
|
|
333
|
+
console.log("Database deleted!");
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Useful for:
|
|
337
|
+
- Resetting the database during development
|
|
338
|
+
- Clearing user data on logout
|
|
339
|
+
- Testing scenarios
|
|
340
|
+
|
|
341
|
+
## Advanced Usage
|
|
342
|
+
|
|
343
|
+
### Custom Sync Configuration
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
import { indexedDBCollectionOptions } from "@firtoz/drizzle-indexeddb";
|
|
347
|
+
|
|
348
|
+
const collection = createCollection(
|
|
349
|
+
indexedDBCollectionOptions({
|
|
350
|
+
db,
|
|
351
|
+
tableName: "todos",
|
|
352
|
+
syncMode: "realtime", // Subscribe to changes automatically
|
|
353
|
+
debug: true, // Enable debug logging
|
|
354
|
+
})
|
|
355
|
+
);
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Handling Migration Errors
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
try {
|
|
362
|
+
const db = await migrateIndexedDB("my-app", config, true);
|
|
363
|
+
} catch (error) {
|
|
364
|
+
console.error("Migration failed:", error);
|
|
365
|
+
|
|
366
|
+
// Option 1: Delete and start fresh
|
|
367
|
+
await deleteIndexedDB("my-app");
|
|
368
|
+
const db = await migrateIndexedDB("my-app", config, true);
|
|
369
|
+
|
|
370
|
+
// Option 2: Handle specific errors
|
|
371
|
+
if (error.message.includes("Primary key structure changed")) {
|
|
372
|
+
// Guide user to export data, delete DB, and reimport
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Performance Optimization
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
// Enable debug mode to see performance metrics
|
|
381
|
+
const db = await migrateIndexedDB("my-app", config, true);
|
|
382
|
+
|
|
383
|
+
// Output shows:
|
|
384
|
+
// [PERF] IndexedDB snapshot migrator start for my-app
|
|
385
|
+
// [PERF] Latest applied migration index: 5 (checked 5 migrations)
|
|
386
|
+
// [PERF] Found 2 pending migrations to apply: ["add_priority", "add_category"]
|
|
387
|
+
// [PERF] Upgrade started: v5 → v7
|
|
388
|
+
// [PERF] Creating object store: categories
|
|
389
|
+
// [PERF] Creating index: name on categories
|
|
390
|
+
// [PERF] Migration 5 complete
|
|
391
|
+
// [PERF] Migration 6 complete
|
|
392
|
+
// [PERF] All 2 migrations applied successfully
|
|
393
|
+
// [PERF] Migrator complete - database ready
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
## Schema Changes
|
|
397
|
+
|
|
398
|
+
### Adding a Column
|
|
399
|
+
|
|
400
|
+
Just update your schema and regenerate:
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
// Before
|
|
404
|
+
const todoTable = syncableTable("todos", {
|
|
405
|
+
title: text("title").notNull(),
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// After
|
|
409
|
+
const todoTable = syncableTable("todos", {
|
|
410
|
+
title: text("title").notNull(),
|
|
411
|
+
priority: text("priority").notNull().default("medium"),
|
|
412
|
+
});
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
```bash
|
|
416
|
+
drizzle-kit generate
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
The migrator handles it automatically!
|
|
420
|
+
|
|
421
|
+
### Adding an Index
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
424
|
+
const todoTable = syncableTable("todos", {
|
|
425
|
+
title: text("title").notNull(),
|
|
426
|
+
completed: integer("completed", { mode: "boolean" }),
|
|
427
|
+
}, (table) => [
|
|
428
|
+
index("title_idx").on(table.title),
|
|
429
|
+
index("completed_idx").on(table.completed),
|
|
430
|
+
]);
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### Renaming a Column
|
|
434
|
+
|
|
435
|
+
Drizzle snapshots don't track renames directly, but you can:
|
|
436
|
+
|
|
437
|
+
1. Use function-based migrations to handle data transformation
|
|
438
|
+
2. Or: Add new column, copy data, delete old column (3 separate migrations)
|
|
439
|
+
|
|
440
|
+
### Deleting a Table
|
|
441
|
+
|
|
442
|
+
Remove from schema and regenerate - the migrator will delete the object store.
|
|
443
|
+
|
|
444
|
+
## Troubleshooting
|
|
445
|
+
|
|
446
|
+
### "Primary key structure changed" Error
|
|
447
|
+
|
|
448
|
+
This happens when you change the primary key of a table. IndexedDB doesn't support changing keyPath after creation.
|
|
449
|
+
|
|
450
|
+
**Solution:**
|
|
451
|
+
1. Export your data
|
|
452
|
+
2. Delete the database: `await deleteIndexedDB("my-app")`
|
|
453
|
+
3. Re-run migrations
|
|
454
|
+
4. Import your data
|
|
455
|
+
|
|
456
|
+
### Migrations Not Applying
|
|
457
|
+
|
|
458
|
+
- Check that journal and snapshots are correctly imported
|
|
459
|
+
- Verify the snapshot files exist in `drizzle/snapshots/`
|
|
460
|
+
- Enable debug mode to see what's happening
|
|
461
|
+
- Check browser DevTools → Application → IndexedDB
|
|
462
|
+
|
|
463
|
+
### Performance Issues
|
|
464
|
+
|
|
465
|
+
- Add indexes to frequently queried columns
|
|
466
|
+
- Use `syncMode: "on-demand"` for collections that don't need real-time updates
|
|
467
|
+
- Consider pagination for large datasets
|
|
468
|
+
- Use `deletedAt` soft deletes instead of hard deletes for better performance
|
|
469
|
+
|
|
470
|
+
## License
|
|
471
|
+
|
|
472
|
+
MIT
|
|
473
|
+
|
|
474
|
+
## Author
|
|
475
|
+
|
|
476
|
+
Firtina Ozbalikchi <firtoz@github.com>
|
|
477
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@firtoz/drizzle-indexeddb",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "IndexedDB migrations powered by Drizzle ORM snapshots",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"module": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./src/index.ts",
|
|
12
|
+
"import": "./src/index.ts",
|
|
13
|
+
"require": "./src/index.ts"
|
|
14
|
+
},
|
|
15
|
+
"./*": {
|
|
16
|
+
"types": "./src/*.ts",
|
|
17
|
+
"import": "./src/*.ts",
|
|
18
|
+
"require": "./src/*.ts"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"src/**/*.ts",
|
|
23
|
+
"!src/**/*.test.ts",
|
|
24
|
+
"README.md",
|
|
25
|
+
"CHANGELOG.md"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"typecheck": "tsc --noEmit -p ./tsconfig.json",
|
|
29
|
+
"lint": "biome check --write src",
|
|
30
|
+
"lint:ci": "biome ci src",
|
|
31
|
+
"format": "biome format src --write",
|
|
32
|
+
"test": "bun test --pass-with-no-tests",
|
|
33
|
+
"test:watch": "bun test --watch"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"typescript",
|
|
37
|
+
"indexeddb",
|
|
38
|
+
"drizzle",
|
|
39
|
+
"migrations"
|
|
40
|
+
],
|
|
41
|
+
"author": "Firtina Ozbalikchi <firtoz@github.com>",
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"homepage": "https://github.com/firtoz/fullstack-toolkit#readme",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "https://github.com/firtoz/fullstack-toolkit.git",
|
|
47
|
+
"directory": "packages/drizzle-indexeddb"
|
|
48
|
+
},
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/firtoz/fullstack-toolkit/issues"
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=18.0.0"
|
|
54
|
+
},
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@firtoz/drizzle-utils": "^0.1.0",
|
|
60
|
+
"@tanstack/db": "^0.5.0",
|
|
61
|
+
"drizzle-orm": "^0.44.7",
|
|
62
|
+
"drizzle-valibot": "^0.4.2",
|
|
63
|
+
"valibot": "^1.0.0"
|
|
64
|
+
},
|
|
65
|
+
"peerDependencies": {
|
|
66
|
+
"react": "^19.2.0"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@types/react": "^19.2.5"
|
|
70
|
+
}
|
|
71
|
+
}
|