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",
|
name: "002_add_iViewContent_to_contacts",
|
||||||
|
// Note that many times iViewContent was set to null despite the DEFAULT setting.
|
||||||
sql: `
|
sql: `
|
||||||
ALTER TABLE contacts ADD COLUMN iViewContent BOOLEAN DEFAULT TRUE;
|
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);
|
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;
|
did: string;
|
||||||
contactMethods?: Array<ContactMethod>;
|
contactMethods?: Array<ContactMethod>;
|
||||||
iViewContent?: boolean;
|
/** When true, hide this contact's activity from the feed. Default (null/undefined) = show. */
|
||||||
|
hideTheirContent?: boolean;
|
||||||
name?: string;
|
name?: string;
|
||||||
nextPubKeyHashB64?: string; // base64-encoded SHA256 hash of next public key
|
nextPubKeyHashB64?: string; // base64-encoded SHA256 hash of next public key
|
||||||
notes?: string;
|
notes?: string;
|
||||||
|
|||||||
@@ -12,9 +12,7 @@
|
|||||||
*
|
*
|
||||||
* 1. **Single Application**: Each migration runs exactly once per database
|
* 1. **Single Application**: Each migration runs exactly once per database
|
||||||
* 2. **Tracked Execution**: All applied migrations are recorded in a migrations table
|
* 2. **Tracked Execution**: All applied migrations are recorded in a migrations table
|
||||||
* 3. **Schema Validation**: Actual database schema is validated before and after migrations
|
* 3. **Comprehensive Logging**: Detailed logging for debugging and monitoring
|
||||||
* 4. **Graceful Recovery**: Handles cases where schema exists but tracking is missing
|
|
||||||
* 5. **Comprehensive Logging**: Detailed logging for debugging and monitoring
|
|
||||||
*
|
*
|
||||||
* ## Migration Flow
|
* ## Migration Flow
|
||||||
*
|
*
|
||||||
@@ -26,9 +24,7 @@
|
|||||||
* b. Check if schema already exists
|
* b. Check if schema already exists
|
||||||
* c. Skip if already applied
|
* c. Skip if already applied
|
||||||
* d. Apply migration SQL
|
* d. Apply migration SQL
|
||||||
* e. Validate schema was created
|
* e. Record migration as applied
|
||||||
* f. Record migration as applied
|
|
||||||
* 4. Final validation of all migrations
|
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* ## Usage Example
|
* ## Usage Example
|
||||||
@@ -77,25 +73,6 @@ interface Migration {
|
|||||||
statements?: string[];
|
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
|
* Migration registry to store and manage database migrations
|
||||||
*
|
*
|
||||||
@@ -207,354 +184,6 @@ export function registerMigration(migration: Migration): void {
|
|||||||
migrationRegistry.registerMigration(migration);
|
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
|
* Run all registered migrations against the database
|
||||||
*
|
*
|
||||||
@@ -600,7 +229,7 @@ export async function runMigrations<T>(
|
|||||||
extractMigrationNames: (result: T) => Set<string>,
|
extractMigrationNames: (result: T) => Set<string>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
logger.debug("📋 [Migration] Starting migration process...");
|
logger.debug("[Migration] Starting migration process...");
|
||||||
|
|
||||||
// Create migrations table if it doesn't exist
|
// Create migrations table if it doesn't exist
|
||||||
// Note: We use IF NOT EXISTS here because this is infrastructure, not a business migration
|
// 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
|
// Only log migration counts in development
|
||||||
logger.debug(
|
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;
|
let appliedCount = 0;
|
||||||
@@ -645,70 +274,35 @@ export async function runMigrations<T>(
|
|||||||
continue;
|
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
|
// Apply the migration
|
||||||
logger.debug(`🔄 [Migration] Applying migration: ${migration.name}`);
|
logger.debug(`[Migration] Applying migration: ${migration.name}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Execute the migration SQL as single atomic operation
|
// Execute the migration SQL as single atomic operation
|
||||||
logger.debug(`🔧 [Migration] Executing SQL for: ${migration.name}`);
|
logger.debug(`[Migration] Executing SQL for: ${migration.name}`);
|
||||||
logger.debug(`🔧 [Migration] SQL content: ${migration.sql}`);
|
logger.debug(`[Migration] SQL content: ${migration.sql}`);
|
||||||
|
|
||||||
// Execute the migration SQL directly - it should be atomic
|
// Execute the migration SQL directly - it should be atomic
|
||||||
// The SQL itself should handle any necessary transactions
|
// The SQL itself should handle any necessary transactions
|
||||||
const execResult = await sqlExec(migration.sql);
|
const execResult = await sqlExec(migration.sql);
|
||||||
|
|
||||||
logger.debug(
|
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
|
// Record that the migration was applied
|
||||||
await sqlExec("INSERT INTO migrations (name) VALUES (?)", [
|
await sqlExec("INSERT INTO migrations (name) VALUES (?)", [
|
||||||
migration.name,
|
migration.name,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
logger.debug(`🎉 [Migration] Successfully applied: ${migration.name}`);
|
logger.debug(`✅ [Migration] Successfully applied: ${migration.name}`);
|
||||||
appliedCount++;
|
appliedCount++;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`❌ [Migration] Error applying ${migration.name}:`, error);
|
logger.error(`❌ [Migration] Error applying ${migration.name}:`, error);
|
||||||
|
|
||||||
// Provide explicit rollback instructions for migration failures
|
// Provide explicit rollback instructions for migration failures
|
||||||
logger.error(
|
logger.error(
|
||||||
`🔄 [Migration] ROLLBACK INSTRUCTIONS for ${migration.name}:`,
|
`[Migration] ROLLBACK INSTRUCTIONS for ${migration.name}:`,
|
||||||
);
|
);
|
||||||
logger.error(` 1. Stop the application immediately`);
|
logger.error(` 1. Stop the application immediately`);
|
||||||
logger.error(
|
logger.error(
|
||||||
@@ -740,41 +334,12 @@ export async function runMigrations<T>(
|
|||||||
errorMessage.includes("already exists"))
|
errorMessage.includes("already exists"))
|
||||||
) {
|
) {
|
||||||
logger.debug(
|
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 {
|
} else {
|
||||||
// For other types of errors, still fail the migration
|
// For other types of errors, still fail the migration
|
||||||
logger.error(
|
logger.error(
|
||||||
`❌ [Migration] Failed to apply ${migration.name}:`,
|
`❌ [Migration] Failed to apply ${migration.name}:`,
|
||||||
error,
|
error,
|
||||||
);
|
);
|
||||||
throw new Error(`Migration ${migration.name} failed: ${error}`);
|
throw new Error(`Migration ${migration.name} failed: ${error}`);
|
||||||
@@ -800,11 +365,10 @@ export async function runMigrations<T>(
|
|||||||
|
|
||||||
// Only show completion message in development
|
// Only show completion message in development
|
||||||
logger.log(
|
logger.log(
|
||||||
`🎉 [Migration] Migration process complete! Summary: ${appliedCount} applied, ${skippedCount} skipped`,
|
`[Migration] Migration process complete. Summary: ${appliedCount} applied, ${skippedCount} skipped`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("\n💥 [Migration] Migration process failed:", error);
|
logger.error("❌ [Migration] Migration process failed:", error);
|
||||||
logger.error("[MigrationService] Migration process failed:", error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -714,7 +714,7 @@ export class CapacitorPlatformService
|
|||||||
*
|
*
|
||||||
* For critical tables like `contacts`, the method validates:
|
* For critical tables like `contacts`, the method validates:
|
||||||
* - Table structure using `PRAGMA table_info`
|
* - 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
|
* - Column data types and constraints
|
||||||
*
|
*
|
||||||
* ## Error Handling:
|
* ## 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")) {
|
if (existingTables.includes("contacts")) {
|
||||||
try {
|
try {
|
||||||
const contactsSchema = await this.db.query(
|
const contactsSchema = await this.db.query(
|
||||||
@@ -795,23 +795,23 @@ export class CapacitorPlatformService
|
|||||||
contactsSchema,
|
contactsSchema,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check for iViewContent column specifically
|
// Check for hideTheirContent column specifically
|
||||||
const hasIViewContent = contactsSchema.values?.some(
|
const hasIHideContent = contactsSchema.values?.some(
|
||||||
(col: unknown) =>
|
(col: unknown) =>
|
||||||
(typeof col === "object" &&
|
(typeof col === "object" &&
|
||||||
col !== null &&
|
col !== null &&
|
||||||
"name" in col &&
|
"name" in col &&
|
||||||
(col as { name: string }).name === "iViewContent") ||
|
(col as { name: string }).name === "hideTheirContent") ||
|
||||||
(Array.isArray(col) && col[1] === "iViewContent"),
|
(Array.isArray(col) && col[1] === "hideTheirContent"),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (hasIViewContent) {
|
if (hasIHideContent) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`✅ [DB-Integrity] iViewContent column exists in contacts table`,
|
`✅ [DB-Integrity] hideTheirContent column exists in contacts table`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
logger.error(
|
logger.error(
|
||||||
`❌ [DB-Integrity] iViewContent column missing from contacts table`,
|
`❌ [DB-Integrity] hideTheirContent column missing from contacts table`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -296,7 +296,7 @@ export const PlatformServiceMixin = {
|
|||||||
column === "warnIfProdServer" ||
|
column === "warnIfProdServer" ||
|
||||||
column === "warnIfTestServer" ||
|
column === "warnIfTestServer" ||
|
||||||
// contacts
|
// contacts
|
||||||
column === "iViewContent" ||
|
column === "hideTheirContent" ||
|
||||||
column === "registered" ||
|
column === "registered" ||
|
||||||
column === "seesMe"
|
column === "seesMe"
|
||||||
) {
|
) {
|
||||||
@@ -911,7 +911,7 @@ export const PlatformServiceMixin = {
|
|||||||
// Create a new contact object with proper typing
|
// Create a new contact object with proper typing
|
||||||
const normalizedContact: Contact = {
|
const normalizedContact: Contact = {
|
||||||
did: contact.did,
|
did: contact.did,
|
||||||
iViewContent: contact.iViewContent,
|
hideTheirContent: contact.hideTheirContent,
|
||||||
name: contact.name,
|
name: contact.name,
|
||||||
nextPubKeyHashB64: contact.nextPubKeyHashB64,
|
nextPubKeyHashB64: contact.nextPubKeyHashB64,
|
||||||
notes: contact.notes,
|
notes: contact.notes,
|
||||||
@@ -1380,8 +1380,10 @@ export const PlatformServiceMixin = {
|
|||||||
? contact.profileImageUrl
|
? contact.profileImageUrl
|
||||||
: null,
|
: null,
|
||||||
notes: contact.notes !== undefined ? contact.notes : null,
|
notes: contact.notes !== undefined ? contact.notes : null,
|
||||||
iViewContent:
|
hideTheirContent:
|
||||||
contact.iViewContent !== undefined ? contact.iViewContent : null,
|
contact.hideTheirContent !== undefined
|
||||||
|
? contact.hideTheirContent
|
||||||
|
: null,
|
||||||
contactMethods:
|
contactMethods:
|
||||||
contact.contactMethods !== undefined
|
contact.contactMethods !== undefined
|
||||||
? Array.isArray(contact.contactMethods)
|
? Array.isArray(contact.contactMethods)
|
||||||
@@ -1392,7 +1394,7 @@ export const PlatformServiceMixin = {
|
|||||||
|
|
||||||
await this.$dbExec(
|
await this.$dbExec(
|
||||||
`INSERT OR REPLACE INTO contacts
|
`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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[
|
[
|
||||||
safeContact.did,
|
safeContact.did,
|
||||||
@@ -1403,7 +1405,7 @@ export const PlatformServiceMixin = {
|
|||||||
safeContact.nextPubKeyHashB64,
|
safeContact.nextPubKeyHashB64,
|
||||||
safeContact.profileImageUrl,
|
safeContact.profileImageUrl,
|
||||||
safeContact.notes,
|
safeContact.notes,
|
||||||
safeContact.iViewContent,
|
safeContact.hideTheirContent,
|
||||||
safeContact.contactMethods,
|
safeContact.contactMethods,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -204,7 +204,7 @@
|
|||||||
class="flex flex-col items-center"
|
class="flex flex-col items-center"
|
||||||
>
|
>
|
||||||
<button
|
<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"
|
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"
|
title="You watch their activity"
|
||||||
@click="confirmViewContent(contactFromDid, false)"
|
@click="confirmViewContent(contactFromDid, false)"
|
||||||
@@ -213,7 +213,7 @@
|
|||||||
<font-awesome icon="eye" class="fa-fw" />
|
<font-awesome icon="eye" class="fa-fw" />
|
||||||
</button>
|
</button>
|
||||||
<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"
|
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"
|
title="You do not watch their activity"
|
||||||
@click="confirmViewContent(contactFromDid, true)"
|
@click="confirmViewContent(contactFromDid, true)"
|
||||||
@@ -1141,7 +1141,7 @@ export default class DIDView extends Vue {
|
|||||||
this.notify.confirm(contentVisibilityPrompt, async () => {
|
this.notify.confirm(contentVisibilityPrompt, async () => {
|
||||||
const success = await this.setViewContent(contact, view);
|
const success = await this.setViewContent(contact, view);
|
||||||
if (success) {
|
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
|
* @returns Boolean indicating success
|
||||||
*/
|
*/
|
||||||
async setViewContent(contact: Contact, visibility: boolean) {
|
async setViewContent(contact: Contact, visibility: boolean) {
|
||||||
await this.$updateContact(contact.did, { iViewContent: visibility });
|
await this.$updateContact(contact.did, { hideTheirContent: !visibility });
|
||||||
const message =
|
const message =
|
||||||
"You will" +
|
"You will" +
|
||||||
(visibility ? "" : " not") +
|
(visibility ? "" : " not") +
|
||||||
|
|||||||
@@ -773,7 +773,7 @@ export default class HomeView extends Vue {
|
|||||||
private async loadContacts() {
|
private async loadContacts() {
|
||||||
this.allContacts = await this.$contacts();
|
this.allContacts = await this.$contacts();
|
||||||
this.blockedContactDids = this.allContacts
|
this.blockedContactDids = this.allContacts
|
||||||
.filter((c) => !c.iViewContent)
|
.filter((c) => c.hideTheirContent)
|
||||||
.map((c) => c.did);
|
.map((c) => c.did);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user