@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/function-migrator.ts
CHANGED
|
@@ -1,9 +1,54 @@
|
|
|
1
|
-
// IndexedDB migrator
|
|
1
|
+
// IndexedDB migrator with declarative migration format
|
|
2
|
+
|
|
3
|
+
import { type IDBCreator, type IDBDatabaseLike, openIndexedDb } from "./utils";
|
|
4
|
+
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Declarative Migration Types
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
export interface CreateTableOperation {
|
|
10
|
+
type: "createTable";
|
|
11
|
+
name: string;
|
|
12
|
+
keyPath?: string;
|
|
13
|
+
autoIncrement?: boolean;
|
|
14
|
+
indexes?: Array<{
|
|
15
|
+
name: string;
|
|
16
|
+
keyPath: string | string[];
|
|
17
|
+
unique?: boolean;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface DeleteTableOperation {
|
|
22
|
+
type: "deleteTable";
|
|
23
|
+
name: string;
|
|
24
|
+
}
|
|
2
25
|
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
26
|
+
export interface CreateIndexOperation {
|
|
27
|
+
type: "createIndex";
|
|
28
|
+
tableName: string;
|
|
29
|
+
indexName: string;
|
|
30
|
+
keyPath: string | string[];
|
|
31
|
+
unique?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface DeleteIndexOperation {
|
|
35
|
+
type: "deleteIndex";
|
|
36
|
+
tableName: string;
|
|
37
|
+
indexName: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type MigrationOperation =
|
|
41
|
+
| CreateTableOperation
|
|
42
|
+
| DeleteTableOperation
|
|
43
|
+
| CreateIndexOperation
|
|
44
|
+
| DeleteIndexOperation;
|
|
45
|
+
|
|
46
|
+
/** A migration is an array of operations to perform */
|
|
47
|
+
export type Migration = MigrationOperation[];
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// Migration Record
|
|
51
|
+
// ============================================================================
|
|
7
52
|
|
|
8
53
|
interface MigrationRecord {
|
|
9
54
|
id: number;
|
|
@@ -12,160 +57,184 @@ interface MigrationRecord {
|
|
|
12
57
|
|
|
13
58
|
const MIGRATIONS_STORE = "__drizzle_migrations";
|
|
14
59
|
|
|
60
|
+
// ============================================================================
|
|
61
|
+
// Migration Executor
|
|
62
|
+
// ============================================================================
|
|
63
|
+
|
|
15
64
|
/**
|
|
16
|
-
*
|
|
65
|
+
* Executes a single migration operation on the database.
|
|
66
|
+
*/
|
|
67
|
+
function executeMigrationOperation(
|
|
68
|
+
db: IDBDatabaseLike,
|
|
69
|
+
op: MigrationOperation,
|
|
70
|
+
): void {
|
|
71
|
+
switch (op.type) {
|
|
72
|
+
case "createTable": {
|
|
73
|
+
if (!db.hasStore(op.name)) {
|
|
74
|
+
db.createStore(op.name, {
|
|
75
|
+
keyPath: op.keyPath,
|
|
76
|
+
autoIncrement: op.autoIncrement,
|
|
77
|
+
});
|
|
78
|
+
// Create indexes if specified
|
|
79
|
+
if (op.indexes) {
|
|
80
|
+
for (const idx of op.indexes) {
|
|
81
|
+
db.createIndex(op.name, idx.name, idx.keyPath, {
|
|
82
|
+
unique: idx.unique,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
case "deleteTable": {
|
|
90
|
+
if (db.hasStore(op.name)) {
|
|
91
|
+
db.deleteStore(op.name);
|
|
92
|
+
}
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
case "createIndex": {
|
|
96
|
+
if (db.hasStore(op.tableName)) {
|
|
97
|
+
db.createIndex(op.tableName, op.indexName, op.keyPath, {
|
|
98
|
+
unique: op.unique,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
case "deleteIndex": {
|
|
104
|
+
if (db.hasStore(op.tableName)) {
|
|
105
|
+
db.deleteIndex(op.tableName, op.indexName);
|
|
106
|
+
}
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Executes a full migration (array of operations).
|
|
114
|
+
*/
|
|
115
|
+
function executeMigration(db: IDBDatabaseLike, migration: Migration): void {
|
|
116
|
+
for (const op of migration) {
|
|
117
|
+
executeMigrationOperation(db, op);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ============================================================================
|
|
122
|
+
// Main Migrator
|
|
123
|
+
// ============================================================================
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Runs IndexedDB migrations using declarative migration arrays.
|
|
127
|
+
* Version = total migrations + 1.
|
|
17
128
|
*
|
|
18
129
|
* Example usage:
|
|
19
130
|
* ```typescript
|
|
20
|
-
*
|
|
21
|
-
*
|
|
131
|
+
* const migrations: Migration[] = [
|
|
132
|
+
* [
|
|
133
|
+
* { type: "createTable", name: "todo", keyPath: "id", indexes: [
|
|
134
|
+
* { name: "todo_user_id", keyPath: "user_id" }
|
|
135
|
+
* ]}
|
|
136
|
+
* ],
|
|
137
|
+
* [
|
|
138
|
+
* { type: "createTable", name: "user", keyPath: "id" }
|
|
139
|
+
* ]
|
|
140
|
+
* ];
|
|
22
141
|
*
|
|
23
|
-
* const db = await migrateIndexedDBWithFunctions('my-db', migrations
|
|
142
|
+
* const db = await migrateIndexedDBWithFunctions('my-db', migrations);
|
|
24
143
|
* ```
|
|
25
144
|
*/
|
|
26
145
|
export async function migrateIndexedDBWithFunctions(
|
|
27
146
|
dbName: string,
|
|
28
|
-
migrations:
|
|
147
|
+
migrations: Migration[],
|
|
29
148
|
debug: boolean = false,
|
|
30
|
-
|
|
149
|
+
dbCreator?: IDBCreator,
|
|
150
|
+
): Promise<IDBDatabaseLike> {
|
|
31
151
|
if (debug) {
|
|
32
|
-
console.log(
|
|
33
|
-
`[${new Date().toISOString()}] [PERF] IndexedDB migrator start for ${dbName}`,
|
|
34
|
-
);
|
|
152
|
+
console.log(`[IndexedDB] Starting migration for ${dbName}`);
|
|
35
153
|
}
|
|
36
154
|
|
|
37
|
-
|
|
38
|
-
const currentDb = await openDatabaseForMigrationCheck(dbName);
|
|
39
|
-
|
|
40
|
-
const appliedMigrations = await getAppliedMigrations(currentDb);
|
|
155
|
+
const targetVersion = migrations.length + 1;
|
|
41
156
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
: -1;
|
|
157
|
+
// Open database to check current state
|
|
158
|
+
let db = await openIndexedDb(dbName, dbCreator);
|
|
159
|
+
const currentVersion = db.version;
|
|
46
160
|
|
|
47
161
|
if (debug) {
|
|
48
162
|
console.log(
|
|
49
|
-
`[
|
|
163
|
+
`[IndexedDB] Current version: ${currentVersion}, Target: ${targetVersion}`,
|
|
50
164
|
);
|
|
51
165
|
}
|
|
52
166
|
|
|
53
|
-
//
|
|
167
|
+
// If already at target version, check if all migrations are recorded
|
|
168
|
+
if (currentVersion >= targetVersion) {
|
|
169
|
+
const applied = await getAppliedMigrations(db);
|
|
170
|
+
if (applied.length === migrations.length) {
|
|
171
|
+
if (debug) {
|
|
172
|
+
console.log("[IndexedDB] Already up to date");
|
|
173
|
+
}
|
|
174
|
+
return db;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Get applied migrations before closing
|
|
179
|
+
const appliedMigrations = await getAppliedMigrations(db);
|
|
180
|
+
const appliedSet = new Set(appliedMigrations.map((m) => m.id));
|
|
181
|
+
|
|
182
|
+
// Find pending migrations
|
|
54
183
|
const pendingMigrations = migrations
|
|
55
|
-
.map((
|
|
56
|
-
.filter(({ idx }) => idx
|
|
184
|
+
.map((migration, idx) => ({ migration, idx }))
|
|
185
|
+
.filter(({ idx }) => !appliedSet.has(idx));
|
|
57
186
|
|
|
58
187
|
if (pendingMigrations.length === 0) {
|
|
59
188
|
if (debug) {
|
|
60
|
-
console.log(
|
|
61
|
-
`[${new Date().toISOString()}] [PERF] No pending migrations - database is up to date`,
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
currentDb.close();
|
|
65
|
-
// Re-open with correct version (migrations.length + 1 because version starts at 1)
|
|
66
|
-
const db = await openDatabase(dbName, migrations.length + 1);
|
|
67
|
-
if (debug) {
|
|
68
|
-
console.log(
|
|
69
|
-
`[${new Date().toISOString()}] [PERF] Migrator complete (no migrations needed)`,
|
|
70
|
-
);
|
|
189
|
+
console.log("[IndexedDB] No pending migrations");
|
|
71
190
|
}
|
|
72
191
|
return db;
|
|
73
192
|
}
|
|
74
193
|
|
|
75
194
|
if (debug) {
|
|
76
195
|
console.log(
|
|
77
|
-
`[
|
|
196
|
+
`[IndexedDB] ${pendingMigrations.length} pending migrations to apply`,
|
|
78
197
|
);
|
|
79
198
|
}
|
|
80
199
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
request.onupgradeneeded = async (event) => {
|
|
97
|
-
const db = (event.target as IDBOpenDBRequest).result;
|
|
98
|
-
const transaction = (event.target as IDBOpenDBRequest).transaction;
|
|
99
|
-
|
|
100
|
-
if (!transaction) {
|
|
101
|
-
reject(new Error("Transaction not available during upgrade"));
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (debug) {
|
|
106
|
-
console.log(
|
|
107
|
-
`[${new Date().toISOString()}] [PERF] Upgrade started: v${event.oldVersion} → v${event.newVersion}`,
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
// Ensure migrations store exists
|
|
113
|
-
if (!db.objectStoreNames.contains(MIGRATIONS_STORE)) {
|
|
114
|
-
const migrationStore = db.createObjectStore(MIGRATIONS_STORE, {
|
|
115
|
-
keyPath: "id",
|
|
116
|
-
autoIncrement: false,
|
|
117
|
-
});
|
|
118
|
-
migrationStore.createIndex("appliedAt", "appliedAt", {
|
|
119
|
-
unique: false,
|
|
120
|
-
});
|
|
121
|
-
if (debug) {
|
|
122
|
-
console.log(
|
|
123
|
-
`[${new Date().toISOString()}] [PERF] Created migrations tracking store`,
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Apply each pending migration
|
|
129
|
-
for (const { fn, idx } of pendingMigrations) {
|
|
130
|
-
if (debug) {
|
|
131
|
-
console.log(
|
|
132
|
-
`[${new Date().toISOString()}] [PERF] Applying migration ${idx}...`,
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Execute the migration function
|
|
137
|
-
await fn(db, transaction);
|
|
138
|
-
|
|
139
|
-
// Record the migration
|
|
140
|
-
const migrationStore = transaction.objectStore(MIGRATIONS_STORE);
|
|
141
|
-
migrationStore.add({
|
|
142
|
-
id: idx,
|
|
143
|
-
appliedAt: Date.now(),
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
if (debug) {
|
|
147
|
-
console.log(
|
|
148
|
-
`[${new Date().toISOString()}] [PERF] Migration ${idx} complete`,
|
|
149
|
-
);
|
|
150
|
-
}
|
|
200
|
+
// Close to allow version upgrade
|
|
201
|
+
db.close();
|
|
202
|
+
|
|
203
|
+
// Open with target version, running migrations during upgrade
|
|
204
|
+
await openIndexedDb(dbName, dbCreator, {
|
|
205
|
+
version: targetVersion,
|
|
206
|
+
onUpgrade: (upgradeDb) => {
|
|
207
|
+
// Ensure migrations store exists
|
|
208
|
+
if (!upgradeDb.hasStore(MIGRATIONS_STORE)) {
|
|
209
|
+
upgradeDb.createStore(MIGRATIONS_STORE, {
|
|
210
|
+
keyPath: "id",
|
|
211
|
+
autoIncrement: false,
|
|
212
|
+
});
|
|
213
|
+
if (debug) {
|
|
214
|
+
console.log("[IndexedDB] Created migrations store");
|
|
151
215
|
}
|
|
216
|
+
}
|
|
152
217
|
|
|
218
|
+
// Run pending migrations
|
|
219
|
+
for (const { migration, idx } of pendingMigrations) {
|
|
153
220
|
if (debug) {
|
|
154
|
-
console.log(
|
|
155
|
-
`[${new Date().toISOString()}] [PERF] All ${pendingMigrations.length} migrations applied successfully`,
|
|
156
|
-
);
|
|
221
|
+
console.log(`[IndexedDB] Running migration ${idx}`);
|
|
157
222
|
}
|
|
158
|
-
|
|
159
|
-
console.error("[IndexedDBMigrator] Migration failed:", error);
|
|
160
|
-
transaction.abort();
|
|
161
|
-
reject(error);
|
|
223
|
+
executeMigration(upgradeDb, migration);
|
|
162
224
|
}
|
|
163
|
-
}
|
|
225
|
+
},
|
|
164
226
|
});
|
|
165
227
|
|
|
228
|
+
// Reopen normally and record applied migrations
|
|
229
|
+
db = await openIndexedDb(dbName, dbCreator);
|
|
230
|
+
|
|
231
|
+
for (const { idx } of pendingMigrations) {
|
|
232
|
+
await db.add(MIGRATIONS_STORE, [{ id: idx, appliedAt: Date.now() }]);
|
|
233
|
+
}
|
|
234
|
+
|
|
166
235
|
if (debug) {
|
|
167
236
|
console.log(
|
|
168
|
-
`[${
|
|
237
|
+
`[IndexedDB] Applied ${pendingMigrations.length} migrations, now at version ${targetVersion}`,
|
|
169
238
|
);
|
|
170
239
|
}
|
|
171
240
|
|
|
@@ -173,62 +242,13 @@ export async function migrateIndexedDBWithFunctions(
|
|
|
173
242
|
}
|
|
174
243
|
|
|
175
244
|
/**
|
|
176
|
-
*
|
|
177
|
-
*/
|
|
178
|
-
async function openDatabaseForMigrationCheck(
|
|
179
|
-
dbName: string,
|
|
180
|
-
): Promise<IDBDatabase> {
|
|
181
|
-
return new Promise((resolve, reject) => {
|
|
182
|
-
const request = indexedDB.open(dbName);
|
|
183
|
-
request.onerror = () => {
|
|
184
|
-
reject(request.error);
|
|
185
|
-
};
|
|
186
|
-
request.onsuccess = () => {
|
|
187
|
-
resolve(request.result);
|
|
188
|
-
};
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Opens database with a specific version
|
|
194
|
-
*/
|
|
195
|
-
async function openDatabase(
|
|
196
|
-
dbName: string,
|
|
197
|
-
version: number,
|
|
198
|
-
): Promise<IDBDatabase> {
|
|
199
|
-
return new Promise((resolve, reject) => {
|
|
200
|
-
const request = indexedDB.open(dbName, version);
|
|
201
|
-
request.onerror = () => {
|
|
202
|
-
reject(request.error);
|
|
203
|
-
};
|
|
204
|
-
request.onsuccess = () => {
|
|
205
|
-
resolve(request.result);
|
|
206
|
-
};
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Gets the list of applied migrations from the database
|
|
245
|
+
* Gets applied migrations from the database.
|
|
212
246
|
*/
|
|
213
247
|
async function getAppliedMigrations(
|
|
214
|
-
db:
|
|
248
|
+
db: IDBDatabaseLike,
|
|
215
249
|
): Promise<MigrationRecord[]> {
|
|
216
|
-
if (!db.
|
|
250
|
+
if (!db.hasStore(MIGRATIONS_STORE)) {
|
|
217
251
|
return [];
|
|
218
252
|
}
|
|
219
|
-
|
|
220
|
-
return new Promise((resolve, reject) => {
|
|
221
|
-
const transaction = db.transaction(MIGRATIONS_STORE, "readonly");
|
|
222
|
-
|
|
223
|
-
const store = transaction.objectStore(MIGRATIONS_STORE);
|
|
224
|
-
|
|
225
|
-
const request = store.getAll();
|
|
226
|
-
|
|
227
|
-
request.onerror = () => {
|
|
228
|
-
reject(request.error);
|
|
229
|
-
};
|
|
230
|
-
request.onsuccess = () => {
|
|
231
|
-
resolve(request.result);
|
|
232
|
-
};
|
|
233
|
-
});
|
|
253
|
+
return db.getAll<MigrationRecord>(MIGRATIONS_STORE);
|
|
234
254
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
|
-
export {
|
|
2
|
-
migrateIndexedDB,
|
|
3
|
-
type IndexedDBMigrationConfig,
|
|
4
|
-
} from "./snapshot-migrator";
|
|
5
|
-
|
|
6
1
|
export {
|
|
7
2
|
migrateIndexedDBWithFunctions,
|
|
8
|
-
type
|
|
3
|
+
type Migration,
|
|
4
|
+
type MigrationOperation,
|
|
5
|
+
type CreateTableOperation,
|
|
6
|
+
type DeleteTableOperation,
|
|
7
|
+
type CreateIndexOperation,
|
|
8
|
+
type DeleteIndexOperation,
|
|
9
9
|
} from "./function-migrator";
|
|
10
10
|
|
|
11
|
-
export {
|
|
11
|
+
export {
|
|
12
|
+
deleteIndexedDB,
|
|
13
|
+
type IDBCreator,
|
|
14
|
+
type IDBOpenOptions,
|
|
15
|
+
type IDBDatabaseLike,
|
|
16
|
+
type IndexInfo,
|
|
17
|
+
type CreateStoreOptions,
|
|
18
|
+
type CreateIndexOptions,
|
|
19
|
+
type KeyRangeSpec,
|
|
20
|
+
} from "./utils";
|
|
12
21
|
|
|
13
22
|
export {
|
|
14
23
|
indexedDBCollectionOptions,
|