forked from jsnbuchanan/crowd-funder-for-time-pwa
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:
@@ -36,11 +36,15 @@ const MIGRATIONS = [
|
||||
{
|
||||
name: "001_initial",
|
||||
sql: `
|
||||
-- Enable foreign key constraints for data integrity
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
-- Create accounts table with UNIQUE constraint on did
|
||||
CREATE TABLE IF NOT EXISTS accounts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
dateCreated TEXT NOT NULL,
|
||||
derivationPath TEXT,
|
||||
did TEXT NOT NULL,
|
||||
did TEXT NOT NULL UNIQUE, -- UNIQUE constraint for foreign key support
|
||||
identityEncrBase64 TEXT, -- encrypted & base64-encoded
|
||||
mnemonicEncrBase64 TEXT, -- encrypted & base64-encoded
|
||||
passkeyCredIdHex TEXT,
|
||||
@@ -102,7 +106,8 @@ const MIGRATIONS = [
|
||||
profileImageUrl TEXT,
|
||||
publicKeyBase64 TEXT,
|
||||
seesMe BOOLEAN,
|
||||
registered BOOLEAN
|
||||
registered BOOLEAN,
|
||||
iViewContent BOOLEAN DEFAULT TRUE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_contacts_did ON contacts(did);
|
||||
@@ -122,46 +127,22 @@ const MIGRATIONS = [
|
||||
name TEXT PRIMARY KEY,
|
||||
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "002_add_iViewContent_to_contacts",
|
||||
sql: `
|
||||
ALTER TABLE contacts ADD COLUMN iViewContent BOOLEAN DEFAULT TRUE;
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "003_active_did_separation",
|
||||
sql: `
|
||||
-- SIMPLIFIED MIGRATION: Create active_identity table and migrate data
|
||||
-- This migration handles the separation of activeDid from settings
|
||||
-- using a simpler, more reliable approach that works on all SQLite versions
|
||||
|
||||
-- Create new active_identity table with proper constraints
|
||||
|
||||
-- Create active_identity table with foreign key constraint
|
||||
CREATE TABLE IF NOT EXISTS active_identity (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
activeDid TEXT NOT NULL DEFAULT '',
|
||||
lastUpdated TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
activeDid TEXT DEFAULT NULL, -- NULL instead of empty string
|
||||
lastUpdated TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (activeDid) REFERENCES accounts(did) ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
-- Add performance indexes
|
||||
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);
|
||||
|
||||
-- Insert default record (will be updated during migration)
|
||||
INSERT OR IGNORE INTO active_identity (id, activeDid, lastUpdated) VALUES (1, '', datetime('now'));
|
||||
|
||||
-- MIGRATE EXISTING DATA: Copy activeDid from settings to active_identity
|
||||
-- This prevents data loss when migration runs on existing databases
|
||||
-- Use a more robust approach that handles missing columns gracefully
|
||||
UPDATE active_identity
|
||||
SET activeDid = COALESCE(
|
||||
(SELECT activeDid FROM settings WHERE id = 1 AND activeDid IS NOT NULL AND activeDid != ''),
|
||||
''
|
||||
),
|
||||
lastUpdated = datetime('now')
|
||||
WHERE id = 1;
|
||||
`,
|
||||
-- Seed singleton row
|
||||
INSERT INTO active_identity (id, activeDid, lastUpdated) VALUES (1, NULL, datetime('now'));
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -184,4 +165,41 @@ export async function runMigrations<T>(
|
||||
logger.info("[Migration] Running migration service");
|
||||
await runMigrationsService(sqlExec, sqlQuery, extractMigrationNames);
|
||||
logger.info("[Migration] Database migrations completed");
|
||||
|
||||
// Bootstrapping: Ensure active account is selected after migrations
|
||||
logger.info("[Migration] Running bootstrapping hooks");
|
||||
try {
|
||||
// Check if we have accounts but no active selection
|
||||
const accountsResult = await sqlQuery("SELECT COUNT(*) FROM accounts");
|
||||
const accountsCount = accountsResult
|
||||
? (accountsResult.values?.[0]?.[0] as number)
|
||||
: 0;
|
||||
|
||||
const activeResult = await sqlQuery(
|
||||
"SELECT activeDid FROM active_identity WHERE id = 1",
|
||||
);
|
||||
const activeDid = activeResult
|
||||
? (activeResult.values?.[0]?.[0] as string)
|
||||
: null;
|
||||
|
||||
if (accountsCount > 0 && (!activeDid || activeDid === "")) {
|
||||
logger.info("[Migration] Auto-selecting first account as active");
|
||||
const firstAccountResult = await sqlQuery(
|
||||
"SELECT did FROM accounts ORDER BY dateCreated, did LIMIT 1",
|
||||
);
|
||||
const firstAccountDid = firstAccountResult
|
||||
? (firstAccountResult.values?.[0]?.[0] as string)
|
||||
: null;
|
||||
|
||||
if (firstAccountDid) {
|
||||
await sqlExec(
|
||||
"UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1",
|
||||
[firstAccountDid],
|
||||
);
|
||||
logger.info(`[Migration] Set active account to: ${firstAccountDid}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn("[Migration] Bootstrapping hook failed (non-critical):", error);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user