|
| 1 | +# Migration Guide: IndexedDB to Azure Table Storage |
| 2 | + |
| 3 | +This guide explains how to migrate from the previous IndexedDB-based version to the new API-based architecture. |
| 4 | + |
| 5 | +## What Changed |
| 6 | + |
| 7 | +### Before (v1.x) |
| 8 | +- Data stored client-side in browser IndexedDB |
| 9 | +- Single-user per browser |
| 10 | +- No server-side component |
| 11 | +- Dexie.js for database access |
| 12 | + |
| 13 | +### After (v2.0) |
| 14 | +- Data stored server-side in Azure Table Storage |
| 15 | +- Multi-user support with userId partitioning |
| 16 | +- RESTful API with Shared Key authentication |
| 17 | +- API client wraps HTTP calls |
| 18 | + |
| 19 | +## Architecture Changes |
| 20 | + |
| 21 | +``` |
| 22 | +Before: |
| 23 | +[Browser] → [IndexedDB] |
| 24 | +
|
| 25 | +After: |
| 26 | +[Browser] → [API] → [Azure Table Storage] |
| 27 | + ↑ |
| 28 | + Shared Key Auth |
| 29 | +``` |
| 30 | + |
| 31 | +## Key Differences |
| 32 | + |
| 33 | +1. **Authentication:** |
| 34 | + - UI: Still uses Azure SWA (Microsoft SSO) - **no changes** |
| 35 | + - API: New Shared Key authentication via `x-api-key` header |
| 36 | + |
| 37 | +2. **Data Storage:** |
| 38 | + - Moved from client-side (IndexedDB) to server-side (Table Storage) |
| 39 | + - Each user's data is partitioned by `userId` |
| 40 | + |
| 41 | +3. **IDs:** |
| 42 | + - IndexedDB used auto-increment integers |
| 43 | + - Table Storage uses string-based RowKeys |
| 44 | + - API converts between them for frontend compatibility |
| 45 | + |
| 46 | +## Breaking Changes |
| 47 | + |
| 48 | +- `db.ts` API is mostly compatible, but uses HTTP under the hood |
| 49 | + |
| 50 | +## Migration Steps |
| 51 | + |
| 52 | +### For Developers |
| 53 | + |
| 54 | +1. **Update environment configuration:** |
| 55 | + |
| 56 | + Create `.env`: |
| 57 | + ```env |
| 58 | + VITE_API_URL=http://localhost:7071/api |
| 59 | + VITE_API_KEY=dev-shared-key-123 |
| 60 | + ``` |
| 61 | + |
| 62 | + Create `api/local.settings.json`: |
| 63 | + ```json |
| 64 | + { |
| 65 | + "IsEncrypted": false, |
| 66 | + "Values": { |
| 67 | + "AzureWebJobsStorage": "UseDevelopmentStorage=true", |
| 68 | + "FUNCTIONS_WORKER_RUNTIME": "node", |
| 69 | + "AZURE_STORAGE_CONNECTION_STRING": "UseDevelopmentStorage=true", |
| 70 | + "API_SHARED_KEY": "dev-shared-key-123" |
| 71 | + } |
| 72 | + } |
| 73 | + ``` |
| 74 | + |
| 75 | +2. **Install dependencies:** |
| 76 | + ```bash |
| 77 | + npm install |
| 78 | + cd api && npm install && cd .. |
| 79 | + ``` |
| 80 | + |
| 81 | +3. **Start Azurite:** |
| 82 | + ```bash |
| 83 | + azurite --silent --location ./azurite |
| 84 | + ``` |
| 85 | + |
| 86 | +4. **Create tables:** |
| 87 | + ```bash |
| 88 | + az storage table create --name routines --connection-string "UseDevelopmentStorage=true" |
| 89 | + az storage table create --name exercises --connection-string "UseDevelopmentStorage=true" |
| 90 | + ``` |
| 91 | + |
| 92 | +5. **Run API and UI:** |
| 93 | + ```bash |
| 94 | + # Terminal 1 |
| 95 | + cd api && npm start |
| 96 | + |
| 97 | + # Terminal 2 |
| 98 | + npm run dev |
| 99 | + ``` |
| 100 | + |
| 101 | +### For Users |
| 102 | + |
| 103 | +**Data Migration Options:** |
| 104 | + |
| 105 | +#### Option 1: Manual Re-entry |
| 106 | +If you have limited data, the simplest approach is to re-enter it in the new version. |
| 107 | + |
| 108 | +#### Option 2: Export/Import Script |
| 109 | +Create a migration script to export from IndexedDB and import via API: |
| 110 | + |
| 111 | +```javascript |
| 112 | +// export-data.js - Run in browser console on OLD version |
| 113 | +async function exportData() { |
| 114 | + const { db } = await import('./src/db.ts'); |
| 115 | + |
| 116 | + const routines = await db.routines.toArray(); |
| 117 | + const exercises = await db.exercises.toArray(); |
| 118 | + const exportData = {\n routines,\n exercises\n }; |
| 119 | + |
| 120 | + // Download as JSON |
| 121 | + const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' }); |
| 122 | + const url = URL.createObjectURL(blob); |
| 123 | + const a = document.createElement('a'); |
| 124 | + a.href = url; |
| 125 | + a.download = 'pump-export.json'; |
| 126 | + a.click(); |
| 127 | +} |
| 128 | +
|
| 129 | +function blobToBase64(blob) { |
| 130 | + return new Promise((resolve) => { |
| 131 | + const reader = new FileReader(); |
| 132 | + reader.onloadend = () => { |
| 133 | + resolve(reader.result.split(',')[1]); |
| 134 | + }; |
| 135 | + reader.readAsDataURL(blob); |
| 136 | + }); |
| 137 | +} |
| 138 | +
|
| 139 | +exportData(); |
| 140 | +``` |
| 141 | +
|
| 142 | +```javascript |
| 143 | +// import-data.js - Run via Node.js with NEW version API |
| 144 | +const fs = require('fs'); |
| 145 | +const fetch = require('node-fetch'); |
| 146 | +
|
| 147 | +const API_URL = 'http://localhost:7071/api'; |
| 148 | +const API_KEY = 'dev-shared-key-123'; |
| 149 | +const USER_ID = 'your-user-id'; // Get from Azure SWA auth |
| 150 | +
|
| 151 | +async function importData(filePath) { |
| 152 | + const data = JSON.parse(fs.readFileSync(filePath, 'utf8')); |
| 153 | + |
| 154 | + const headers = { |
| 155 | + 'Content-Type': 'application/json', |
| 156 | + 'x-api-key': API_KEY |
| 157 | + }; |
| 158 | + |
| 159 | + // Import routines |
| 160 | + const routineIdMap = new Map(); |
| 161 | + for (const routine of data.routines) { |
| 162 | + const res = await fetch(`${API_URL}/routines?userId=${USER_ID}`, { |
| 163 | + method: 'POST', |
| 164 | + headers, |
| 165 | + body: JSON.stringify({ |
| 166 | + date: routine.date, |
| 167 | + name: routine.name, |
| 168 | + order: routine.order, |
| 169 | + userId: USER_ID |
| 170 | + }) |
| 171 | + }); |
| 172 | + const created = await res.json(); |
| 173 | + routineIdMap.set(routine.id, created.id); |
| 174 | + } |
| 175 | + |
| 176 | + // Import exercises |
| 177 | + const exerciseIdMap = new Map(); |
| 178 | + for (const exercise of data.exercises) { |
| 179 | + const newRoutineId = routineIdMap.get(exercise.routineId); |
| 180 | + const res = await fetch(`${API_URL}/exercises?userId=${USER_ID}`, { |
| 181 | + method: 'POST', |
| 182 | + headers, |
| 183 | + body: JSON.stringify({ |
| 184 | + ...exercise, |
| 185 | + routineId: newRoutineId, |
| 186 | + userId: USER_ID |
| 187 | + }) |
| 188 | + }); |
| 189 | + const created = await res.json(); |
| 190 | + exerciseIdMap.set(exercise.id, created.id); |
| 191 | + } |
| 192 | + |
| 193 | + console.log('Import complete!'); |
| 194 | +} |
| 195 | +
|
| 196 | +importData('pump-export.json'); |
| 197 | +``` |
| 198 | +
|
| 199 | +## Rollback |
| 200 | +
|
| 201 | +To rollback to the previous version: |
| 202 | +
|
| 203 | +```bash |
| 204 | +git checkout save-x # or whichever branch had the old version |
| 205 | +``` |
| 206 | +
|
| 207 | +The old version will continue to work with its IndexedDB data unchanged. |
| 208 | +
|
| 209 | +## Production Deployment |
| 210 | +
|
| 211 | +See main [README.md](README.md) for Azure deployment instructions. |
| 212 | +
|
| 213 | +Key considerations: |
| 214 | +- Use strong Shared Keys in production |
| 215 | +- Configure CORS properly on Function App |
| 216 | +- Set appropriate Table Storage retention policies |
| 217 | +- Monitor API usage and costs |
| 218 | +
|
| 219 | +## Support |
| 220 | +
|
| 221 | +If you encounter issues during migration: |
| 222 | +1. Check browser console for errors |
| 223 | +2. Check Function App logs in Azure Portal |
| 224 | +3. Verify API_KEY matches between frontend and backend |
| 225 | +4. Ensure tables exist in Storage Account |
| 226 | +
|
0 commit comments