|
1 | | -// TODO: Implement exportDB() — dump IndexedDB stores to generic JSON format |
2 | | -// |
3 | | -// Responsibilities: |
4 | | -// - Accept a database name (or IDBDatabase reference) and optional store filter |
5 | | -// - Extract database schema (object store names, key paths, autoIncrement, indexes) |
6 | | -// - Read all records from each store |
7 | | -// - Serialize records using type-tagged serialization (bigint, Date, Uint8Array) |
8 | | -// - Return the generic ExportFormat JSON object |
| 1 | +import type { ExportFormat, ExportOptions, IndexSchema, StoreSchema } from '../types/index.js'; |
| 2 | +import { serialize, BACKUP_VERSION } from '../serialization/index.js'; |
| 3 | + |
| 4 | +/** |
| 5 | + * Extract the schema for a single object store, including all its indexes. |
| 6 | + * |
| 7 | + * @param store - The IDBObjectStore to extract the schema from. |
| 8 | + * @returns The store schema definition. |
| 9 | + */ |
| 10 | +function extractStoreSchema(store: IDBObjectStore): StoreSchema { |
| 11 | + const indexes: IndexSchema[] = []; |
| 12 | + |
| 13 | + const indexNames = Array.from(store.indexNames); |
| 14 | + |
| 15 | + for (const indexName of indexNames) { |
| 16 | + const index = store.index(indexName); |
| 17 | + indexes.push({ |
| 18 | + name: index.name, |
| 19 | + keyPath: index.keyPath, |
| 20 | + unique: index.unique, |
| 21 | + multiEntry: index.multiEntry, |
| 22 | + }); |
| 23 | + } |
| 24 | + |
| 25 | + return { |
| 26 | + keyPath: store.keyPath, |
| 27 | + autoIncrement: store.autoIncrement, |
| 28 | + indexes, |
| 29 | + }; |
| 30 | +} |
| 31 | + |
| 32 | +/** |
| 33 | + * Read all records from an object store using a cursor. |
| 34 | + * |
| 35 | + * Each record is serialized using type-tagged serialization to ensure |
| 36 | + * non-JSON-safe types (Uint8Array, bigint, Date) are preserved. |
| 37 | + * |
| 38 | + * @param store - The IDBObjectStore to read records from. |
| 39 | + * @returns A promise that resolves to an array of serialized records. |
| 40 | + */ |
| 41 | +function readAllRecords(store: IDBObjectStore): Promise<Array<{ key: unknown; value: unknown }>> { |
| 42 | + return new Promise((resolve, reject) => { |
| 43 | + const records: Array<{ key: unknown; value: unknown }> = []; |
| 44 | + const request = store.openCursor(); |
| 45 | + |
| 46 | + request.onsuccess = () => { |
| 47 | + const cursor = request.result; |
| 48 | + if (cursor) { |
| 49 | + records.push({ |
| 50 | + key: serialize(cursor.primaryKey), |
| 51 | + value: serialize(cursor.value), |
| 52 | + }); |
| 53 | + cursor.continue(); |
| 54 | + } else { |
| 55 | + resolve(records); |
| 56 | + } |
| 57 | + }; |
| 58 | + |
| 59 | + request.onerror = () => { |
| 60 | + reject(new Error(`Failed to read records from store "${store.name}": ${String(request.error)}`)); |
| 61 | + }; |
| 62 | + }); |
| 63 | +} |
| 64 | + |
| 65 | +/** |
| 66 | + * Open an IndexedDB database by name. |
| 67 | + * |
| 68 | + * @param dbName - The name of the database to open. |
| 69 | + * @returns A promise that resolves to the opened IDBDatabase instance. |
| 70 | + */ |
| 71 | +function openDatabase(dbName: string): Promise<IDBDatabase> { |
| 72 | + return new Promise((resolve, reject) => { |
| 73 | + const request = indexedDB.open(dbName); |
| 74 | + |
| 75 | + request.onsuccess = () => { |
| 76 | + resolve(request.result); |
| 77 | + }; |
| 78 | + |
| 79 | + request.onerror = () => { |
| 80 | + reject(new Error(`Failed to open database "${dbName}": ${String(request.error)}`)); |
| 81 | + }; |
| 82 | + }); |
| 83 | +} |
| 84 | + |
| 85 | +/** |
| 86 | + * Export an IndexedDB database to the generic JSON backup format. |
| 87 | + * |
| 88 | + * Opens the specified database, extracts the schema for each object store, |
| 89 | + * reads all records (with type-tagged serialization), and returns the |
| 90 | + * complete ExportFormat envelope. |
| 91 | + * |
| 92 | + * @param options - Export configuration. |
| 93 | + * @param options.dbName - The name of the IndexedDB database to export. |
| 94 | + * @param options.storeNames - Optional list of store names to export. If omitted, all stores are exported. |
| 95 | + * @returns A promise that resolves to the ExportFormat JSON object. |
| 96 | + * |
| 97 | + * @example |
| 98 | + * ```typescript |
| 99 | + * const backup = await exportDB({ dbName: 'my-app-db' }); |
| 100 | + * console.log(JSON.stringify(backup, null, 2)); |
| 101 | + * ``` |
| 102 | + */ |
| 103 | +export async function exportDB(options: ExportOptions): Promise<ExportFormat> { |
| 104 | + const { dbName, storeNames } = options; |
| 105 | + |
| 106 | + const db = await openDatabase(dbName); |
| 107 | + |
| 108 | + try { |
| 109 | + // Determine which stores to export |
| 110 | + const allStoreNames = Array.from(db.objectStoreNames); |
| 111 | + const targetStores = storeNames |
| 112 | + ? [...new Set(storeNames.filter((name) => allStoreNames.includes(name)))] |
| 113 | + : allStoreNames; |
| 114 | + |
| 115 | + if (targetStores.length === 0) { |
| 116 | + // Return an empty export if no stores match |
| 117 | + return { |
| 118 | + backupVersion: BACKUP_VERSION, |
| 119 | + databaseName: db.name, |
| 120 | + databaseVersion: db.version, |
| 121 | + exportedAt: new Date().toISOString(), |
| 122 | + schema: {}, |
| 123 | + stores: {}, |
| 124 | + }; |
| 125 | + } |
| 126 | + |
| 127 | + // Open a single read-only transaction across all target stores |
| 128 | + const transaction = db.transaction(targetStores, 'readonly'); |
| 129 | + const schema: Record<string, StoreSchema> = {}; |
| 130 | + const stores: Record<string, Array<{ key: unknown; value: unknown }>> = {}; |
| 131 | + |
| 132 | + // Process each store: extract schema and read records |
| 133 | + const storePromises = targetStores.map(async (storeName) => { |
| 134 | + const store = transaction.objectStore(storeName); |
| 135 | + schema[storeName] = extractStoreSchema(store); |
| 136 | + stores[storeName] = await readAllRecords(store); |
| 137 | + }); |
| 138 | + |
| 139 | + await Promise.all(storePromises); |
| 140 | + |
| 141 | + return { |
| 142 | + backupVersion: BACKUP_VERSION, |
| 143 | + databaseName: db.name, |
| 144 | + databaseVersion: db.version, |
| 145 | + exportedAt: new Date().toISOString(), |
| 146 | + schema, |
| 147 | + stores, |
| 148 | + }; |
| 149 | + } finally { |
| 150 | + db.close(); |
| 151 | + } |
| 152 | +} |
0 commit comments