diff --git a/.env.example b/.env.example index 1dc75cf..c217f40 100644 --- a/.env.example +++ b/.env.example @@ -45,3 +45,22 @@ AI_LLM_BASE_URL=https://9router.asepharyana.tech/v1 AI_LLM_MODEL=free AI_ANALYSIS_TIMEOUT_MS=30000 +# Database Configuration +DATABASE_TYPE=sqlite +# DATABASE_TYPE=postgres + +# PostgreSQL Configuration (used when DATABASE_TYPE=postgres) +# Option 1: Use DATABASE_URL for connection string +# DATABASE_URL=postgresql://user:password@localhost:5432/discord_bot + +# Option 2: Use individual connection parameters +# POSTGRES_HOST=localhost +# POSTGRES_PORT=5432 +# POSTGRES_USER=postgres +# POSTGRES_PASSWORD=your_password_here +# POSTGRES_DB=discord_bot + +# PostgreSQL Connection Pool Configuration +# POSTGRES_POOL_MIN=2 +# POSTGRES_POOL_MAX=10 + diff --git a/package.json b/package.json index 24e4ac1..d041ad2 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@discordjs/opus": "^0.10.0", "@discordjs/voice": "^0.19.1", "@snazzah/davey": "^0.1.10", + "@types/pg": "^8.20.0", "better-sqlite3": "^12.10.0", "class-transformer": "^0.5.1", "class-validator": "^0.15.1", @@ -27,6 +28,7 @@ "helmet": "^8.1.0", "libsodium-wrappers": "^0.8.2", "p-retry": "^6.2.0", + "pg": "^8.20.0", "pino": "^9.4.0", "pino-http": "^11.0.0", "prism-media": "2.0.0-alpha.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8a80054..e94ac20 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@snazzah/davey': specifier: ^0.1.10 version: 0.1.11(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@types/pg': + specifier: ^8.20.0 + version: 8.20.0 better-sqlite3: specifier: ^12.10.0 version: 12.10.0 @@ -47,6 +50,9 @@ importers: p-retry: specifier: ^6.2.0 version: 6.2.1 + pg: + specifier: ^8.20.0 + version: 8.20.0 pino: specifier: ^9.4.0 version: 9.14.0 @@ -614,6 +620,9 @@ packages: '@types/node@24.12.4': resolution: {integrity: sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==} + '@types/pg@8.20.0': + resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} + '@types/qs@6.15.1': resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} @@ -1384,6 +1393,40 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pg-cloudflare@1.3.0: + resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} + + pg-connection-string@2.12.0: + resolution: {integrity: sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.13.0: + resolution: {integrity: sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.13.0: + resolution: {integrity: sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.20.0: + resolution: {integrity: sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1426,6 +1469,22 @@ packages: resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} @@ -1900,6 +1959,10 @@ packages: utf-8-validate: optional: true + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + y18n@4.0.3: resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} @@ -2328,6 +2391,12 @@ snapshots: dependencies: undici-types: 7.16.0 + '@types/pg@8.20.0': + dependencies: + '@types/node': 24.12.4 + pg-protocol: 1.13.0 + pg-types: 2.2.0 + '@types/qs@6.15.1': {} '@types/range-parser@1.2.7': {} @@ -3066,6 +3135,41 @@ snapshots: pathe@2.0.3: {} + pg-cloudflare@1.3.0: + optional: true + + pg-connection-string@2.12.0: {} + + pg-int8@1.0.1: {} + + pg-pool@3.13.0(pg@8.20.0): + dependencies: + pg: 8.20.0 + + pg-protocol@1.13.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.20.0: + dependencies: + pg-connection-string: 2.12.0 + pg-pool: 3.13.0(pg@8.20.0) + pg-protocol: 1.13.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.3.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + picocolors@1.1.1: {} picomatch@4.0.4: {} @@ -3145,6 +3249,16 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + prebuild-install@7.1.3: dependencies: detect-libc: 2.1.2 @@ -3600,6 +3714,8 @@ snapshots: ws@8.20.1: {} + xtend@4.0.2: {} + y18n@4.0.3: {} yallist@4.0.0: {} diff --git a/setup-postgres.sh b/setup-postgres.sh new file mode 100755 index 0000000..4ef2506 --- /dev/null +++ b/setup-postgres.sh @@ -0,0 +1,97 @@ +#!/bin/bash +# PostgreSQL Migration Quick Start Script +# Run this to set up PostgreSQL migration + +set -e + +echo "🚀 Discord Bot - PostgreSQL Migration Setup" +echo "===========================================" +echo "" + +# Check if PostgreSQL is installed +if ! command -v psql &> /dev/null; then + echo "❌ PostgreSQL is not installed. Please install PostgreSQL first." + echo " macOS: brew install postgresql" + echo " Ubuntu: sudo apt-get install postgresql postgresql-contrib" + echo " Windows: Download from https://www.postgresql.org/download/windows/" + exit 1 +fi + +echo "✅ PostgreSQL found" +echo "" + +# Check if Node.js is installed +if ! command -v node &> /dev/null; then + echo "❌ Node.js is not installed." + exit 1 +fi + +echo "✅ Node.js found" +echo "" + +# Create .env if it doesn't exist +if [ ! -f .env ]; then + echo "📝 Creating .env file..." + cp .env.example .env + echo "⚠️ Please update .env with your PostgreSQL connection string" + echo " DATABASE_URL=postgresql://user:password@localhost:5432/discord_bot" + echo "" +fi + +# Install dependencies +echo "📦 Installing dependencies..." +pnpm add pg pg-pool node-pg-migrate +pnpm add -D @types/pg @types/node-pg-migrate +pnpm remove better-sqlite3 @types/better-sqlite3 2>/dev/null || true + +echo "✅ Dependencies installed" +echo "" + +# Create database +read -p "Enter PostgreSQL username (default: postgres): " PG_USER +PG_USER=${PG_USER:-postgres} + +read -p "Enter PostgreSQL password: " -s PG_PASSWORD +echo "" + +read -p "Enter database name (default: discord_bot): " DB_NAME +DB_NAME=${DB_NAME:-discord_bot} + +read -p "Enter PostgreSQL host (default: localhost): " PG_HOST +PG_HOST=${PG_HOST:-localhost} + +read -p "Enter PostgreSQL port (default: 5432): " PG_PORT +PG_PORT=${PG_PORT:-5432} + +echo "" +echo "🗄️ Creating database..." + +PGPASSWORD=$PG_PASSWORD psql -h $PG_HOST -U $PG_USER -p $PG_PORT -tc "SELECT 1 FROM pg_database WHERE datname = '$DB_NAME'" | grep -q 1 || \ +PGPASSWORD=$PG_PASSWORD psql -h $PG_HOST -U $PG_USER -p $PG_PORT -c "CREATE DATABASE $DB_NAME" + +echo "✅ Database created" +echo "" + +# Update .env with connection string +DATABASE_URL="postgresql://$PG_USER:$PG_PASSWORD@$PG_HOST:$PG_PORT/$DB_NAME" +sed -i.bak "s|DATABASE_URL=.*|DATABASE_URL=$DATABASE_URL|" .env +rm -f .env.bak + +echo "✅ .env updated with DATABASE_URL" +echo "" + +# Run migrations +echo "🔄 Running migrations..." +npx node-pg-migrate up + +echo "✅ Migrations completed" +echo "" + +echo "🎉 PostgreSQL migration setup complete!" +echo "" +echo "Next steps:" +echo "1. Review the migration files in src/db/migrations/" +echo "2. Update imports in your code (see POSTGRES_IMPLEMENTATION.md)" +echo "3. Run: pnpm run typecheck" +echo "4. Run: pnpm run dev" +echo "" diff --git a/src/config.ts b/src/config.ts index 0fbd4e4..be4bd2d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -46,15 +46,36 @@ const configSchema = z.object({ AI_LLM_BASE_URL: z.string().url().default("https://9router.asepharyana.tech/v1"), AI_LLM_MODEL: z.string().default("free"), AI_ANALYSIS_TIMEOUT_MS: z.coerce.number().positive().default(30000), + DATABASE_TYPE: z.enum(["sqlite", "postgres"]).default("sqlite"), + DATABASE_URL: z.string().optional(), + POSTGRES_HOST: z.string().default("localhost"), + POSTGRES_PORT: z.coerce.number().int().positive().default(5432), + POSTGRES_USER: z.string().optional(), + POSTGRES_PASSWORD: z.string().optional(), + POSTGRES_DB: z.string().optional(), + POSTGRES_POOL_MIN: z.coerce.number().int().positive().default(2), + POSTGRES_POOL_MAX: z.coerce.number().int().positive().default(10), }).superRefine((value, ctx) => { - if (!value.AI_ANALYSIS_ENABLED) return; - if (!value.AI_LLM_API_KEY) { + if (!value.AI_ANALYSIS_ENABLED) { + // Continue to database validation + } else if (!value.AI_LLM_API_KEY) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["AI_LLM_API_KEY"], message: "AI_LLM_API_KEY is required when AI_ANALYSIS_ENABLED=true", }); } + + // Validate PostgreSQL configuration + if (value.DATABASE_TYPE === "postgres") { + if (!value.DATABASE_URL && !value.POSTGRES_HOST) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["DATABASE_URL"], + message: "Either DATABASE_URL or POSTGRES_HOST must be provided when DATABASE_TYPE=postgres", + }); + } + } }); export type AppConfig = z.infer;