diff --git a/scripts/check-db.js b/scripts/check-db.js index 7c7daaa1..cd698b26 100644 --- a/scripts/check-db.js +++ b/scripts/check-db.js @@ -3,84 +3,199 @@ import 'dotenv/config'; import { execSync } from 'node:child_process'; import chalk from 'chalk'; import semver from 'semver'; +// eslint-disable-next-line import/no-unresolved import { PrismaClient } from '../generated/prisma/client.js'; import { PrismaPg } from '@prisma/adapter-pg'; const MIN_VERSION = '9.4.0'; if (process.env.SKIP_DB_CHECK) { - console.log('Skipping database check.'); + console.log(chalk.yellow('āš ļø Skipping database check (SKIP_DB_CHECK is set).\n')); process.exit(0); } -const url = new URL(process.env.DATABASE_URL); - -const adapter = new PrismaPg( - { connectionString: url.toString() }, - { schema: url.searchParams.get('schema') }, -); - -const prisma = new PrismaClient({ adapter }); +console.log(chalk.bold.cyan('\nšŸ” Checking Database Configuration...\n')); function success(msg) { console.log(chalk.greenBright(`āœ“ ${msg}`)); } -function error(msg) { +function error(msg, solution = null, documentation = null) { console.log(chalk.redBright(`āœ— ${msg}`)); + if (solution) { + console.log(chalk.yellow(`\nšŸ’” Solution:\n${solution}`)); + } + if (documentation) { + console.log(chalk.blue(`\nšŸ“– Documentation: ${documentation}`)); + } } async function checkEnv() { if (!process.env.DATABASE_URL) { + const solution = ` 1. Create a .env file in the project root + 2. Add DATABASE_URL with your PostgreSQL connection string + 3. Format: postgresql://username:password@host:port/database + 4. Example: postgresql://umami:mypassword@localhost:5432/umami`; + + error('DATABASE_URL is not defined.', solution, 'See .env.example for template'); throw new Error('DATABASE_URL is not defined.'); - } else { - success('DATABASE_URL is defined.'); + } + + // Validate URL format before proceeding + try { + const url = new URL(process.env.DATABASE_URL); + if (url.protocol !== 'postgresql:') { + const solution = ` DATABASE_URL must use PostgreSQL protocol + Format: postgresql://username:password@host:port/database + Example: postgresql://umami:mypassword@localhost:5432/umami`; + + error('DATABASE_URL must be a PostgreSQL connection string.', solution); + throw new Error('Invalid DATABASE_URL protocol'); + } + success('DATABASE_URL is defined and format is valid.'); + } catch (e) { + if (e.message === 'Invalid DATABASE_URL protocol') { + throw e; + } + const solution = ` DATABASE_URL format is invalid + Format: postgresql://username:password@host:port/database + Example: postgresql://umami:mypassword@localhost:5432/umami`; + + error('DATABASE_URL format is invalid.', solution); + throw new Error('Invalid DATABASE_URL format'); } } async function checkConnection() { + const url = new URL(process.env.DATABASE_URL); + + const adapter = new PrismaPg( + { connectionString: url.toString() }, + { schema: url.searchParams.get('schema') }, + ); + + const prisma = new PrismaClient({ adapter }); + try { await prisma.$connect(); - success('Database connection successful.'); + await prisma.$disconnect(); } catch (e) { - throw new Error('Unable to connect to the database: ' + e.message); + const solution = ` Common causes and solutions: + 1. PostgreSQL is not running + - Start PostgreSQL service + - Check: pg_ctl status or systemctl status postgresql + + 2. Wrong credentials + - Verify username and password in DATABASE_URL + - Test connection: psql -U username -d database -h host + + 3. Database doesn't exist + - Create database: createdb umami + - Or use psql: CREATE DATABASE umami; + + 4. Connection refused + - Check PostgreSQL is listening on the correct port + - Verify firewall settings + - Check pg_hba.conf for access permissions`; + + error(`Unable to connect to the database: ${e.message}`, solution); + throw new Error('Database connection failed'); } } async function checkDatabaseVersion() { - const query = await prisma.$queryRaw`select version() as version`; - const version = semver.valid(semver.coerce(query[0].version)); + const url = new URL(process.env.DATABASE_URL); - if (semver.lt(version, MIN_VERSION)) { - throw new Error( - `Database version is not compatible. Please upgrade to ${MIN_VERSION} or greater.`, - ); + const adapter = new PrismaPg( + { connectionString: url.toString() }, + { schema: url.searchParams.get('schema') }, + ); + + const prisma = new PrismaClient({ adapter }); + + try { + const query = await prisma.$queryRaw`select version() as version`; + const version = semver.valid(semver.coerce(query[0].version)); + + if (semver.lt(version, MIN_VERSION)) { + const solution = ` Your PostgreSQL version (${version}) is below the minimum required (${MIN_VERSION}) + + Upgrade options: + 1. Using package manager (Ubuntu/Debian): + sudo apt-get update + sudo apt-get install postgresql-14 + + 2. Using package manager (macOS with Homebrew): + brew upgrade postgresql + + 3. Download from official site: + https://www.postgresql.org/download/`; + + error( + `Database version ${version} is not compatible. Minimum required: ${MIN_VERSION}`, + solution, + 'https://www.postgresql.org/download/', + ); + await prisma.$disconnect(); + throw new Error('Database version incompatible'); + } + + success(`Database version check successful (PostgreSQL ${version}).`); + await prisma.$disconnect(); + } catch (e) { + if (e.message === 'Database version incompatible') { + throw e; + } + error(`Unable to check database version: ${e.message}`); + throw e; } - - success('Database version check successful.'); } async function applyMigration() { if (!process.env.SKIP_DB_MIGRATION) { - console.log(execSync('prisma migrate deploy').toString()); + try { + console.log(chalk.cyan('\nApplying database migrations...\n')); + console.log(execSync('prisma migrate deploy').toString()); + success('Database is up to date.'); + } catch { + const solution = ` Migration failed. Try these steps: + 1. Ensure database is accessible + 2. Check migration files in prisma/migrations + 3. Reset database if needed: prisma migrate reset + 4. Manually apply migrations: prisma migrate deploy`; - success('Database is up to date.'); + error('Failed to apply database migrations.', solution); + throw new Error('Migration failed'); + } + } else { + console.log(chalk.yellow('āš ļø Skipping database migrations (SKIP_DB_MIGRATION is set).\n')); } } (async () => { let err = false; - for (const fn of [checkEnv, checkConnection, checkDatabaseVersion, applyMigration]) { + const checks = [ + { name: 'Environment', fn: checkEnv }, + { name: 'Connection', fn: checkConnection }, + { name: 'Version', fn: checkDatabaseVersion }, + { name: 'Migration', fn: applyMigration }, + ]; + + for (const check of checks) { try { - await fn(); - } catch (e) { - error(e.message); + await check.fn(); + } catch { err = true; - } finally { - if (err) { - process.exit(1); - } + break; // Stop on first error } } + + if (err) { + console.log(chalk.red.bold('\nāŒ Database check failed!\n')); + console.log(chalk.cyan('Please fix the errors above and try again.\n')); + process.exit(1); + } else { + console.log(chalk.green.bold('\nāœ… All database checks passed!\n')); + } })(); diff --git a/scripts/check-env.js b/scripts/check-env.js index 79c0984d..a696ba96 100644 --- a/scripts/check-env.js +++ b/scripts/check-env.js @@ -1,5 +1,32 @@ /* eslint-disable no-console */ import 'dotenv/config'; +import chalk from 'chalk'; + +/** + * Variable descriptions and examples + */ +const variableInfo = { + DATABASE_URL: { + description: 'PostgreSQL database connection string', + example: 'postgresql://username:password@localhost:5432/umami', + required: true, + }, + CLOUD_URL: { + description: 'Umami Cloud URL', + example: 'https://cloud.umami.is', + required: false, + }, + CLICKHOUSE_URL: { + description: 'ClickHouse database URL (required when CLOUD_URL is set)', + example: 'https://clickhouse.example.com', + required: false, + }, + REDIS_URL: { + description: 'Redis connection URL (required when CLOUD_URL is set)', + example: 'redis://localhost:6379', + required: false, + }, +}; function checkMissing(vars) { const missing = vars.reduce((arr, key) => { @@ -10,14 +37,37 @@ function checkMissing(vars) { }, []); if (missing.length) { - console.log(`The following environment variables are not defined:`); + console.log(chalk.red.bold('\nāŒ Environment Configuration Error\n')); + console.log(chalk.yellow('The following environment variables are not defined:\n')); + for (const item of missing) { - console.log(' - ', item); + const info = variableInfo[item] || {}; + console.log(chalk.red(` āœ— ${item}`)); + + if (info.description) { + console.log(chalk.gray(` Description: ${info.description}`)); + } + + if (info.example) { + console.log(chalk.cyan(` Example: ${info.example}`)); + } + + console.log(''); } + + console.log(chalk.yellow.bold('šŸ’” Solution:\n')); + console.log(" 1. Create a .env file in the project root if it doesn't exist"); + console.log(' 2. Copy the template from .env.example:'); + console.log(chalk.cyan(' cp .env.example .env')); + console.log(' 3. Add the missing variables to your .env file\n'); + + console.log(chalk.blue('šŸ“– For more information, see .env.example or SETUP.md\n')); + process.exit(1); } } +// Check required variables based on configuration if (!process.env.SKIP_DB_CHECK && !process.env.DATABASE_TYPE) { checkMissing(['DATABASE_URL']); } @@ -25,3 +75,6 @@ if (!process.env.SKIP_DB_CHECK && !process.env.DATABASE_TYPE) { if (process.env.CLOUD_URL) { checkMissing(['CLOUD_URL', 'CLICKHOUSE_URL', 'REDIS_URL']); } + +// Success message +console.log(chalk.green('āœ“ Environment variables validated successfully\n'));