feat: implement Active Pointer + Smart Deletion Pattern for accounts

- Consolidate migrations: merge 002/003 into 001_initial with UNIQUE did constraint
- Add foreign key: active_identity.activeDid REFERENCES accounts.did ON DELETE RESTRICT
- Replace empty string defaults with NULL for proper empty state handling
- Implement atomic smart deletion with auto-switch logic in IdentitySwitcherView
- Add DAL methods: $getAllAccountDids, $getActiveDid, $setActiveDid, $pickNextAccountDid
- Add migration bootstrapping to auto-select first account if none selected
- Block deletion of last remaining account with user notification

Refs: doc/active-pointer-smart-deletion-pattern.md
This commit is contained in:
Matthew Raymer
2025-09-07 10:30:48 +00:00
parent c9cfeafd50
commit a20c321a16
4 changed files with 540 additions and 38 deletions

View File

@@ -737,6 +737,87 @@ export const PlatformServiceMixin = {
}
},
// =================================================
// SMART DELETION PATTERN DAL METHODS
// =================================================
/**
* Get all account DIDs ordered by creation date
* Required for smart deletion pattern
*/
async $getAllAccountDids(): Promise<string[]> {
const result = await this.$dbQuery(
"SELECT did FROM accounts ORDER BY dateCreated, did",
);
return result?.values?.map((row) => row[0] as string) || [];
},
/**
* Get account DID by ID
* Required for smart deletion pattern
*/
async $getAccountDidById(id: number): Promise<string> {
const result = await this.$dbQuery(
"SELECT did FROM accounts WHERE id = ?",
[id],
);
return result?.values?.[0]?.[0] as string;
},
/**
* Get active DID (returns null if none selected)
* Required for smart deletion pattern
*/
async $getActiveDid(): Promise<string | null> {
const result = await this.$dbQuery(
"SELECT activeDid FROM active_identity WHERE id = 1",
);
return (result?.values?.[0]?.[0] as string) || null;
},
/**
* Set active DID (can be null for no selection)
* Required for smart deletion pattern
*/
async $setActiveDid(did: string | null): Promise<void> {
await this.$dbExec(
"UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1",
[did],
);
},
/**
* Count total accounts
* Required for smart deletion pattern
*/
async $countAccounts(): Promise<number> {
const result = await this.$dbQuery("SELECT COUNT(*) FROM accounts");
return (result?.values?.[0]?.[0] as number) || 0;
},
/**
* Deterministic "next" picker for account selection
* Required for smart deletion pattern
*/
$pickNextAccountDid(all: string[], current?: string): string {
const sorted = [...all].sort();
if (!current) return sorted[0];
const i = sorted.indexOf(current);
return sorted[(i + 1) % sorted.length];
},
/**
* Ensure an active account is selected (repair hook)
* Required for smart deletion pattern bootstrapping
*/
async $ensureActiveSelected(): Promise<void> {
const active = await this.$getActiveDid();
const all = await this.$getAllAccountDids();
if (active === null && all.length > 0) {
await this.$setActiveDid(this.$pickNextAccountDid(all));
}
},
// =================================================
// ULTRA-CONCISE DATABASE METHODS (shortest names)
// =================================================