Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 128 additions & 18 deletions apps/core/server/db/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,24 @@ if (!process.env.DATABASE_URL) {
}

// Log migration info for debugging
const sanitizedDbUrl = (() => {
try {
const url = new URL(process.env.DATABASE_URL!);
url.password = "****";
return url.toString();
} catch {
return process.env.DATABASE_URL!.replace(/:[^:@]+@/, ":****@");
}
})(); // Hide password

console.log("[Migrations] Starting migration process");
console.log("[Migrations] Migrations folder:", migrationsFolder);
console.log("[Migrations] Database URL:", sanitizedDbUrl);
Comment on lines +43 to +45
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The script uses both console.log and the structured logger.info for startup messages. While using console.log can be useful for immediate visibility in some deployment environments, this mix creates inconsistency. For better log management, it would be ideal to rely solely on the structured logger (pino). Pino can be configured with a synchronous transport for development or debugging scenarios if immediate output is a concern. If console.log is a hard requirement for your deployment platform, consider consolidating this logic so you don't have to call both console.log and logger.info for the same event.


logger.info(
{
migrationsFolder,
databaseUrl: (() => {
try {
const url = new URL(process.env.DATABASE_URL!);
url.password = "****";
return url.toString();
} catch {
return process.env.DATABASE_URL!.replace(/:[^:@]+@/, ":****@");
}
})(), // Hide password
databaseUrl: sanitizedDbUrl,
},
"[Migrations] Starting migration process",
);
Expand All @@ -55,11 +61,102 @@ const db = drizzle(migrationClient);

// Run migrations
async function main() {
// Verify migrations folder exists and has files
try {
const fs = await import("node:fs/promises");
const migrationFiles = await fs.readdir(migrationsFolder);
const sqlFiles = migrationFiles.filter((f) => f.endsWith(".sql"));

logger.info(
{
migrationsFolder,
totalFiles: migrationFiles.length,
sqlFiles: sqlFiles.length,
},
"[Migrations] Found migration files",
);

if (sqlFiles.length === 0) {
const errorMsg = `No SQL migration files found in ${migrationsFolder}`;
console.error(`[Migrations] ERROR: ${errorMsg}`);
logger.error({}, errorMsg);
process.exit(1);
}
} catch (error: any) {
const errorMsg = `Failed to read migrations folder: ${error.message}`;
console.error(`[Migrations] ERROR: ${errorMsg}`);
console.error(`[Migrations] Stack: ${error.stack}`);
logger.error({ err: error }, errorMsg);
process.exit(1);
Comment on lines +83 to +90

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resource Management on Early Exit:

When aborting the process due to missing migration files or a failed folder read, the code calls process.exit(1) without first closing the database connection (migrationClient.end()). This can lead to unclosed connections or resource leaks, especially in environments where process exit is not immediate or graceful.

Recommended Solution:
Before calling process.exit(1), ensure that await migrationClient.end() is called to properly close the database connection:

await migrationClient.end();
process.exit(1);

}
Comment on lines +85 to +91
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

It's a TypeScript best practice to type catch clause variables as unknown instead of any. This enforces that you safely check the error's type before trying to access its properties, preventing potential runtime errors.

Suggested change
} catch (error: any) {
const errorMsg = `Failed to read migrations folder: ${error.message}`;
console.error(`[Migrations] ERROR: ${errorMsg}`);
console.error(`[Migrations] Stack: ${error.stack}`);
logger.error({ err: error }, errorMsg);
process.exit(1);
}
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
const errorMsg = `Failed to read migrations folder: ${errorMessage}`;
console.error(`[Migrations] ERROR: ${errorMsg}`);
if (error instanceof Error && error.stack) {
console.error(`[Migrations] Stack: ${error.stack}`);
}
logger.error({ err: error }, errorMsg);
process.exit(1);
}


// Test database connection before running migrations
console.log("[Migrations] Testing database connection...");
try {
const result = await migrationClient`SELECT 1 as test`;
console.log("[Migrations] ✓ Database connection successful");
logger.info({}, "[Migrations] Database connection test passed");
} catch (error: any) {
const errorMsg = `Database connection failed: ${error.message}`;
console.error("[Migrations] ✗ DATABASE CONNECTION FAILED");
console.error("Error Code:", error?.code);
console.error("Error Message:", error?.message);
console.error("Database URL:", sanitizedDbUrl);
console.error("Full Error:", JSON.stringify(error, null, 2));

logger.error(
{
err: error,
code: error?.code,
databaseUrl: sanitizedDbUrl,
},
errorMsg,
);
await migrationClient.end();
process.exit(1);
}
Comment on lines +99 to +117
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There's a recurring pattern of logging detailed error information to both console.error and logger.error before exiting the process. This is repeated in multiple error handling blocks (e.g., lines 85-90, 99-116, and 175-195). To improve maintainability and reduce code duplication, consider creating a centralized error handling function.

