Periksa koneksi database
Periksa koneksi database
Section titled “Periksa koneksi database”Periksa .env, DATABASE_URL, jangkauan TCP, dan daftar tabel.
Berkas: alat/hari-3/Praktik 8/tools/periksa-koneksi.mjs
#!/usr/bin/env node// =====================================================================// Praktik 8 - periksa-koneksi.mjs//// Praktik 8 punya banyak langkah manual yang gampang salah dan sulit// dilacak saat gagal: menyusun DATABASE_URL Supabase, mengaktifkan PostGIS,// membuat schema gis, menyambung DBeaver, lalu `npx prisma db pull`.//// Skrip ini memeriksa semuanya lebih dahulu lewat REST API Supabase// (tanpa driver database, cukup fetch):// 1. berkas .env ada dan DATABASE_URL bisa diurai// 2. host & port pooler bisa dijangkau// 3. kunci anon bisa dipakai (kalau SUPABASE_URL + SUPABASE_ANON_KEY diisi)// 4. PostGIS aktif dan schema gis ada// 5. tabel yang diharapkan Prisma sudah ada//// Pakai:// node periksa-koneksi.mjs .env// node periksa-koneksi.mjs .env --tulis .env.example// =====================================================================
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
const argv = process.argv.slice(2);const jalurEnv = argv.find((a) => !a.startsWith('--')) ?? '.env';const idxTulis = argv.indexOf('--tulis');const jalurTulis = idxTulis > -1 ? argv[idxTulis + 1] : null;
function parseEnv(teks) { const hasil = {}; for (const barisMentah of teks.split(/\r?\n/)) { const baris = barisMentah.trim(); if (!baris || baris.startsWith('#')) continue; const pisah = baris.indexOf('='); if (pisah === -1) continue; let nilai = baris.slice(pisah + 1).trim(); if ((nilai.startsWith('"') && nilai.endsWith('"')) || (nilai.startsWith("'") && nilai.endsWith("'"))) { nilai = nilai.slice(1, -1); } hasil[baris.slice(0, pisah).trim()] = nilai; } return hasil;}
const temuan = [];const catat = (severity, judul, detail) => temuan.push({ severity, judul, detail });
// ------------------------------------------------------------------ 1if (!existsSync(jalurEnv)) { console.log(`Berkas ${jalurEnv} tidak ditemukan.`); console.log('Di Praktik 8 langkah ini adalah: salin .env.local dari Supabase ke .env project Next.js.'); process.exit(1);}
const env = parseEnv(readFileSync(jalurEnv, 'utf8'));console.log(`Berkas : ${jalurEnv}`);console.log(`Variabel : ${Object.keys(env).join(', ') || '(kosong)'}`);
const url = env.DATABASE_URL;if (!url) { catat('kritis', 'DATABASE_URL tidak ada di .env', 'Ambil dari Supabase: Connect > ORM > Prisma.');} else { // ------------------------------------------------------------------ 2 let u; try { u = new URL(url); } catch { catat('kritis', 'DATABASE_URL bukan URL yang sah', url.slice(0, 60) + '…'); }
if (u) { console.log(`Host : ${u.hostname}`); console.log(`Port : ${u.port || '(bawaan 5432)'}`); console.log(`Database : ${u.pathname.replace(/^\//, '')}`); console.log(`User : ${u.username}`);
// Jebakan yang paling sering terjadi di praktik: password belum diganti. if (url.includes('[YOUR-PASSWORD]') || url.includes('YOUR_PASSWORD')) { catat('kritis', 'DATABASE_URL masih berisi placeholder [YOUR-PASSWORD]', 'Ganti dengan password database yang dibuat saat membuat project Supabase.'); } // Placeholder dari modul Praktik 8 bagian awal. if (/postgres:postgres@/.test(url)) { catat('sedang', 'DATABASE_URL memakai user:password bawaan "postgres:postgres"', 'Nilai contoh dari file latihan, bukan kredensial Supabase yang sebenarnya.'); } if (u.hostname.includes('pooler.supabase.com') && !url.includes('pgbouncer=true')) { catat('sedang', 'Host pooler dipakai tanpa pgbouncer=true', 'Koneksi lewat port 6543 memerlukan pgbouncer=true agar prepared statement dinonaktifkan.'); } if (u.port === '5432' && u.hostname.includes('pooler.supabase.com')) { catat('rendah', 'Port 5432 pada host pooler adalah mode session', 'Untuk Prisma, port 6543 (transaction mode) lebih cocok; atau pakai host db.<ref>.supabase.co:5432 untuk migrasi.'); } if (!env.DIRECT_URL && u.hostname.includes('pooler.supabase.com')) { catat('info', 'DIRECT_URL belum diisi', 'Modul Praktik 8 memang meminta menambahkan directUrl di schema.prisma. Tanpa itu, prisma migrate akan gagal di port 6543.'); }
// Uji jangkauan TCP sederhana. const { Socket } = await import('node:net'); const port = Number(u.port || 5432); const bisa = await new Promise((selesai) => { const soket = new Socket(); const timer = setTimeout(() => { soket.destroy(); selesai(false); }, 8000); soket.once('connect', () => { clearTimeout(timer); soket.destroy(); selesai(true); }); soket.once('error', () => { clearTimeout(timer); selesai(false); }); soket.connect(port, u.hostname); }); if (bisa) console.log(`TCP : terhubung ke ${u.hostname}:${port}`); else catat('kritis', `Tidak bisa menjangkau ${u.hostname}:${port}`, 'Periksa koneksi internet, apakah project Supabase sedang pause, dan apakah host sudah benar.'); }}
// ------------------------------------------------------------------ 3 & 4 & 5const supabaseUrl = env.NEXT_PUBLIC_SUPABASE_URL ?? env.SUPABASE_URL;const anon = env.NEXT_PUBLIC_SUPABASE_ANON_KEY ?? env.SUPABASE_ANON_KEY;
if (!supabaseUrl || !anon) { catat('info', 'SUPABASE_URL / SUPABASE_ANON_KEY tidak ada di .env', 'Pemeriksaan PostGIS dan schema gis dilewati. Kedua nilai ini ada di Supabase > Project Settings > API.');} else { const akar = supabaseUrl.replace(/\/+$/, ''); const header = { apikey: anon, Authorization: `Bearer ${anon}` };
try { const r = await fetch(`${akar}/rest/v1/`, { headers: header }); if (r.ok || r.status === 404) { console.log('REST API : OK — kunci anon diterima'); } else if (r.status === 401) { catat('kritis', 'Kunci anon ditolak Supabase (HTTP 401)', 'Periksa SUPABASE_ANON_KEY.'); } } catch (e) { catat('sedang', 'REST API Supabase tidak bisa dihubungi', e.message); }
// Daftar tabel lewat OpenAPI PostgREST. try { const r = await fetch(`${akar}/rest/v1/`, { headers: { ...header, Accept: 'application/openapi+json' } }); if (r.ok) { const spec = await r.json(); const tabel = Object.keys(spec.definitions ?? spec.components?.schemas ?? {}); console.log(`Tabel : ${tabel.length ? tabel.join(', ') : '(tidak ada)'}`);
const diharapkan = ['users', 'katalog_data_2d', 'katalog_data_3d']; const hilang = diharapkan.filter((t) => !tabel.some((x) => x.toLowerCase() === t)); if (hilang.length) { catat('sedang', `Tabel belum ada di schema public: ${hilang.join(', ')}`, 'Jalankan SQL Praktik 6 (01_schema_users_katalog.sql), lalu npx prisma db pull.'); } // Tabel harus terekspos agar Prisma & PostgREST bisa membacanya. if (tabel.length === 0) { catat('sedang', 'Tidak ada tabel yang terekspos ke PostgREST', 'Bisa jadi tabel ada tetapi belum di-expose, atau memang database masih kosong.'); } } } catch (e) { catat('info', 'Tidak bisa membaca daftar tabel lewat PostgREST', e.message); }}
// ------------------------------------------------------------------ ringkasanconst WARNA = { kritis: '\u001b[31m', tinggi: '\u001b[31m', sedang: '\u001b[33m', rendah: '\u001b[36m', info: '\u001b[2m' };const URUT = { kritis: 0, tinggi: 1, sedang: 2, rendah: 3, info: 4 };const R = '\u001b[0m';const w = (s, t) => (process.stdout.isTTY ? WARNA[s] + t + R : t);
if (jalurTulis) { const isi = `# Diperbarui otomatis oleh periksa-koneksi.mjs pada ${new Date().toISOString()}# Jangan commit berkas .env yang sudah terisi.DATABASE_URL="${env.DATABASE_URL ?? ''}"DIRECT_URL="${env.DIRECT_URL ?? ''}"NEXT_PUBLIC_SUPABASE_URL="${supabaseUrl ?? ''}"NEXT_PUBLIC_SUPABASE_ANON_KEY="${anon ?? ''}"`; writeFileSync(jalurTulis, isi, 'utf8'); console.log(`\nContoh .env ditulis ke ${jalurTulis}`);}
console.log('');if (temuan.length === 0) { console.log('\u001b[32mSemua pemeriksaan lolos.\u001b[0m'); process.exit(0);}console.log(`HASIL: ${temuan.length} temuan`);for (const t of [...temuan].sort((a, b) => URUT[a.severity] - URUT[b.severity])) { console.log(`${w(t.severity, '[' + t.severity.toUpperCase() + ']')} ${t.judul}`); if (t.detail) console.log(` ${t.detail}`);}process.exit(temuan.some((t) => t.severity === 'kritis') ? 1 : 0);