Browse Source

chore: update plan for handling MASTER_SETTINGS_KEY

pull/188/head
Matthew Raymer 3 weeks ago
parent
commit
fad7093fbd
  1. 253
      doc/activeDid-migration-plan.md

253
doc/activeDid-migration-plan.md

@ -1,7 +1,7 @@
# ActiveDid Migration Plan - Separate Table Architecture # ActiveDid Migration Plan - Separate Table Architecture
**Author**: Matthew Raymer **Author**: Matthew Raymer
**Date**: 2025-08-29T07:24Z **Date**: 2025-08-29T08:03Z
**Status**: 🎯 **PLANNING** - Active migration planning phase **Status**: 🎯 **PLANNING** - Active migration planning phase
## Objective ## Objective
@ -85,29 +85,35 @@ flowchart TD
## Repro: End-to-End Procedure ## Repro: End-to-End Procedure
### Phase 1: Enhanced Schema Creation ### Phase 1: Enhanced Schema Creation via migration.ts
```sql ```typescript
-- Create new active_identity table with proper constraints // Add to MIGRATIONS array in src/db-sql/migration.ts
CREATE TABLE active_identity ( {
name: "003_active_did_separate_table",
sql: `
-- Create new active_identity table with proper constraints
CREATE TABLE IF NOT EXISTS active_identity (
id INTEGER PRIMARY KEY CHECK (id = 1), id INTEGER PRIMARY KEY CHECK (id = 1),
activeDid TEXT NOT NULL, activeDid TEXT NOT NULL,
lastUpdated TEXT NOT NULL DEFAULT (datetime('now')), lastUpdated TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (activeDid) REFERENCES accounts(did) ON DELETE CASCADE FOREIGN KEY (activeDid) REFERENCES accounts(did) ON DELETE CASCADE
); );
-- Add performance indexes -- Add performance indexes
CREATE INDEX IF NOT EXISTS idx_active_identity_activeDid ON active_identity(activeDid); CREATE INDEX IF NOT EXISTS idx_active_identity_activeDid ON active_identity(activeDid);
CREATE UNIQUE INDEX IF NOT EXISTS idx_active_identity_single_record ON active_identity(id); CREATE UNIQUE INDEX IF NOT EXISTS idx_active_identity_single_record ON active_identity(id);
-- Insert default record (will be updated during migration) -- Insert default record (will be updated during migration)
INSERT INTO active_identity (id, activeDid, lastUpdated) VALUES (1, '', datetime('now')); INSERT OR IGNORE INTO active_identity (id, activeDid, lastUpdated) VALUES (1, '', datetime('now'));
`,
},
``` ```
### Phase 2: Enhanced Data Migration with Validation ### Phase 2: Enhanced Data Migration with Validation
```typescript ```typescript
// Enhanced migration script with comprehensive validation // Enhanced migration function with comprehensive validation
async function migrateActiveDidToSeparateTable(): Promise<MigrationResult> { async function migrateActiveDidToSeparateTable(): Promise<MigrationResult> {
const result: MigrationResult = { const result: MigrationResult = {
success: false, success: false,
@ -117,7 +123,7 @@ async function migrateActiveDidToSeparateTable(): Promise<MigrationResult> {
}; };
try { try {
// 1. Get current activeDid from settings // 1. Get current activeDid from settings (legacy approach)
const currentSettings = await retrieveSettingsForDefaultAccount(); const currentSettings = await retrieveSettingsForDefaultAccount();
const activeDid = currentSettings.activeDid; const activeDid = currentSettings.activeDid;
@ -137,27 +143,20 @@ async function migrateActiveDidToSeparateTable(): Promise<MigrationResult> {
return result; return result;
} }
// 3. Check if active_identity table already has data // 3. Update active_identity table (new system)
const existingActiveIdentity = await dbQuery(
"SELECT activeDid FROM active_identity WHERE id = 1"
);
if (existingActiveIdentity?.values?.length) {
// Update existing record
await dbExec( await dbExec(
"UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1", "UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1",
[activeDid] [activeDid]
); );
} else {
// Insert new record // 4. Ensure legacy settings.activeDid stays in sync (backward compatibility)
// This maintains compatibility with IndexedDB migration service
await dbExec( await dbExec(
"INSERT INTO active_identity (id, activeDid, lastUpdated) VALUES (1, ?, datetime('now'))", "UPDATE settings SET activeDid = ? WHERE id = ?",
[activeDid] [activeDid, MASTER_SETTINGS_KEY]
); );
}
result.success = true; dataMigrated = 1;
result.dataMigrated = 1;
result.warnings.push(`Successfully migrated activeDid: ${activeDid}`); result.warnings.push(`Successfully migrated activeDid: ${activeDid}`);
} catch (error) { } catch (error) {
@ -167,17 +166,9 @@ async function migrateActiveDidToSeparateTable(): Promise<MigrationResult> {
return result; return result;
} }
// Migration result interface
interface MigrationResult {
success: boolean;
errors: string[];
warnings: string[];
dataMigrated: number;
}
``` ```
### Phase 3: Focused API Updates ### Phase 3: Focused API Updates with Dual-Write Pattern
```typescript ```typescript
// Updated PlatformServiceMixin method - maintains backward compatibility // Updated PlatformServiceMixin method - maintains backward compatibility
@ -201,93 +192,20 @@ async $accountSettings(did?: string, defaults: Settings = {}): Promise<Settings>
} }
} }
// New method for active identity management // Enhanced update activeDid method with dual-write pattern
async $getActiveIdentity(): Promise<{ activeDid: string | null }> {
try {
const result = await this.$dbQuery(
"SELECT activeDid FROM active_identity WHERE id = 1"
);
if (!result?.values?.length) {
return { activeDid: null };
}
return { activeDid: result.values[0][0] as string };
} catch (error) {
logger.error("[Settings Trace] ❌ Failed to get active identity:", error);
return { activeDid: null };
}
}
// Enhanced method to get settings without activeDid
async _getSettingsWithoutActiveDid(): Promise<Settings> {
const result = await this.$dbQuery(
"SELECT id, accountDid, apiServer, filterFeedByNearby, filterFeedByVisible, " +
"finishedOnboarding, firstName, hideRegisterPromptOnNewContact, isRegistered, " +
"lastName, lastAckedOfferToUserJwtId, lastAckedOfferToUserProjectsJwtId, " +
"lastNotifiedClaimId, lastViewedClaimId, notifyingNewActivityTime, " +
"notifyingReminderMessage, notifyingReminderTime, partnerApiServer, " +
"passkeyExpirationMinutes, profileImageUrl, searchBoxes, showContactGivesInline, " +
"showGeneralAdvanced, showShortcutBvc, vapid, warnIfProdServer, warnIfTestServer, " +
"webPushServer FROM settings WHERE id = ?",
[MASTER_SETTINGS_KEY]
);
if (!result?.values?.length) {
return DEFAULT_SETTINGS;
}
return this._mapColumnsToValues(result.columns, result.values)[0] as Settings;
}
// Enhanced save settings method
async $saveSettings(changes: Partial<Settings>): Promise<boolean> {
try {
// Remove fields that shouldn't be updated
const { accountDid, id, activeDid, ...safeChanges } = changes;
if (Object.keys(safeChanges).length > 0) {
// Convert settings for database storage
const convertedChanges = this._convertSettingsForStorage(safeChanges);
const setParts: string[] = [];
const params: unknown[] = [];
Object.entries(convertedChanges).forEach(([key, value]) => {
if (value !== undefined) {
setParts.push(`${key} = ?`);
params.push(value);
}
});
if (setParts.length > 0) {
params.push(MASTER_SETTINGS_KEY);
await this.$dbExec(
`UPDATE settings SET ${setParts.join(", ")} WHERE id = ?`,
params,
);
}
}
// Handle activeDid separately in new table
if (changes.activeDid !== undefined) {
await this.$updateActiveDid(changes.activeDid);
}
return true;
} catch (error) {
logger.error("[PlatformServiceMixin] Error saving settings:", error);
return false;
}
}
// Enhanced update activeDid method
async $updateActiveDid(newDid: string | null): Promise<boolean> { async $updateActiveDid(newDid: string | null): Promise<boolean> {
try { try {
if (newDid === null) { if (newDid === null) {
// Clear active identity // Clear active identity in both tables
await this.$dbExec( await this.$dbExec(
"UPDATE active_identity SET activeDid = '', lastUpdated = datetime('now') WHERE id = 1" "UPDATE active_identity SET activeDid = '', lastUpdated = datetime('now') WHERE id = 1"
); );
// Keep legacy field in sync (backward compatibility)
await this.$dbExec(
"UPDATE settings SET activeDid = '' WHERE id = ?",
[MASTER_SETTINGS_KEY]
);
} else { } else {
// Validate DID exists before setting // Validate DID exists before setting
const accountExists = await this.$dbQuery( const accountExists = await this.$dbQuery(
@ -300,11 +218,17 @@ async $updateActiveDid(newDid: string | null): Promise<boolean> {
return false; return false;
} }
// Update active identity // Update active identity in new table
await this.$dbExec( await this.$dbExec(
"UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1", "UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1",
[newDid] [newDid]
); );
// Keep legacy field in sync (backward compatibility)
await this.$dbExec(
"UPDATE settings SET activeDid = ? WHERE id = ?",
[newDid, MASTER_SETTINGS_KEY]
);
} }
// Update internal tracking // Update internal tracking
@ -431,30 +355,30 @@ async $getMergedSettings(defaultKey: string, accountDid?: string, defaultFallbac
## What Works (Evidence) ## What Works (Evidence)
- ✅ **Current activeDid storage** in settings table - ✅ **Current activeDid storage** in settings table
- **Time**: 2025-08-29T07:24Z - **Time**: 2025-08-29T08:03Z
- **Evidence**: `src/db/tables/settings.ts:25` - activeDid field exists - **Evidence**: `src/db-sql/migration.ts:67` - activeDid field exists in initial migration
- **Verify at**: Current database schema and Settings type definition - **Verify at**: Current database schema and Settings type definition
- ✅ **PlatformServiceMixin integration** with activeDid - ✅ **PlatformServiceMixin integration** with activeDid
- **Time**: 2025-08-29T07:24Z - **Time**: 2025-08-29T08:03Z
- **Evidence**: `src/utils/PlatformServiceMixin.ts:108` - activeDid tracking - **Evidence**: `src/utils/PlatformServiceMixin.ts:108` - activeDid tracking
- **Verify at**: Component usage across all platforms - **Verify at**: Component usage across all platforms
- ✅ **Database migration infrastructure** exists - ✅ **Database migration infrastructure** exists
- **Time**: 2025-08-29T07:24Z - **Time**: 2025-08-29T08:03Z
- **Evidence**: `src/db-sql/migration.ts:31` - migration system in place - **Evidence**: `src/db-sql/migration.ts:31` - migration system in place
- **Verify at**: Existing migration scripts and database versioning - **Verify at**: Existing migration scripts and database versioning
## What Doesn't (Evidence & Hypotheses) ## What Doesn't (Evidence & Hypotheses)
- ❌ **No separate active_identity table** exists - ❌ **No separate active_identity table** exists
- **Time**: 2025-08-29T07:24Z - **Time**: 2025-08-29T08:03Z
- **Evidence**: Database schema only shows settings table - **Evidence**: Database schema only shows settings table
- **Hypothesis**: Table needs to be created as part of migration - **Hypothesis**: Table needs to be created as part of migration
- **Next probe**: Create migration script for new table - **Next probe**: Add migration to existing MIGRATIONS array
- ❌ **Data corruption issues** with orphaned activeDid references - ❌ **Data corruption issues** with orphaned activeDid references
- **Time**: 2025-08-29T07:24Z - **Time**: 2025-08-29T08:03Z
- **Evidence**: `IdentitySwitcherView.vue:175` - `hasCorruptedIdentity` detection - **Evidence**: `IdentitySwitcherView.vue:175` - `hasCorruptedIdentity` detection
- **Hypothesis**: Current schema allows activeDid to point to non-existent accounts - **Hypothesis**: Current schema allows activeDid to point to non-existent accounts
- **Next probe**: Implement foreign key constraints in new table - **Next probe**: Implement foreign key constraints in new table
@ -522,7 +446,7 @@ async function rollbackActiveDidMigration(): Promise<boolean> {
| Owner | Task | Exit Criteria | Target Date (UTC) | | Owner | Task | Exit Criteria | Target Date (UTC) |
|-------|------|---------------|-------------------| |-------|------|---------------|-------------------|
| Development Team | Create enhanced migration script | Migration script with validation and rollback | 2025-08-30 | | Development Team | Add migration to existing MIGRATIONS array | Migration script integrated with existing system | 2025-08-30 |
| Development Team | Update type definitions | Settings type updated, ActiveIdentity type created | 2025-08-30 | | Development Team | Update type definitions | Settings type updated, ActiveIdentity type created | 2025-08-30 |
| Development Team | Update PlatformServiceMixin | Core methods updated and tested | 2025-08-31 | | Development Team | Update PlatformServiceMixin | Core methods updated and tested | 2025-08-31 |
| Development Team | Implement foreign key constraints | Schema validation prevents corruption | 2025-08-31 | | Development Team | Implement foreign key constraints | Schema validation prevents corruption | 2025-08-31 |
@ -552,7 +476,7 @@ async function rollbackActiveDidMigration(): Promise<boolean> {
## Collaboration Hooks ## Collaboration Hooks
- **Sign-off checklist**: - **Sign-off checklist**:
- [ ] Migration script tested on development database - [ ] Migration script integrated with existing MIGRATIONS array
- [ ] Foreign key constraints implemented and tested - [ ] Foreign key constraints implemented and tested
- [ ] PlatformServiceMixin updated and tested - [ ] PlatformServiceMixin updated and tested
- [ ] Rollback procedures validated - [ ] Rollback procedures validated
@ -571,31 +495,33 @@ async function rollbackActiveDidMigration(): Promise<boolean> {
## What Needs to Change ## What Needs to Change
### **1. Database Schema** ### **1. Database Schema via migration.ts**
- Add migration to existing MIGRATIONS array in `src/db-sql/migration.ts`
- Create `active_identity` table with foreign key constraints - Create `active_identity` table with foreign key constraints
- Add performance indexes - Add performance indexes
- Remove `activeDid` field from `settings` table - **Keep `activeDid` field in `settings` table temporarily** for backward compatibility
- **Preserve `MASTER_SETTINGS_KEY = "1"`** for legacy migration support
### **2. PlatformServiceMixin Methods** ### **2. PlatformServiceMixin Methods**
- `$accountSettings()` - integrate with new table - `$accountSettings()` - integrate with new table while maintaining backward compatibility
- `$saveSettings()` - handle activeDid in new table - `$saveSettings()` - handle activeDid in new table, sync with legacy field
- `$updateActiveDid()` - validate and update new table - `$updateActiveDid()` - validate and update new table, sync with legacy field
- `$getActiveIdentity()` - new method for identity management - `$getActiveIdentity()` - new method for identity management
### **3. Master Settings Functions** ### **3. Master Settings Functions**
- `retrieveSettingsForDefaultAccount()` - integrate with new table - `retrieveSettingsForDefaultAccount()` - integrate with new table while preserving legacy support
- `$getMergedSettings()` - integrate with new table - `$getMergedSettings()` - integrate with new table while preserving legacy support
### **4. Migration Scripts** ### **4. Type Definitions**
- Create migration script with validation - **Keep `activeDid` in Settings type temporarily** for backward compatibility
- Implement rollback procedures
- Add data corruption detection
### **5. Type Definitions**
- Update Settings type to remove activeDid
- Create ActiveIdentity type for new table - Create ActiveIdentity type for new table
- Update related interfaces - Update related interfaces
### **5. Legacy Compatibility**
- **Preserve `MASTER_SETTINGS_KEY = "1"`** for IndexedDB migration service
- **Maintain dual-write pattern** during transition period
- **Ensure legacy clients can still migrate** from Dexie to SQLite
## What Doesn't Need to Change ## What Doesn't Need to Change
- **All Vue components** - API layer handles migration transparently - **All Vue components** - API layer handles migration transparently
@ -603,3 +529,48 @@ async function rollbackActiveDidMigration(): Promise<boolean> {
- **User interface** - No changes to identity selection UI - **User interface** - No changes to identity selection UI
- **Authentication flow** - Existing system unchanged - **Authentication flow** - Existing system unchanged
- **Component logic** - All activeDid handling through API methods - **Component logic** - All activeDid handling through API methods
- **Migration system** - Use existing migration.ts approach, not separate files
- **IndexedDB migration service** - Must continue working for legacy clients
## Enhanced Architecture: Dual-Write Pattern
### **Phase 1: Add New Table (Current)**
```typescript
// Create active_identity table
// Keep existing settings.activeDid for backward compatibility
// Use dual-write pattern during transition
```
### **Phase 2: Dual-Write Pattern**
```typescript
// When updating activeDid:
// 1. Update active_identity table (new system)
// 2. Update settings.activeDid (legacy compatibility)
// 3. Ensure both stay in sync
```
### **Phase 3: Future Cleanup (Not in Current Scope)**
```typescript
// Eventually:
// 1. Remove activeDid from settings table
// 2. Deprecate MASTER_SETTINGS_KEY
// 3. Use pure accountDid IS NULL pattern
// 4. Update IndexedDB migration service
```
## Backward Compatibility Requirements
### **Critical: IndexedDB Migration Service**
- **Must continue working** for users migrating from Dexie
- **Must recognize `id = "1"`** as master settings
- **Must preserve existing migration paths**
### **Important: Legacy Database Operations**
- **Must continue working** for existing SQLite databases
- **Must handle both old and new patterns**
- **Must not break existing queries**
### **Desired: Cleaner Architecture**
- **New operations** use `accountDid IS NULL` pattern
- **Legacy operations** continue using `MASTER_SETTINGS_KEY`
- **Gradual migration** toward cleaner patterns
Loading…
Cancel
Save