This function could take the error, a message, and any additional context, handle the logging to both console and the structured logger, and then exit the process. For example:

function exitWithError(message: string, error: unknown, context: Record<string, any> = {}) {
  const errorDetails = {
    message: error instanceof Error ? error.message : String(error),
    stack: error instanceof Error ? error.stack : undefined,
    ...context,
  };

  console.error(`[Migrations] ✗ ERROR: ${message}`);
  console.error("Details:", JSON.stringify(errorDetails, null, 2));

  logger.error({ err: error, ...context }, `[Migrations] ✗ ${message}`);
  
  // Consider ending the client connection here if it's always needed before exit
  // await migrationClient.end();

  process.exit(1);
}

Using this would simplify the catch blocks significantly.


// Check for existing migrations (helps diagnose state)
try {
const existingMigrations = await migrationClient`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'drizzle'
AND table_name = '__drizzle_migrations'
`;

if (existingMigrations.length > 0) {
const appliedMigrations = await migrationClient`
SELECT id, hash, created_at
FROM drizzle.__drizzle_migrations
ORDER BY created_at DESC
LIMIT 5
`;
console.log(
`[Migrations] Found ${appliedMigrations.length} previously applied migrations`,
);
logger.info(
{ count: appliedMigrations.length },
"[Migrations] Previous migrations detected",
);
} else {
console.log("[Migrations] No previous migrations found (fresh database)");
logger.info({}, "[Migrations] Fresh database detected");
}
} catch (error: any) {
// Ignore errors here - schema might not exist yet
console.log(
"[Migrations] Could not check migration history (expected on first run)",
);
}
Comment on lines +146 to +151

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error Handling for Migration History Check:

The catch block for the migration history check ignores all errors, assuming they are expected on first run. This could mask other, more serious issues (e.g., permission errors, network issues) that should be logged for visibility and troubleshooting.

Recommended Solution:
Log unexpected errors at a warning level to aid in debugging:

} catch (error: any) {
  logger.warn({ err: error }, "Could not check migration history (expected on first run)");
  console.log("[Migrations] Could not check migration history (expected on first run)");
}


console.log("[Migrations] Running migrations...");
logger.info({}, "[Migrations] Running migrations...");

try {
await migrate(db, { migrationsFolder });
logger.info({}, "[Migrations] ✓ Migrations completed successfully");
console.log("[Migrations] ✓ Migrations completed successfully"); // Ensure visibility in Railway
} catch (error: any) {
// Check if it's a "relation already exists" error (PostgreSQL code 42P07)
// Drizzle wraps PostgreSQL errors, so check the cause as well
Expand All @@ -72,16 +169,29 @@ async function main() {
if (isAlreadyExistsError) {
logger.warn({}, "[Migrations] ⚠️ Some tables already exist - skipping");
logger.info({}, "[Migrations] ✓ Database schema is up to date");
} else {
logger.error(
{
err: error,
code: errorCode,
message: errorMessage,
migrationsFolder,
},
"[Migrations] ✗ Migration failed",
console.log(
"[Migrations] ✓ Database schema is up to date (some tables already exist)",
);
} else {
// Log error details to both logger and console for maximum visibility
const errorDetails = {
code: errorCode,
message: errorMessage,
stack: error?.stack,
cause: error?.cause,
migrationsFolder,
};

console.error("[Migrations] ✗ MIGRATION FAILED - ERROR DETAILS:");
console.error("Error Code:", errorCode);
console.error("Error Message:", errorMessage);
console.error("Full Error:", JSON.stringify(error, null, 2));
console.error("Error Stack:", error?.stack);
if (error?.cause) {
console.error("Error Cause:", JSON.stringify(error.cause, null, 2));
}

logger.error(errorDetails, "[Migrations] ✗ Migration failed");
process.exit(1);
}
}
Expand Down
Loading
Loading