fix problem with hidden contacts due to bad iViewContent values, and rename to hideTheirContent
This commit is contained in:
@@ -175,6 +175,7 @@ const MIGRATIONS = [
|
||||
},
|
||||
{
|
||||
name: "002_add_iViewContent_to_contacts",
|
||||
// Note that many times iViewContent was set to null despite the DEFAULT setting.
|
||||
sql: `
|
||||
ALTER TABLE contacts ADD COLUMN iViewContent BOOLEAN DEFAULT TRUE;
|
||||
`,
|
||||
@@ -213,6 +214,60 @@ const MIGRATIONS = [
|
||||
CREATE INDEX idx_contact_labels_did ON contact_labels(did);
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "007_add_hideTheirContent_to_contacts",
|
||||
// Since we have problems where iViewContent is not set, let's default to show content.
|
||||
// Add hideTheirContent: null/absent/false = show content (safe default)
|
||||
sql: `
|
||||
ALTER TABLE contacts ADD COLUMN hideTheirContent BOOLEAN DEFAULT 0;
|
||||
UPDATE contacts SET hideTheirContent = CASE WHEN iViewContent = 0 THEN 1 ELSE 0 END WHERE iViewContent IS NOT NULL;
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "008_remove_iViewContent_from_contacts",
|
||||
// Recreate contacts without iViewContent: backup, drop, recreate, restore
|
||||
sql: `
|
||||
PRAGMA foreign_keys = OFF;
|
||||
|
||||
CREATE TABLE _contact_labels_backup_008 AS SELECT * FROM contact_labels;
|
||||
DROP TABLE IF EXISTS contact_labels;
|
||||
|
||||
CREATE TABLE _contacts_backup_008 (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
did TEXT NOT NULL,
|
||||
name TEXT,
|
||||
contactMethods TEXT,
|
||||
nextPubKeyHashB64 TEXT,
|
||||
notes TEXT,
|
||||
profileImageUrl TEXT,
|
||||
publicKeyBase64 TEXT,
|
||||
seesMe BOOLEAN,
|
||||
registered BOOLEAN,
|
||||
hideTheirContent BOOLEAN DEFAULT 0
|
||||
);
|
||||
INSERT INTO _contacts_backup_008 (id, did, name, contactMethods, nextPubKeyHashB64, notes, profileImageUrl, publicKeyBase64, seesMe, registered, hideTheirContent)
|
||||
SELECT id, did, name, contactMethods, nextPubKeyHashB64, notes, profileImageUrl, publicKeyBase64, seesMe, registered, COALESCE(hideTheirContent, 0) FROM contacts;
|
||||
|
||||
DROP TABLE contacts;
|
||||
ALTER TABLE _contacts_backup_008 RENAME TO contacts;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_contacts_did ON contacts(did);
|
||||
CREATE INDEX IF NOT EXISTS idx_contacts_name ON contacts(name);
|
||||
|
||||
CREATE TABLE contact_labels (
|
||||
did TEXT NOT NULL,
|
||||
label TEXT NOT NULL,
|
||||
PRIMARY KEY (did, label),
|
||||
FOREIGN KEY (did) REFERENCES contacts(did) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX idx_contact_labels_label ON contact_labels(label);
|
||||
CREATE INDEX idx_contact_labels_did ON contact_labels(did);
|
||||
INSERT INTO contact_labels SELECT * FROM _contact_labels_backup_008;
|
||||
DROP TABLE _contact_labels_backup_008;
|
||||
|
||||
PRAGMA foreign_keys = ON;
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,7 +16,8 @@ export type Contact = {
|
||||
|
||||
did: string;
|
||||
contactMethods?: Array<ContactMethod>;
|
||||
iViewContent?: boolean;
|
||||
/** When true, hide this contact's activity from the feed. Default (null/undefined) = show. */
|
||||
hideTheirContent?: boolean;
|
||||
name?: string;
|
||||
nextPubKeyHashB64?: string; // base64-encoded SHA256 hash of next public key
|
||||
notes?: string;
|
||||
|
||||
@@ -12,9 +12,7 @@
|
||||
*
|
||||
* 1. **Single Application**: Each migration runs exactly once per database
|
||||
* 2. **Tracked Execution**: All applied migrations are recorded in a migrations table
|
||||
* 3. **Schema Validation**: Actual database schema is validated before and after migrations
|
||||
* 4. **Graceful Recovery**: Handles cases where schema exists but tracking is missing
|
||||
* 5. **Comprehensive Logging**: Detailed logging for debugging and monitoring
|
||||
* 3. **Comprehensive Logging**: Detailed logging for debugging and monitoring
|
||||
*
|
||||
* ## Migration Flow
|
||||
*
|
||||
@@ -26,9 +24,7 @@
|
||||
* b. Check if schema already exists
|
||||
* c. Skip if already applied
|
||||
* d. Apply migration SQL
|
||||
* e. Validate schema was created
|
||||
* f. Record migration as applied
|
||||
* 4. Final validation of all migrations
|
||||
* e. Record migration as applied
|
||||
* ```
|
||||
*
|
||||
* ## Usage Example
|
||||
@@ -77,25 +73,6 @@ interface Migration {
|
||||
statements?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Migration validation result
|
||||
*
|
||||
* Contains the results of validating that a migration was successfully
|
||||
* applied by checking the actual database schema.
|
||||
*
|
||||
* @interface MigrationValidation
|
||||
*/
|
||||
interface MigrationValidation {
|
||||
/** Whether the migration validation passed overall */
|
||||
isValid: boolean;
|
||||
/** Whether expected tables exist */
|
||||
tableExists: boolean;
|
||||
/** Whether expected columns exist */
|
||||
hasExpectedColumns: boolean;
|
||||
/** List of validation errors encountered */
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Migration registry to store and manage database migrations
|
||||
*
|
||||
@@ -207,354 +184,6 @@ export function registerMigration(migration: Migration): void {
|
||||
migrationRegistry.registerMigration(migration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a migration was successfully applied by checking schema
|
||||
*
|
||||
* This function performs post-migration validation to ensure that the
|
||||
* expected database schema changes were actually applied. It checks for
|
||||
* the existence of tables, columns, and other schema elements that should
|
||||
* have been created by the migration.
|
||||
*
|
||||
* @param migration - The migration to validate
|
||||
* @param sqlQuery - Function to execute SQL queries
|
||||
* @returns Promise resolving to validation results
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const validation = await validateMigrationApplication(migration, sqlQuery);
|
||||
* if (!validation.isValid) {
|
||||
* console.error('Migration validation failed:', validation.errors);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
/**
|
||||
* Helper function to check if a SQLite result indicates a table exists
|
||||
* @param result - The result from a sqlite_master query
|
||||
* @returns true if the table exists
|
||||
*/
|
||||
function checkSqliteTableResult(result: unknown): boolean {
|
||||
return (
|
||||
(result as unknown as { values: unknown[][] })?.values?.length > 0 ||
|
||||
(Array.isArray(result) && result.length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to validate that a table exists in the database
|
||||
* @param tableName - Name of the table to check
|
||||
* @param sqlQuery - Function to execute SQL queries
|
||||
* @returns Promise resolving to true if table exists
|
||||
*/
|
||||
async function validateTableExists<T>(
|
||||
tableName: string,
|
||||
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const result = await sqlQuery(
|
||||
`SELECT name FROM sqlite_master WHERE type='table' AND name='${tableName}'`,
|
||||
);
|
||||
return checkSqliteTableResult(result);
|
||||
} catch (error) {
|
||||
logger.error(`❌ [Validation] Error checking table ${tableName}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to validate that a column exists in a table
|
||||
* @param tableName - Name of the table
|
||||
* @param columnName - Name of the column to check
|
||||
* @param sqlQuery - Function to execute SQL queries
|
||||
* @returns Promise resolving to true if column exists
|
||||
*/
|
||||
async function validateColumnExists<T>(
|
||||
tableName: string,
|
||||
columnName: string,
|
||||
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
await sqlQuery(`SELECT ${columnName} FROM ${tableName} LIMIT 1`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`❌ [Validation] Error checking column ${columnName} in ${tableName}:`,
|
||||
error,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to validate multiple tables exist
|
||||
* @param tableNames - Array of table names to check
|
||||
* @param sqlQuery - Function to execute SQL queries
|
||||
* @returns Promise resolving to array of validation results
|
||||
*/
|
||||
async function validateMultipleTables<T>(
|
||||
tableNames: string[],
|
||||
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
|
||||
): Promise<{ exists: boolean; missing: string[] }> {
|
||||
const missing: string[] = [];
|
||||
|
||||
for (const tableName of tableNames) {
|
||||
const exists = await validateTableExists(tableName, sqlQuery);
|
||||
if (!exists) {
|
||||
missing.push(tableName);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
exists: missing.length === 0,
|
||||
missing,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to add validation error with consistent logging
|
||||
* @param validation - The validation object to update
|
||||
* @param message - Error message to add
|
||||
* @param error - The error object for logging
|
||||
*/
|
||||
function addValidationError(
|
||||
validation: MigrationValidation,
|
||||
message: string,
|
||||
error: unknown,
|
||||
): void {
|
||||
validation.isValid = false;
|
||||
validation.errors.push(message);
|
||||
logger.error(`❌ [Migration-Validation] ${message}:`, error);
|
||||
}
|
||||
|
||||
async function validateMigrationApplication<T>(
|
||||
migration: Migration,
|
||||
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
|
||||
): Promise<MigrationValidation> {
|
||||
const validation: MigrationValidation = {
|
||||
isValid: true,
|
||||
tableExists: false,
|
||||
hasExpectedColumns: false,
|
||||
errors: [],
|
||||
};
|
||||
|
||||
try {
|
||||
if (migration.name === "001_initial") {
|
||||
// Validate core tables exist for initial migration
|
||||
const tables = [
|
||||
"accounts",
|
||||
"secret",
|
||||
"settings",
|
||||
"contacts",
|
||||
"logs",
|
||||
"temp",
|
||||
];
|
||||
|
||||
const tableValidation = await validateMultipleTables(tables, sqlQuery);
|
||||
if (!tableValidation.exists) {
|
||||
validation.isValid = false;
|
||||
validation.errors.push(
|
||||
`Missing tables: ${tableValidation.missing.join(", ")}`,
|
||||
);
|
||||
logger.error(
|
||||
`❌ [Migration-Validation] Missing tables:`,
|
||||
tableValidation.missing,
|
||||
);
|
||||
}
|
||||
validation.tableExists = tableValidation.exists;
|
||||
} else if (migration.name === "002_add_iViewContent_to_contacts") {
|
||||
// Validate iViewContent column exists in contacts table
|
||||
const columnExists = await validateColumnExists(
|
||||
"contacts",
|
||||
"iViewContent",
|
||||
sqlQuery,
|
||||
);
|
||||
if (!columnExists) {
|
||||
addValidationError(
|
||||
validation,
|
||||
"Column iViewContent missing from contacts table",
|
||||
new Error("Column not found"),
|
||||
);
|
||||
} else {
|
||||
validation.hasExpectedColumns = true;
|
||||
}
|
||||
} else if (migration.name === "004_active_identity_management") {
|
||||
// Validate active_identity table exists and has correct structure
|
||||
const activeIdentityExists = await validateTableExists(
|
||||
"active_identity",
|
||||
sqlQuery,
|
||||
);
|
||||
|
||||
if (!activeIdentityExists) {
|
||||
addValidationError(
|
||||
validation,
|
||||
"Table active_identity missing",
|
||||
new Error("Table not found"),
|
||||
);
|
||||
} else {
|
||||
validation.tableExists = true;
|
||||
|
||||
// Check that active_identity has the expected structure
|
||||
const hasExpectedColumns = await validateColumnExists(
|
||||
"active_identity",
|
||||
"id, activeDid, lastUpdated",
|
||||
sqlQuery,
|
||||
);
|
||||
|
||||
if (!hasExpectedColumns) {
|
||||
addValidationError(
|
||||
validation,
|
||||
"active_identity table missing expected columns",
|
||||
new Error("Columns not found"),
|
||||
);
|
||||
} else {
|
||||
validation.hasExpectedColumns = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check that hasBackedUpSeed column exists in settings table
|
||||
// Note: This validation is included here because migration 004 is consolidated
|
||||
// and includes the functionality from the original migration 003
|
||||
const hasBackedUpSeedExists = await validateColumnExists(
|
||||
"settings",
|
||||
"hasBackedUpSeed",
|
||||
sqlQuery,
|
||||
);
|
||||
|
||||
if (!hasBackedUpSeedExists) {
|
||||
addValidationError(
|
||||
validation,
|
||||
"Column hasBackedUpSeed missing from settings table",
|
||||
new Error("Column not found"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add validation for future migrations here
|
||||
// } else if (migration.name === "003_future_migration") {
|
||||
// // Validate future migration schema changes
|
||||
// }
|
||||
} catch (error) {
|
||||
validation.isValid = false;
|
||||
validation.errors.push(`Validation error: ${error}`);
|
||||
logger.error(
|
||||
`❌ [Migration-Validation] Validation failed for ${migration.name}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
return validation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if migration is already applied by examining actual schema
|
||||
*
|
||||
* This function performs schema introspection to determine if a migration
|
||||
* has already been applied, even if it's not recorded in the migrations
|
||||
* table. This is useful for handling cases where the database schema exists
|
||||
* but the migration tracking got out of sync.
|
||||
*
|
||||
* @param migration - The migration to check
|
||||
* @param sqlQuery - Function to execute SQL queries
|
||||
* @returns Promise resolving to true if schema already exists
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const schemaExists = await isSchemaAlreadyPresent(migration, sqlQuery);
|
||||
* if (schemaExists) {
|
||||
* console.log('Schema already exists, skipping migration');
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
async function isSchemaAlreadyPresent<T>(
|
||||
migration: Migration,
|
||||
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
if (migration.name === "001_initial") {
|
||||
// Check if accounts table exists (primary indicator of initial migration)
|
||||
const result = (await sqlQuery(
|
||||
`SELECT name FROM sqlite_master WHERE type='table' AND name='accounts'`,
|
||||
)) as unknown as { values: unknown[][] };
|
||||
const hasTable =
|
||||
result?.values?.length > 0 ||
|
||||
(Array.isArray(result) && result.length > 0);
|
||||
// Reduced logging - only log on error
|
||||
return hasTable;
|
||||
} else if (migration.name === "002_add_iViewContent_to_contacts") {
|
||||
// Check if iViewContent column exists in contacts table
|
||||
try {
|
||||
await sqlQuery(`SELECT iViewContent FROM contacts LIMIT 1`);
|
||||
// Reduced logging - only log on error
|
||||
return true;
|
||||
} catch (error) {
|
||||
// Reduced logging - only log on error
|
||||
return false;
|
||||
}
|
||||
} else if (migration.name === "003_add_hasBackedUpSeed_to_settings") {
|
||||
// Check if hasBackedUpSeed column exists in settings table
|
||||
try {
|
||||
await sqlQuery(`SELECT hasBackedUpSeed FROM settings LIMIT 1`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
} else if (migration.name === "004_active_identity_management") {
|
||||
// Check if active_identity table exists and has correct structure
|
||||
try {
|
||||
// Check that active_identity table exists
|
||||
const activeIdentityResult = await sqlQuery(
|
||||
`SELECT name FROM sqlite_master WHERE type='table' AND name='active_identity'`,
|
||||
);
|
||||
const hasActiveIdentityTable =
|
||||
(activeIdentityResult as unknown as { values: unknown[][] })?.values
|
||||
?.length > 0 ||
|
||||
(Array.isArray(activeIdentityResult) &&
|
||||
activeIdentityResult.length > 0);
|
||||
|
||||
if (!hasActiveIdentityTable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check that active_identity has the expected structure
|
||||
try {
|
||||
await sqlQuery(
|
||||
`SELECT id, activeDid, lastUpdated FROM active_identity LIMIT 1`,
|
||||
);
|
||||
|
||||
// Also check that hasBackedUpSeed column exists in settings
|
||||
// This is included because migration 004 is consolidated
|
||||
try {
|
||||
await sqlQuery(`SELECT hasBackedUpSeed FROM settings LIMIT 1`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`🔍 [Migration-Schema] Schema check failed for ${migration.name}, assuming not present:`,
|
||||
error,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Add schema checks for future migrations here
|
||||
// } else if (migration.name === "003_future_migration") {
|
||||
// // Check if future migration schema already exists
|
||||
// }
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`🔍 [Migration-Schema] Schema check failed for ${migration.name}, assuming not present:`,
|
||||
error,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all registered migrations against the database
|
||||
*
|
||||
@@ -600,7 +229,7 @@ export async function runMigrations<T>(
|
||||
extractMigrationNames: (result: T) => Set<string>,
|
||||
): Promise<void> {
|
||||
try {
|
||||
logger.debug("📋 [Migration] Starting migration process...");
|
||||
logger.debug("[Migration] Starting migration process...");
|
||||
|
||||
// Create migrations table if it doesn't exist
|
||||
// Note: We use IF NOT EXISTS here because this is infrastructure, not a business migration
|
||||
@@ -628,7 +257,7 @@ export async function runMigrations<T>(
|
||||
|
||||
// Only log migration counts in development
|
||||
logger.debug(
|
||||
`📊 [Migration] Found ${migrations.length} total migrations, ${appliedMigrations.size} already applied`,
|
||||
`[Migration] Found ${migrations.length} total migrations, ${appliedMigrations.size} already applied`,
|
||||
);
|
||||
|
||||
let appliedCount = 0;
|
||||
@@ -645,70 +274,35 @@ export async function runMigrations<T>(
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check 2: Does the schema already exist in the database?
|
||||
const isSchemaPresent = await isSchemaAlreadyPresent(migration, sqlQuery);
|
||||
|
||||
// Handle case where schema exists but isn't recorded
|
||||
if (isSchemaPresent) {
|
||||
try {
|
||||
await sqlExec("INSERT INTO migrations (name) VALUES (?)", [
|
||||
migration.name,
|
||||
]);
|
||||
logger.debug(
|
||||
`✅ [Migration] Marked existing schema as applied: ${migration.name}`,
|
||||
);
|
||||
skippedCount++;
|
||||
continue;
|
||||
} catch (insertError) {
|
||||
logger.warn(
|
||||
`⚠️ [Migration] Could not record existing schema ${migration.name}:`,
|
||||
insertError,
|
||||
);
|
||||
// Continue with normal migration process as fallback
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the migration
|
||||
logger.debug(`🔄 [Migration] Applying migration: ${migration.name}`);
|
||||
logger.debug(`[Migration] Applying migration: ${migration.name}`);
|
||||
|
||||
try {
|
||||
// Execute the migration SQL as single atomic operation
|
||||
logger.debug(`🔧 [Migration] Executing SQL for: ${migration.name}`);
|
||||
logger.debug(`🔧 [Migration] SQL content: ${migration.sql}`);
|
||||
logger.debug(`[Migration] Executing SQL for: ${migration.name}`);
|
||||
logger.debug(`[Migration] SQL content: ${migration.sql}`);
|
||||
|
||||
// Execute the migration SQL directly - it should be atomic
|
||||
// The SQL itself should handle any necessary transactions
|
||||
const execResult = await sqlExec(migration.sql);
|
||||
|
||||
logger.debug(
|
||||
`🔧 [Migration] SQL execution result: ${JSON.stringify(execResult)}`,
|
||||
`[Migration] SQL execution result: ${JSON.stringify(execResult)}`,
|
||||
);
|
||||
|
||||
// Validate the migration was applied correctly
|
||||
const validation = await validateMigrationApplication(
|
||||
migration,
|
||||
sqlQuery,
|
||||
);
|
||||
if (!validation.isValid) {
|
||||
logger.warn(
|
||||
`⚠️ [Migration] Validation failed for ${migration.name}:`,
|
||||
validation.errors,
|
||||
);
|
||||
}
|
||||
|
||||
// Record that the migration was applied
|
||||
await sqlExec("INSERT INTO migrations (name) VALUES (?)", [
|
||||
migration.name,
|
||||
]);
|
||||
|
||||
logger.debug(`🎉 [Migration] Successfully applied: ${migration.name}`);
|
||||
logger.debug(`✅ [Migration] Successfully applied: ${migration.name}`);
|
||||
appliedCount++;
|
||||
} catch (error) {
|
||||
logger.error(`❌ [Migration] Error applying ${migration.name}:`, error);
|
||||
|
||||
// Provide explicit rollback instructions for migration failures
|
||||
logger.error(
|
||||
`🔄 [Migration] ROLLBACK INSTRUCTIONS for ${migration.name}:`,
|
||||
`[Migration] ROLLBACK INSTRUCTIONS for ${migration.name}:`,
|
||||
);
|
||||
logger.error(` 1. Stop the application immediately`);
|
||||
logger.error(
|
||||
@@ -740,41 +334,12 @@ export async function runMigrations<T>(
|
||||
errorMessage.includes("already exists"))
|
||||
) {
|
||||
logger.debug(
|
||||
`⚠️ [Migration] ${migration.name} appears already applied (${errorMessage}). Validating and marking as complete.`,
|
||||
`⚠️ [Migration] ${migration.name} appears already applied (${errorMessage}).`,
|
||||
);
|
||||
|
||||
// Validate the existing schema
|
||||
const validation = await validateMigrationApplication(
|
||||
migration,
|
||||
sqlQuery,
|
||||
);
|
||||
if (!validation.isValid) {
|
||||
logger.warn(
|
||||
`⚠️ [Migration] Schema validation failed for ${migration.name}:`,
|
||||
validation.errors,
|
||||
);
|
||||
// Don't mark as applied if validation fails
|
||||
continue;
|
||||
}
|
||||
|
||||
// Mark the migration as applied since the schema change already exists
|
||||
try {
|
||||
await sqlExec("INSERT INTO migrations (name) VALUES (?)", [
|
||||
migration.name,
|
||||
]);
|
||||
logger.debug(`✅ [Migration] Marked as applied: ${migration.name}`);
|
||||
appliedCount++;
|
||||
} catch (insertError) {
|
||||
// If we can't insert the migration record, log it but don't fail
|
||||
logger.warn(
|
||||
`⚠️ [Migration] Could not record ${migration.name} as applied:`,
|
||||
insertError,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// For other types of errors, still fail the migration
|
||||
logger.error(
|
||||
`❌ [Migration] Failed to apply ${migration.name}:`,
|
||||
`❌ [Migration] Failed to apply ${migration.name}:`,
|
||||
error,
|
||||
);
|
||||
throw new Error(`Migration ${migration.name} failed: ${error}`);
|
||||
@@ -800,11 +365,10 @@ export async function runMigrations<T>(
|
||||
|
||||
// Only show completion message in development
|
||||
logger.log(
|
||||
`🎉 [Migration] Migration process complete! Summary: ${appliedCount} applied, ${skippedCount} skipped`,
|
||||
`[Migration] Migration process complete. Summary: ${appliedCount} applied, ${skippedCount} skipped`,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error("\n💥 [Migration] Migration process failed:", error);
|
||||
logger.error("[MigrationService] Migration process failed:", error);
|
||||
logger.error("❌ [Migration] Migration process failed:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -714,7 +714,7 @@ export class CapacitorPlatformService
|
||||
*
|
||||
* For critical tables like `contacts`, the method validates:
|
||||
* - Table structure using `PRAGMA table_info`
|
||||
* - Presence of important columns (e.g., `iViewContent`)
|
||||
* - Presence of important columns (e.g., `hideTheirContent`)
|
||||
* - Column data types and constraints
|
||||
*
|
||||
* ## Error Handling:
|
||||
@@ -784,7 +784,7 @@ export class CapacitorPlatformService
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Check contacts table schema (including iViewContent column)
|
||||
// Step 3: Check contacts table schema (including hideTheirContent column)
|
||||
if (existingTables.includes("contacts")) {
|
||||
try {
|
||||
const contactsSchema = await this.db.query(
|
||||
@@ -795,23 +795,23 @@ export class CapacitorPlatformService
|
||||
contactsSchema,
|
||||
);
|
||||
|
||||
// Check for iViewContent column specifically
|
||||
const hasIViewContent = contactsSchema.values?.some(
|
||||
// Check for hideTheirContent column specifically
|
||||
const hasIHideContent = contactsSchema.values?.some(
|
||||
(col: unknown) =>
|
||||
(typeof col === "object" &&
|
||||
col !== null &&
|
||||
"name" in col &&
|
||||
(col as { name: string }).name === "iViewContent") ||
|
||||
(Array.isArray(col) && col[1] === "iViewContent"),
|
||||
(col as { name: string }).name === "hideTheirContent") ||
|
||||
(Array.isArray(col) && col[1] === "hideTheirContent"),
|
||||
);
|
||||
|
||||
if (hasIViewContent) {
|
||||
if (hasIHideContent) {
|
||||
logger.debug(
|
||||
`✅ [DB-Integrity] iViewContent column exists in contacts table`,
|
||||
`✅ [DB-Integrity] hideTheirContent column exists in contacts table`,
|
||||
);
|
||||
} else {
|
||||
logger.error(
|
||||
`❌ [DB-Integrity] iViewContent column missing from contacts table`,
|
||||
`❌ [DB-Integrity] hideTheirContent column missing from contacts table`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -296,7 +296,7 @@ export const PlatformServiceMixin = {
|
||||
column === "warnIfProdServer" ||
|
||||
column === "warnIfTestServer" ||
|
||||
// contacts
|
||||
column === "iViewContent" ||
|
||||
column === "hideTheirContent" ||
|
||||
column === "registered" ||
|
||||
column === "seesMe"
|
||||
) {
|
||||
@@ -911,7 +911,7 @@ export const PlatformServiceMixin = {
|
||||
// Create a new contact object with proper typing
|
||||
const normalizedContact: Contact = {
|
||||
did: contact.did,
|
||||
iViewContent: contact.iViewContent,
|
||||
hideTheirContent: contact.hideTheirContent,
|
||||
name: contact.name,
|
||||
nextPubKeyHashB64: contact.nextPubKeyHashB64,
|
||||
notes: contact.notes,
|
||||
@@ -1380,8 +1380,10 @@ export const PlatformServiceMixin = {
|
||||
? contact.profileImageUrl
|
||||
: null,
|
||||
notes: contact.notes !== undefined ? contact.notes : null,
|
||||
iViewContent:
|
||||
contact.iViewContent !== undefined ? contact.iViewContent : null,
|
||||
hideTheirContent:
|
||||
contact.hideTheirContent !== undefined
|
||||
? contact.hideTheirContent
|
||||
: null,
|
||||
contactMethods:
|
||||
contact.contactMethods !== undefined
|
||||
? Array.isArray(contact.contactMethods)
|
||||
@@ -1392,7 +1394,7 @@ export const PlatformServiceMixin = {
|
||||
|
||||
await this.$dbExec(
|
||||
`INSERT OR REPLACE INTO contacts
|
||||
(did, name, publicKeyBase64, seesMe, registered, nextPubKeyHashB64, profileImageUrl, notes, iViewContent, contactMethods)
|
||||
(did, name, publicKeyBase64, seesMe, registered, nextPubKeyHashB64, profileImageUrl, notes, hideTheirContent, contactMethods)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
safeContact.did,
|
||||
@@ -1403,7 +1405,7 @@ export const PlatformServiceMixin = {
|
||||
safeContact.nextPubKeyHashB64,
|
||||
safeContact.profileImageUrl,
|
||||
safeContact.notes,
|
||||
safeContact.iViewContent,
|
||||
safeContact.hideTheirContent,
|
||||
safeContact.contactMethods,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -204,7 +204,7 @@
|
||||
class="flex flex-col items-center"
|
||||
>
|
||||
<button
|
||||
v-if="contactFromDid?.iViewContent"
|
||||
v-if="!contactFromDid?.hideTheirContent"
|
||||
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||
title="You watch their activity"
|
||||
@click="confirmViewContent(contactFromDid, false)"
|
||||
@@ -213,7 +213,7 @@
|
||||
<font-awesome icon="eye" class="fa-fw" />
|
||||
</button>
|
||||
<button
|
||||
v-else-if="!contactFromDid?.iViewContent"
|
||||
v-else-if="contactFromDid?.hideTheirContent"
|
||||
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||
title="You do not watch their activity"
|
||||
@click="confirmViewContent(contactFromDid, true)"
|
||||
@@ -1141,7 +1141,7 @@ export default class DIDView extends Vue {
|
||||
this.notify.confirm(contentVisibilityPrompt, async () => {
|
||||
const success = await this.setViewContent(contact, view);
|
||||
if (success) {
|
||||
contact.iViewContent = view; // see visibility note about not working inside setVisibility
|
||||
contact.hideTheirContent = !view; // inverted: view=true -> hideTheirContent=false
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1154,7 +1154,7 @@ export default class DIDView extends Vue {
|
||||
* @returns Boolean indicating success
|
||||
*/
|
||||
async setViewContent(contact: Contact, visibility: boolean) {
|
||||
await this.$updateContact(contact.did, { iViewContent: visibility });
|
||||
await this.$updateContact(contact.did, { hideTheirContent: !visibility });
|
||||
const message =
|
||||
"You will" +
|
||||
(visibility ? "" : " not") +
|
||||
|
||||
@@ -773,7 +773,7 @@ export default class HomeView extends Vue {
|
||||
private async loadContacts() {
|
||||
this.allContacts = await this.$contacts();
|
||||
this.blockedContactDids = this.allContacts
|
||||
.filter((c) => !c.iViewContent)
|
||||
.filter((c) => c.hideTheirContent)
|
||||
.map((c) => c.did);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user