Post 404 Semua Setelah Upgrade EmDash 0.29 di PostgreSQL: Akar Masalah dan Solusinya

TL;DR — Setelah upgrade EmDash dari 0.21 ke 0.29 di deployment Node.js + PostgreSQL, semua post blog mendadak 404, halaman blog menampilkan "Belum ada postingan", sitemap post balas HTTP 500, dan dashboard admin error "Could not load dashboard data". Tidak ada data yang hilang. Akar masalahnya adalah bug deteksi dialect di bundle npm EmDash: PostgreSQL salah terdeteksi sebagai SQLite, jadi semua query dibuat dengan sintaks SQLite di atas database Postgres. Solusinya patch satu baris via patch-package. Artikel ini menuliskan insidennya selengkap mungkin — gejala, diagnosis, akar masalah, perbaikan, plus dua gotcha bonus — biar kamu nggak kehilangan waktu (dan jantung) karena bug yang sama.
Daftar Isi
- Kondisi deployment
- Gejala
- Diagnosis: reproduksi lokal, baca error aslinya
- Akar masalah: bundler me-rename class yang dicocokkan berdasarkan nama
- Solusi: satu baris via patch-package
- Gotcha bonus dari sesi yang sama
- Route plugin default-nya admin-only
- Query param GET tidak pernah sampai ke validasi input route
- Pelajaran yang bisa diambil
- Versi yang terdampak
Kondisi deployment
- EmDash di-upgrade 0.21.0 → 0.29.0
- Astro output: "server" dengan adapter @astrojs/node standalone
Database: PostgreSQL via postgres({ ... }) dari emdash/db- Deploy pakai Docker (multi-stage build, npm ci → astro build)
Kalau kamu menjalankan EmDash di SQLite, libSQL/Turso, atau Cloudflare D1, bug ini tidak berdampak sama sekali — mungkin itu sebabnya bisa lolos rilis. Bug ini hanya menggigit deployment Postgres, dan hanya lewat bundle npm yang dipublikasikan. Kalau kamu baru mau pindah ke EmDash, panduan migrasi WordPress ke EmDash membahas jalur mulusnya; artikel ini membahas hari ketika semuanya berantakan.
Gejala
- Semua URL post balas 404 — termasuk post yang jelas-jelas ada di sitemap.
- Halaman blog menampilkan empty state ("Belum ada postingan"), bukan error — dari sisi template, query "berhasil" tapi hasilnya nol baris.
- Sitemap post balas HTTP 500.
- Dashboard admin terbuka tapi menampilkan "Could not load dashboard data — Refresh the page or try again."
- Halaman statis/marketing yang tidak query CMS tetap normal — bikin site terlihat "setengah hidup" dan makin membingungkan.
Interpretasi paling menakutkan: "upgrade menghapus semua konten saya". Ternyata tidak. Sebelum melakukan apa pun, cek dulu:
SELECT status, COUNT(*) FROM ec_posts GROUP BY status;
-- status | count
-- published | 85 <- semua masih adaKalau angkanya benar, tarik napas dulu. Ini bug query, bukan kehilangan data. Jangan buru-buru restore backup.
Diagnosis: reproduksi lokal, baca error aslinya
Build lalu jalankan server produksi secara lokal melawan database yang sama, kemudian buka route yang bermasalah:
npm run build
node ./dist/server/entry.mjs
# lalu: curl http://127.0.0.1:4321/blogDua error langsung muncul:
[emdash] runtime init failed (page renders without CMS data):
Error: Migration failed: function datetime(unknown) does not exist
(migration: 044_comment_reactions)
Failed to load posts: Error: Failed to load collection:
syntax error at or near "ON"Dua error ini penyakitnya sama dengan dua wajah: datetime('now') adalah fungsi SQLite yang tidak ada di PostgreSQL (makanya migrasi meledak), dan loader collection juga menghasilkan SQL rasa SQLite yang ditolak Postgres. Artinya EmDash mengira dia sedang bicara dengan SQLite. Kenapa bisa?
Akar masalah: bundler me-rename class yang dicocokkan berdasarkan nama
EmDash mendeteksi dialect database aktif seperti ini (dist/dialect-helpers-*.mjs):
/** Detect dialect type from a Kysely instance via the adapter class name. */
function detectDialect(db) {
if (db.getExecutor().adapter.constructor.name === "PostgresAdapter") return "postgres";
return "sqlite"; // <- fallback-nya
}Semua helper dialect — currentTimestamp(), jsonExtractExpr(), buildStatusCondition() — bercabang dari sini. Kalau deteksi gagal, semuanya jatuh ke SQL SQLite. Sekarang lihat apa yang benar-benar dikirim di paket npm (dist/database/pg-migration-lock.mjs):
import { PostgresAdapter, PostgresDialect, sql } from "kysely";
/**
* Extends the stock adapter ... and keeps the class NAME `PostgresAdapter`
* because `detectDialect()` identifies the dialect via
* `adapter.constructor.name` — a differently-named adapter would make
* every dialect helper fall back to SQLite SQL against a Postgres database.
*/
var PostgresAdapter$1 = class extends PostgresAdapter {
async acquireMigrationLock(db, _opt) { /* ... */ }
};
var FailFastPostgresDialect = class extends PostgresDialect {
createAdapter() {
return new PostgresAdapter$1();
}
};Komentar di kodenya sendiri mendokumentasikan aturannya — dan baris tepat di bawahnya melanggar aturan itu. Di source TypeScript, subclass-nya bernama PostgresAdapter, tapi bundler me-rename binding-nya jadi PostgresAdapter$1 untuk menghindari bentrok dengan import kysely. Karena var X = class extends Y {} adalah class expression, JavaScript mengambil .name class dari nama variabelnya:
const d = new FailFastPostgresDialect({ pool: {} });
console.log(d.createAdapter().constructor.name);
// -> "PostgresAdapter$1" (seharusnya "PostgresAdapter")"PostgresAdapter$1" !== "PostgresAdapter" → detectDialect() mengembalikan "sqlite" → semua query dan migrasi baru dibuat dalam dialect SQLite → post 404, blog kosong, sitemap 500, dashboard mati. Satu identifier yang di-rename, satu site tumbang.
Solusi: satu baris via patch-package
Sampai diperbaiki di upstream, kunci lagi nama class-nya.
1. Edit node_modules/emdash/dist/database/pg-migration-lock.mjs dan tambahkan satu baris tepat sebelum FailFastPostgresDialect:
Object.defineProperty(PostgresAdapter$1, "name", { value: "PostgresAdapter" });2. Verifikasi patch-nya bekerja:
node -e "import('./node_modules/emdash/dist/database/pg-migration-lock.mjs').then(m => {
const d = new m.FailFastPostgresDialect({ pool: {} });
console.log(d.createAdapter().constructor.name); // -> PostgresAdapter
})"3. Simpan sebagai patch permanen supaya terpasang di setiap install:
npm install -D patch-package
npx patch-package emdash// package.json
"scripts": {
"postinstall": "patch-package"
}Ini menghasilkan patches/emdash+0.29.0.patch — commit file-nya. Setiap npm install berikutnya otomatis menerapkan ulang patch.
4. Pengguna Docker — langkah ini penting. Kalau Dockerfile kamu meng-install dependency sebelum meng-copy repo (layout umum untuk cache), folder patches/ belum ada saat postinstall jalan, patch-package diam-diam tidak menemukan apa pun, dan produksi tetap memakai bundle yang rusak padahal build lokalmu sudah benar. Copy patch-nya lebih dulu:
COPY package.json package-lock.json ./
# patches/ harus ada sebelum npm ci — postinstall menjalankan patch-package
COPY patches ./patches
RUN npm ci5. Rebuild, jalankan server hasil build melawan database asli, dan verifikasi route yang tadinya rusak: blog menampilkan post lagi, post individual ter-render, sitemap balas 200, dashboard admin terbuka. Migrasi yang tadinya gagal (044_comment_reactions) juga akan sukses di boot berikutnya karena sekarang menghasilkan SQL Postgres.
Gotcha bonus dari sesi yang sama
Route plugin default-nya admin-only
Kalau kamu mendaftarkan route plugin custom dan memanggilnya dari site publik, request anonim kena 401. Route butuh public: true eksplisit:
routes: {
counts: {
public: true, // <- tanpa ini: 401 untuk pengunjung
handler: async (ctx) => { /* ... */ },
},
},Query param GET tidak pernah sampai ke validasi input route
Docs bilang input GET/DELETE diambil dari query parameter, tapi dispatcher route di 0.29 hanya mem-parse body JSON. Route GET dengan schema input zod selalu gagal 400 karena input-nya selamanya undefined. Solusinya — buang schema input di route GET dan parse URL-nya sendiri:
counts: {
public: true,
handler: async (ctx) => {
const slug = new URL(ctx.request.url).searchParams.get("slug") ?? "";
if (!slugSchema.safeParse(slug).success) {
throw new Response(JSON.stringify({ error: "Invalid slug" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
// ...
},
},Pelajaran yang bisa diambil
- Build hijau bukan berarti deploy jalan. astro build selesai tanpa error satu pun — bug-nya hanya muncul saat runtime, melawan koneksi Postgres sungguhan. Untuk upgrade CMS atau database, jalankan server hasil build secara lokal melawan database semirip produksi dan klik route-route penting sebelum naik. Lebih aman lagi lewat staging dulu — lihat cara menyiapkan staging environment di EmDash.
- Jangan panik restore backup saat konten "hilang". Tiga puluh detik SELECT COUNT(*) menyelamatkan kami dari restore yang sia-sia (dan berpotensi merusak). Bedakan data hilang dengan query gagal.
- Deteksi tipe berbasis nama memang rapuh dari desainnya. constructor.name gampang patah kena bundler dan minifier. Kalau kamu maintain library, pakai properti penanda eksplisit atau instanceof.
- Patch harus ikut sampai ke deploy. Patch yang terpasang di laptopmu tapi tidak di dalam build Docker lebih buruk daripada tidak ada patch — karena kelihatannya sudah beres. Cek urutan layer Dockerfile.
Performa site juga langsung pulih begitu query benar lagi — kalau mau tuning lebih jauh, mulai dari Core Web Vitals dan performa CMS.
Versi yang terdampak
Teramati di emdash@0.29.0 (adapter Node + PostgreSQL). Versi 0.21.0 tidak terdampak (wrapper dialect Postgres fail-fast-nya belum ada). Deployment SQLite/libSQL/D1 aman di versi mana pun. Kalau kamu membaca ini setelah rilis yang lebih baru, cek apakah dist/database/pg-migration-lock.mjs masih meng-assign subclass adapter ke binding yang di-rename — kalau variabelnya bukan lagi PostgresAdapter$1, atau deteksinya tidak lagi memakai constructor.name, patch-nya boleh dilepas.








Komentar