@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.
@@ -1,9 +1,54 @@
1
- // IndexedDB migrator that executes generated migration functions
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 type IndexedDBMigrationFunction = (
4
- db: IDBDatabase,
5
- transaction: IDBTransaction,
6
- ) => Promise<void>;
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
- * Runs IndexedDB migrations using generated migration functions
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
- * import { migrations } from './drizzle/indexeddb-migrations';
21
- * import { migrateIndexedDBWithFunctions } from '@firtoz/drizzle-indexeddb';
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, true);
142
+ * const db = await migrateIndexedDBWithFunctions('my-db', migrations);
24
143
  * ```
25
144
  */
26
145
  export async function migrateIndexedDBWithFunctions(
27
146
  dbName: string,
28
- migrations: IndexedDBMigrationFunction[],
147
+ migrations: Migration[],
29
148
  debug: boolean = false,
30
- ): Promise<IDBDatabase> {
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
- // First, open the database to check which migrations have been applied
38
- const currentDb = await openDatabaseForMigrationCheck(dbName);
39
-
40
- const appliedMigrations = await getAppliedMigrations(currentDb);
155
+ const targetVersion = migrations.length + 1;
41
156
 
42
- const latestAppliedIdx =
43
- appliedMigrations.length > 0
44
- ? Math.max(...appliedMigrations.map((m) => m.id))
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
- `[${new Date().toISOString()}] [PERF] Latest applied migration index: ${latestAppliedIdx} (checked ${appliedMigrations.length} migrations)`,
163
+ `[IndexedDB] Current version: ${currentVersion}, Target: ${targetVersion}`,
50
164
  );
51
165
  }
52
166
 
53
- // Determine which migrations need to be applied
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((fn, idx) => ({ fn, idx }))
56
- .filter(({ idx }) => idx > latestAppliedIdx);
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
- `[${new Date().toISOString()}] [PERF] Found ${pendingMigrations.length} pending migrations to apply`,
196
+ `[IndexedDB] ${pendingMigrations.length} pending migrations to apply`,
78
197
  );
79
198
  }
80
199
 
81
- currentDb.close();
82
-
83
- // Calculate the target version (number of total migrations)
84
- const targetVersion = migrations.length;
85
-
86
- // Open database with version upgrade to trigger migration
87
- const db = await new Promise<IDBDatabase>((resolve, reject) => {
88
- // Use +1 here because first version is 1...
89
- const request = indexedDB.open(dbName, targetVersion + 1);
90
-
91
- request.onerror = () => reject(request.error);
92
- request.onsuccess = () => {
93
- resolve(request.result);
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
- } catch (error) {
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
- `[${new Date().toISOString()}] [PERF] Migrator complete - database ready`,
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
- * Opens database for checking migrations (without triggering upgrade)
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: IDBDatabase,
248
+ db: IDBDatabaseLike,
215
249
  ): Promise<MigrationRecord[]> {
216
- if (!db.objectStoreNames.contains(MIGRATIONS_STORE)) {
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 IndexedDBMigrationFunction,
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 { deleteIndexedDB } from "./utils";
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,