forked from jsnbuchanan/crowd-funder-for-time-pwa
feat: Complete Migration 004 Complexity Resolution (Phases 1-4)
- Phase 1: Simplify Migration Definition ✅ * Remove duplicate SQL definitions from migration 004 * Eliminate recovery logic that could cause duplicate execution * Establish single source of truth for migration SQL - Phase 2: Fix Database Result Handling ✅ * Remove DatabaseResult type assumptions from migration code * Implement database-agnostic result extraction with extractSingleValue() * Normalize results from AbsurdSqlDatabaseService and CapacitorPlatformService - Phase 3: Ensure Atomic Execution ✅ * Remove individual statement execution logic * Execute migrations as single atomic SQL blocks only * Add explicit rollback instructions and failure cause logging * Ensure migration tracking is accurate - Phase 4: Remove Excessive Debugging ✅ * Move detailed logging to development-only mode * Preserve essential error logging for production * Optimize startup performance by reducing logging overhead * Maintain full debugging capability in development Migration system now follows single-source, atomic execution principle with improved performance and comprehensive error handling. Timestamp: 2025-09-17 05:08:05 UTC
This commit is contained in:
@@ -6,12 +6,6 @@ import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
|
||||
import { arrayBufferToBase64 } from "@/libs/crypto";
|
||||
import { logger } from "@/utils/logger";
|
||||
|
||||
// Database result interface for SQLite queries
|
||||
interface DatabaseResult {
|
||||
values?: unknown[][];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// Generate a random secret for the secret table
|
||||
|
||||
// It's not really secure to maintain the secret next to the user's data.
|
||||
@@ -183,30 +177,33 @@ const MIGRATIONS = [
|
||||
DELETE FROM settings WHERE accountDid IS NULL;
|
||||
UPDATE settings SET activeDid = NULL;
|
||||
`,
|
||||
// Split into individual statements for better error handling
|
||||
statements: [
|
||||
"PRAGMA foreign_keys = ON",
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_did_unique ON accounts(did)",
|
||||
`CREATE TABLE IF NOT EXISTS active_identity (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
activeDid TEXT REFERENCES accounts(did) ON DELETE RESTRICT,
|
||||
lastUpdated TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)`,
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS idx_active_identity_single_record ON active_identity(id)",
|
||||
`INSERT INTO active_identity (id, activeDid, lastUpdated)
|
||||
SELECT 1, NULL, datetime('now')
|
||||
WHERE NOT EXISTS (SELECT 1 FROM active_identity WHERE id = 1)`,
|
||||
`UPDATE active_identity
|
||||
SET activeDid = (SELECT activeDid FROM settings WHERE id = 1),
|
||||
lastUpdated = datetime('now')
|
||||
WHERE id = 1
|
||||
AND EXISTS (SELECT 1 FROM settings WHERE id = 1 AND activeDid IS NOT NULL AND activeDid != '')`,
|
||||
"DELETE FROM settings WHERE accountDid IS NULL",
|
||||
"UPDATE settings SET activeDid = NULL",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Extract single value from database query result
|
||||
* Works with different database service result formats
|
||||
*/
|
||||
function extractSingleValue<T>(result: T): string | number | null {
|
||||
if (!result) return null;
|
||||
|
||||
// Handle AbsurdSQL format: QueryExecResult[]
|
||||
if (Array.isArray(result) && result.length > 0 && result[0]?.values) {
|
||||
const values = result[0].values;
|
||||
return values.length > 0 ? values[0][0] : null;
|
||||
}
|
||||
|
||||
// Handle Capacitor SQLite format: { values: unknown[][] }
|
||||
if (typeof result === "object" && result !== null && "values" in result) {
|
||||
const values = (result as { values: unknown[][] }).values;
|
||||
return values && values.length > 0
|
||||
? (values[0][0] as string | number)
|
||||
: null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param sqlExec - A function that executes a SQL statement and returns the result
|
||||
* @param extractMigrationNames - A function that extracts the names (string array) from "select name from migrations"
|
||||
@@ -216,26 +213,36 @@ export async function runMigrations<T>(
|
||||
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
|
||||
extractMigrationNames: (result: T) => Set<string>,
|
||||
): Promise<void> {
|
||||
logger.debug("[Migration] Starting database migrations");
|
||||
// Only log migration start in development
|
||||
const isDevelopment = process.env.VITE_PLATFORM === "development";
|
||||
if (isDevelopment) {
|
||||
logger.debug("[Migration] Starting database migrations");
|
||||
}
|
||||
|
||||
for (const migration of MIGRATIONS) {
|
||||
logger.debug("[Migration] Registering migration:", migration.name);
|
||||
if (isDevelopment) {
|
||||
logger.debug("[Migration] Registering migration:", migration.name);
|
||||
}
|
||||
registerMigration(migration);
|
||||
}
|
||||
|
||||
logger.debug("[Migration] Running migration service");
|
||||
if (isDevelopment) {
|
||||
logger.debug("[Migration] Running migration service");
|
||||
}
|
||||
await runMigrationsService(sqlExec, sqlQuery, extractMigrationNames);
|
||||
logger.debug("[Migration] Database migrations completed");
|
||||
|
||||
if (isDevelopment) {
|
||||
logger.debug("[Migration] Database migrations completed");
|
||||
}
|
||||
|
||||
// Bootstrapping: Ensure active account is selected after migrations
|
||||
logger.debug("[Migration] Running bootstrapping hooks");
|
||||
if (isDevelopment) {
|
||||
logger.debug("[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 as DatabaseResult).values
|
||||
? ((accountsResult as DatabaseResult).values?.[0]?.[0] as number)
|
||||
: 0;
|
||||
const accountsCount = (extractSingleValue(accountsResult) as number) || 0;
|
||||
|
||||
// Check if active_identity table exists, and if not, try to recover
|
||||
let activeDid: string | null = null;
|
||||
@@ -243,90 +250,26 @@ export async function runMigrations<T>(
|
||||
const activeResult = await sqlQuery(
|
||||
"SELECT activeDid FROM active_identity WHERE id = 1",
|
||||
);
|
||||
activeDid =
|
||||
activeResult && (activeResult as DatabaseResult).values
|
||||
? ((activeResult as DatabaseResult).values?.[0]?.[0] as string)
|
||||
: null;
|
||||
activeDid = (extractSingleValue(activeResult) as string) || null;
|
||||
} catch (error) {
|
||||
// Table doesn't exist - this means migration 004 failed but was marked as applied
|
||||
logger.warn(
|
||||
"[Migration] active_identity table missing, attempting recovery",
|
||||
);
|
||||
|
||||
// Check if migration 004 is marked as applied
|
||||
const migrationResult = await sqlQuery(
|
||||
"SELECT name FROM migrations WHERE name = '004_active_identity_management'",
|
||||
);
|
||||
const isMigrationMarked =
|
||||
migrationResult && (migrationResult as DatabaseResult).values
|
||||
? ((migrationResult as DatabaseResult).values?.length ?? 0) > 0
|
||||
: false;
|
||||
|
||||
if (isMigrationMarked) {
|
||||
logger.warn(
|
||||
"[Migration] Migration 004 marked as applied but table missing - recreating table",
|
||||
// Table doesn't exist - migration 004 may not have run yet
|
||||
if (isDevelopment) {
|
||||
logger.debug(
|
||||
"[Migration] active_identity table not found - migration may not have run",
|
||||
);
|
||||
|
||||
// Recreate the active_identity table using the individual statements
|
||||
const statements = [
|
||||
"PRAGMA foreign_keys = ON",
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_did_unique ON accounts(did)",
|
||||
`CREATE TABLE IF NOT EXISTS active_identity (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
activeDid TEXT REFERENCES accounts(did) ON DELETE RESTRICT,
|
||||
lastUpdated TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)`,
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS idx_active_identity_single_record ON active_identity(id)",
|
||||
`INSERT INTO active_identity (id, activeDid, lastUpdated)
|
||||
SELECT 1, NULL, datetime('now')
|
||||
WHERE NOT EXISTS (SELECT 1 FROM active_identity WHERE id = 1)`,
|
||||
`UPDATE active_identity
|
||||
SET activeDid = (SELECT activeDid FROM settings WHERE id = 1),
|
||||
lastUpdated = datetime('now')
|
||||
WHERE id = 1
|
||||
AND EXISTS (SELECT 1 FROM settings WHERE id = 1 AND activeDid IS NOT NULL AND activeDid != '')`,
|
||||
"DELETE FROM settings WHERE accountDid IS NULL",
|
||||
"UPDATE settings SET activeDid = NULL",
|
||||
];
|
||||
|
||||
for (const statement of statements) {
|
||||
try {
|
||||
await sqlExec(statement);
|
||||
} catch (stmtError) {
|
||||
logger.warn(
|
||||
`[Migration] Recovery statement failed: ${statement}`,
|
||||
stmtError,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get activeDid again after recovery
|
||||
try {
|
||||
const activeResult = await sqlQuery(
|
||||
"SELECT activeDid FROM active_identity WHERE id = 1",
|
||||
);
|
||||
activeDid =
|
||||
activeResult && (activeResult as DatabaseResult).values
|
||||
? ((activeResult as DatabaseResult).values?.[0]?.[0] as string)
|
||||
: null;
|
||||
} catch (recoveryError) {
|
||||
logger.error(
|
||||
"[Migration] Recovery failed - active_identity table still not accessible",
|
||||
recoveryError,
|
||||
);
|
||||
}
|
||||
}
|
||||
activeDid = null;
|
||||
}
|
||||
|
||||
if (accountsCount > 0 && (!activeDid || activeDid === "")) {
|
||||
logger.debug("[Migration] Auto-selecting first account as active");
|
||||
if (isDevelopment) {
|
||||
logger.debug("[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 as DatabaseResult).values
|
||||
? ((firstAccountResult as DatabaseResult).values?.[0]?.[0] as string)
|
||||
: null;
|
||||
(extractSingleValue(firstAccountResult) as string) || null;
|
||||
|
||||
if (firstAccountDid) {
|
||||
await sqlExec(
|
||||
|
||||
Reference in New Issue
Block a user