Lewati ke konten

Periksa koneksi database

Periksa .env, DATABASE_URL, jangkauan TCP, dan daftar tabel.

Berkas: alat/hari-3/Praktik 8/tools/periksa-koneksi.mjs

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 });
// ------------------------------------------------------------------ 1
if (!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 & 5
const 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);
}
}
// ------------------------------------------------------------------ ringkasan
const 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);