@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/src/snapshot-migrator.ts
DELETED
|
@@ -1,420 +0,0 @@
|
|
|
1
|
-
// IndexedDB migrator that uses Drizzle snapshot files to create object stores and indexes
|
|
2
|
-
|
|
3
|
-
import type { Journal, Snapshot } from "@firtoz/drizzle-utils";
|
|
4
|
-
|
|
5
|
-
// ============================================================================
|
|
6
|
-
// Migration Config Types
|
|
7
|
-
// ============================================================================
|
|
8
|
-
|
|
9
|
-
export interface IndexedDBMigrationConfig {
|
|
10
|
-
journal: Journal;
|
|
11
|
-
snapshots: Record<string, Snapshot>;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface MigrationRecord {
|
|
15
|
-
id: number;
|
|
16
|
-
tag: string;
|
|
17
|
-
when: number;
|
|
18
|
-
appliedAt: number;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const MIGRATIONS_STORE = "__drizzle_migrations";
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Opens an IndexedDB database and runs migrations if needed
|
|
25
|
-
*/
|
|
26
|
-
export async function migrateIndexedDB(
|
|
27
|
-
dbName: string,
|
|
28
|
-
config: IndexedDBMigrationConfig,
|
|
29
|
-
debug: boolean = false,
|
|
30
|
-
): Promise<IDBDatabase> {
|
|
31
|
-
if (debug) {
|
|
32
|
-
console.log(
|
|
33
|
-
`[${new Date().toISOString()}] [PERF] IndexedDB snapshot migrator start for ${dbName}`,
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// First, open the database to check which migrations have been applied
|
|
38
|
-
|
|
39
|
-
const currentDb = await openDatabaseForMigrationCheck(dbName);
|
|
40
|
-
|
|
41
|
-
const appliedMigrations = await getAppliedMigrations(currentDb);
|
|
42
|
-
|
|
43
|
-
const latestAppliedIdx =
|
|
44
|
-
appliedMigrations.length > 0
|
|
45
|
-
? Math.max(...appliedMigrations.map((m) => m.id))
|
|
46
|
-
: -1;
|
|
47
|
-
|
|
48
|
-
if (debug) {
|
|
49
|
-
console.log(
|
|
50
|
-
`[${new Date().toISOString()}] [PERF] Latest applied migration index: ${latestAppliedIdx} (checked ${appliedMigrations.length} migrations)`,
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Determine which migrations need to be applied
|
|
55
|
-
const pendingMigrations = config.journal.entries.filter(
|
|
56
|
-
(entry) => entry.idx > latestAppliedIdx,
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
if (pendingMigrations.length === 0) {
|
|
60
|
-
if (debug) {
|
|
61
|
-
console.log(
|
|
62
|
-
`[${new Date().toISOString()}] [PERF] No pending migrations - database is up to date`,
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
currentDb.close();
|
|
66
|
-
// Re-open with correct version
|
|
67
|
-
|
|
68
|
-
const db = await openDatabase(dbName, config.journal.entries.length);
|
|
69
|
-
|
|
70
|
-
if (debug) {
|
|
71
|
-
console.log(
|
|
72
|
-
`[${new Date().toISOString()}] [PERF] Migrator complete (no migrations needed)`,
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
return db;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (debug) {
|
|
79
|
-
console.log(
|
|
80
|
-
`[${new Date().toISOString()}] [PERF] Found ${pendingMigrations.length} pending migrations to apply:`,
|
|
81
|
-
pendingMigrations.map((m) => m.tag),
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
currentDb.close();
|
|
86
|
-
|
|
87
|
-
// Calculate the target version (number of total migrations)
|
|
88
|
-
const targetVersion = config.journal.entries.length;
|
|
89
|
-
|
|
90
|
-
// Open database with version upgrade to trigger migration
|
|
91
|
-
|
|
92
|
-
const db = await new Promise<IDBDatabase>((resolve, reject) => {
|
|
93
|
-
const request = indexedDB.open(dbName, targetVersion);
|
|
94
|
-
|
|
95
|
-
request.onerror = () => reject(request.error);
|
|
96
|
-
request.onsuccess = () => {
|
|
97
|
-
resolve(request.result);
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
request.onupgradeneeded = (event) => {
|
|
101
|
-
const db = (event.target as IDBOpenDBRequest).result;
|
|
102
|
-
const transaction = (event.target as IDBOpenDBRequest).transaction;
|
|
103
|
-
|
|
104
|
-
if (!transaction) {
|
|
105
|
-
reject(new Error("Transaction not available during upgrade"));
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (debug) {
|
|
110
|
-
console.log(
|
|
111
|
-
`[${new Date().toISOString()}] [PERF] Upgrade started: v${event.oldVersion} → v${event.newVersion}`,
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
try {
|
|
116
|
-
// Ensure migrations store exists
|
|
117
|
-
|
|
118
|
-
if (!db.objectStoreNames.contains(MIGRATIONS_STORE)) {
|
|
119
|
-
const migrationStore = db.createObjectStore(MIGRATIONS_STORE, {
|
|
120
|
-
keyPath: "id",
|
|
121
|
-
autoIncrement: false,
|
|
122
|
-
});
|
|
123
|
-
migrationStore.createIndex("tag", "tag", { unique: true });
|
|
124
|
-
migrationStore.createIndex("when", "when", { unique: false });
|
|
125
|
-
if (debug) {
|
|
126
|
-
console.log(
|
|
127
|
-
`[${new Date().toISOString()}] [PERF] Created migrations tracking store`,
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Apply each pending migration
|
|
133
|
-
for (const journalEntry of pendingMigrations) {
|
|
134
|
-
const snapshotKey = `m${journalEntry.idx.toString().padStart(4, "0")}`;
|
|
135
|
-
const snapshot = config.snapshots[snapshotKey];
|
|
136
|
-
|
|
137
|
-
if (!snapshot) {
|
|
138
|
-
throw new Error(`Missing snapshot: ${snapshotKey}`);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Get previous snapshot for comparison (if this isn't the first migration)
|
|
142
|
-
const previousSnapshot =
|
|
143
|
-
journalEntry.idx > 0
|
|
144
|
-
? config.snapshots[
|
|
145
|
-
`m${(journalEntry.idx - 1).toString().padStart(4, "0")}`
|
|
146
|
-
]
|
|
147
|
-
: null;
|
|
148
|
-
|
|
149
|
-
if (debug) {
|
|
150
|
-
console.log(
|
|
151
|
-
`[${new Date().toISOString()}] [PERF] Applying migration ${journalEntry.idx}: ${journalEntry.tag}`,
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
applySnapshot(
|
|
156
|
-
db,
|
|
157
|
-
snapshot,
|
|
158
|
-
previousSnapshot,
|
|
159
|
-
transaction,
|
|
160
|
-
debug,
|
|
161
|
-
dbName,
|
|
162
|
-
journalEntry.idx,
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
// Record the migration
|
|
166
|
-
const migrationStore = transaction.objectStore(MIGRATIONS_STORE);
|
|
167
|
-
migrationStore.add({
|
|
168
|
-
id: journalEntry.idx,
|
|
169
|
-
tag: journalEntry.tag,
|
|
170
|
-
when: journalEntry.when,
|
|
171
|
-
appliedAt: Date.now(),
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
if (debug) {
|
|
175
|
-
console.log(
|
|
176
|
-
`[${new Date().toISOString()}] [PERF] Migration ${journalEntry.idx} complete`,
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (debug) {
|
|
182
|
-
console.log(
|
|
183
|
-
`[${new Date().toISOString()}] [PERF] All ${pendingMigrations.length} migrations applied successfully`,
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
} catch (error) {
|
|
187
|
-
console.error("[IndexedDBMigrator] Migration failed:", error);
|
|
188
|
-
transaction.abort();
|
|
189
|
-
reject(error);
|
|
190
|
-
}
|
|
191
|
-
};
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
if (debug) {
|
|
195
|
-
console.log(
|
|
196
|
-
`[${new Date().toISOString()}] [PERF] Migrator complete - database ready`,
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return db;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Opens database for checking migrations (without triggering upgrade)
|
|
205
|
-
*/
|
|
206
|
-
async function openDatabaseForMigrationCheck(
|
|
207
|
-
dbName: string,
|
|
208
|
-
): Promise<IDBDatabase> {
|
|
209
|
-
return new Promise((resolve, reject) => {
|
|
210
|
-
const request = indexedDB.open(dbName);
|
|
211
|
-
request.onerror = () => reject(request.error);
|
|
212
|
-
request.onsuccess = () => resolve(request.result);
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Opens database with a specific version
|
|
218
|
-
*/
|
|
219
|
-
async function openDatabase(
|
|
220
|
-
dbName: string,
|
|
221
|
-
version: number,
|
|
222
|
-
): Promise<IDBDatabase> {
|
|
223
|
-
return new Promise((resolve, reject) => {
|
|
224
|
-
const request = indexedDB.open(dbName, version);
|
|
225
|
-
request.onerror = () => reject(request.error);
|
|
226
|
-
request.onsuccess = () => resolve(request.result);
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Gets the list of applied migrations from the database
|
|
232
|
-
*/
|
|
233
|
-
async function getAppliedMigrations(
|
|
234
|
-
db: IDBDatabase,
|
|
235
|
-
): Promise<MigrationRecord[]> {
|
|
236
|
-
if (!db.objectStoreNames.contains(MIGRATIONS_STORE)) {
|
|
237
|
-
return [];
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return new Promise((resolve, reject) => {
|
|
241
|
-
const transaction = db.transaction(MIGRATIONS_STORE, "readonly");
|
|
242
|
-
const store = transaction.objectStore(MIGRATIONS_STORE);
|
|
243
|
-
const request = store.getAll();
|
|
244
|
-
|
|
245
|
-
request.onerror = () => reject(request.error);
|
|
246
|
-
request.onsuccess = () => resolve(request.result);
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Applies a snapshot to the database by creating/updating object stores and indexes
|
|
252
|
-
*/
|
|
253
|
-
function applySnapshot(
|
|
254
|
-
db: IDBDatabase,
|
|
255
|
-
snapshot: Snapshot,
|
|
256
|
-
previousSnapshot: Snapshot | null,
|
|
257
|
-
transaction: IDBTransaction,
|
|
258
|
-
debug: boolean,
|
|
259
|
-
_dbName: string,
|
|
260
|
-
_migrationIdx: number,
|
|
261
|
-
): void {
|
|
262
|
-
// Process each table in the snapshot
|
|
263
|
-
for (const [tableName, tableDefinition] of Object.entries(snapshot.tables)) {
|
|
264
|
-
const storeExists = db.objectStoreNames.contains(tableName);
|
|
265
|
-
let objectStore: IDBObjectStore;
|
|
266
|
-
|
|
267
|
-
if (!storeExists) {
|
|
268
|
-
// Create new object store
|
|
269
|
-
|
|
270
|
-
if (debug) {
|
|
271
|
-
console.log(
|
|
272
|
-
`[${new Date().toISOString()}] [PERF] Creating object store: ${tableName}`,
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Find the primary key column (optional in IndexedDB)
|
|
277
|
-
const primaryKeyColumn = Object.entries(tableDefinition.columns).find(
|
|
278
|
-
([_, col]) => col.primaryKey,
|
|
279
|
-
);
|
|
280
|
-
|
|
281
|
-
if (primaryKeyColumn) {
|
|
282
|
-
// In-line key: Use the primary key column as keyPath
|
|
283
|
-
const keyPath = primaryKeyColumn[0];
|
|
284
|
-
const autoIncrement = primaryKeyColumn[1].autoincrement;
|
|
285
|
-
|
|
286
|
-
objectStore = db.createObjectStore(tableName, {
|
|
287
|
-
keyPath,
|
|
288
|
-
autoIncrement,
|
|
289
|
-
});
|
|
290
|
-
} else {
|
|
291
|
-
// Out-of-line key: No keyPath, can use auto-increment or external keys
|
|
292
|
-
if (debug) {
|
|
293
|
-
console.log(
|
|
294
|
-
`[${new Date().toISOString()}] [PERF] Creating object store ${tableName} with out-of-line keys (no keyPath)`,
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
objectStore = db.createObjectStore(tableName, {
|
|
298
|
-
autoIncrement: true, // Auto-generate keys by default
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
} else {
|
|
302
|
-
// Get existing object store
|
|
303
|
-
objectStore = transaction.objectStore(tableName);
|
|
304
|
-
|
|
305
|
-
// Check if the primary key or autoIncrement has changed
|
|
306
|
-
const primaryKeyColumn = Object.entries(tableDefinition.columns).find(
|
|
307
|
-
([_, col]) => col.primaryKey,
|
|
308
|
-
);
|
|
309
|
-
|
|
310
|
-
// Determine expected key configuration
|
|
311
|
-
const expectedKeyPath = primaryKeyColumn ? primaryKeyColumn[0] : null;
|
|
312
|
-
const expectedAutoIncrement = primaryKeyColumn
|
|
313
|
-
? primaryKeyColumn[1].autoincrement
|
|
314
|
-
: true; // Default for out-of-line keys
|
|
315
|
-
|
|
316
|
-
// Normalize keyPath for comparison (null, undefined, or empty string are all "no keyPath")
|
|
317
|
-
const currentKeyPath = objectStore.keyPath || null;
|
|
318
|
-
const normalizedExpectedKeyPath = expectedKeyPath || null;
|
|
319
|
-
|
|
320
|
-
// Warn if key structure changed - this requires manual migration
|
|
321
|
-
if (
|
|
322
|
-
currentKeyPath !== normalizedExpectedKeyPath ||
|
|
323
|
-
objectStore.autoIncrement !== expectedAutoIncrement
|
|
324
|
-
) {
|
|
325
|
-
const message =
|
|
326
|
-
`[IndexedDBMigrator] Primary key structure changed for ${tableName}. ` +
|
|
327
|
-
`This requires manual migration. ` +
|
|
328
|
-
`Old keyPath: ${currentKeyPath || "(none)"}, new keyPath: ${normalizedExpectedKeyPath || "(none)"}. ` +
|
|
329
|
-
`Old autoIncrement: ${objectStore.autoIncrement}, new autoIncrement: ${expectedAutoIncrement}. ` +
|
|
330
|
-
`Consider exporting data, deleting the database, and re-importing.`;
|
|
331
|
-
|
|
332
|
-
console.error(message);
|
|
333
|
-
throw new Error(message);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const currentIndexes = tableDefinition.indexes;
|
|
338
|
-
|
|
339
|
-
// Remove indexes that no longer exist in the schema
|
|
340
|
-
const existingIndexNames = Array.from(objectStore.indexNames);
|
|
341
|
-
for (const existingIndexName of existingIndexNames) {
|
|
342
|
-
if (!currentIndexes[existingIndexName]) {
|
|
343
|
-
if (debug) {
|
|
344
|
-
console.log(
|
|
345
|
-
`[${new Date().toISOString()}] [PERF] Deleting index: ${existingIndexName} from ${tableName}`,
|
|
346
|
-
);
|
|
347
|
-
}
|
|
348
|
-
objectStore.deleteIndex(existingIndexName);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// Add or update indexes
|
|
353
|
-
for (const [indexName, indexDef] of Object.entries(currentIndexes)) {
|
|
354
|
-
const indexExists = objectStore.indexNames.contains(indexName);
|
|
355
|
-
|
|
356
|
-
// IndexedDB indexes can only be on a single column or use an array keyPath
|
|
357
|
-
const keyPath =
|
|
358
|
-
indexDef.columns.length === 1 ? indexDef.columns[0] : indexDef.columns;
|
|
359
|
-
|
|
360
|
-
if (indexExists) {
|
|
361
|
-
// Check if index definition has changed
|
|
362
|
-
const existingIndex = objectStore.index(indexName);
|
|
363
|
-
const keyPathChanged = Array.isArray(keyPath)
|
|
364
|
-
? JSON.stringify(existingIndex.keyPath) !== JSON.stringify(keyPath)
|
|
365
|
-
: existingIndex.keyPath !== keyPath;
|
|
366
|
-
const uniqueChanged = existingIndex.unique !== indexDef.isUnique;
|
|
367
|
-
|
|
368
|
-
if (keyPathChanged || uniqueChanged) {
|
|
369
|
-
if (debug) {
|
|
370
|
-
console.log(
|
|
371
|
-
`[${new Date().toISOString()}] [PERF] Recreating index: ${indexName} on ${tableName}`,
|
|
372
|
-
);
|
|
373
|
-
}
|
|
374
|
-
// Recreate the index
|
|
375
|
-
objectStore.deleteIndex(indexName);
|
|
376
|
-
objectStore.createIndex(indexName, keyPath, {
|
|
377
|
-
unique: indexDef.isUnique,
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
} else {
|
|
381
|
-
// Create new index
|
|
382
|
-
|
|
383
|
-
if (debug) {
|
|
384
|
-
console.log(
|
|
385
|
-
`[${new Date().toISOString()}] [PERF] Creating index: ${indexName} on ${tableName}`,
|
|
386
|
-
);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
try {
|
|
390
|
-
objectStore.createIndex(indexName, keyPath, {
|
|
391
|
-
unique: indexDef.isUnique,
|
|
392
|
-
});
|
|
393
|
-
} catch (error) {
|
|
394
|
-
console.error(
|
|
395
|
-
`[${new Date().toISOString()}] [IndexedDBMigrator] failed to create index ${indexName}:`,
|
|
396
|
-
error,
|
|
397
|
-
);
|
|
398
|
-
throw error;
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Handle deleted tables
|
|
405
|
-
if (previousSnapshot) {
|
|
406
|
-
for (const tableName of Object.keys(previousSnapshot.tables)) {
|
|
407
|
-
if (
|
|
408
|
-
!snapshot.tables[tableName] &&
|
|
409
|
-
db.objectStoreNames.contains(tableName)
|
|
410
|
-
) {
|
|
411
|
-
if (debug) {
|
|
412
|
-
console.log(
|
|
413
|
-
`[${new Date().toISOString()}] [PERF] Deleting object store: ${tableName}`,
|
|
414
|
-
);
|
|
415
|
-
}
|
|
416
|
-
db.deleteObjectStore(tableName);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
}
|