COALESCE di Pintu API: Titip Nilai ALL di Kueri, Bukan di Handler
Super-admin tanpa bidang mengembalikan NULL dari database. Satu COALESCE di kueri login membuat kontrak API tetap total dan lencana admin tampil pas.
Ringkasan
Kirain bug sepele soal lencana bidang super-admin yang hilang, ternyata DB ngasih NULL dan token lama belum punya kolom itu. Daripada nambal di tiap handler Go, mending sekalian beres di query pakai COALESCE biar selalu balikin 'ALL'. Di frontend tipenya dibikin opsional biar token lama nggak error, lencana juga cuma muncul kalau bidangnya bukan 'ALL'.
Pertama kali saya mengetes pembaruan dasbor admin, saya login sebagai super-admin dan menatap layar. Lencana divisi yang kemarin tampil di kepala admin bidang keuangan ternyata nggak ada di sesi saya. Insting pertama bilang ini bug tampilan: tinggal tampilkan bidang di kueri, oper ke frontend, beres. Ternyata tebakan itu salah besar.
Masalahnya bukan cuma soal super-admin yang emang nggak punya bidang spesifik sehingga database mengembalikan NULL. Ada lapisan lain yang hampir lolos: token sesi lama. Pengguna yang sudah login sebelum pembaruan ini masih membawa payload yang sama sekali belum punya kolom tersebut. Kalo frontend dipaksa merender data yang nggak ada, hasilnya tulisan undefined yang mencolok di sidebar pemerintahan.
Tebakan pertama: periksa NULL di handler
Saya sempat berpikir menangani ini di sisi Go handler. Ngecek di handler login, kalau user.Bidang kosong, ganti jadi string ALL. Ulangi hal yang sama di handler profil. Tapi pendekatan itu rapuh dan melanggar prinsip DRY. Setiap endpoint baru yang mengembalikan konteks pengguna butuh pemeriksaan yang sama, dan cukup satu developer lupa, bug yang sama muncul lagi. Itu abstraksi yang bocor.
Pindahkan ke batas paling awal: kueri SQL
Keputusan yang lebih masuk akal: pindahkan logikanya ke batas paling awal, yaitu kueri database itu sendiri. Kueri SELECT di handler login saya ubah jadi COALESCE(bidang, 'ALL') AS bidang [1]. Sama persis di endpoint profil, biar LoginResponse maupun MeResponse jujur sama.
COALESCE mengembalikan nilai non-NULL pertama dalam daftar argumennya, atau NULL kalo nggak ada satupun, dengan tipe hasil yang merupakan agregat dari tipe argumennya [1]. Dengan 'ALL' sebagai nilai cadangan, setiap respons backend dijamin mengembalikan string yang valid. Nggak ada lagi NULL yang lolos ke lapisan aplikasi. Kontrak data jadi total: semua konsumen API, termasuk sesi lama yang ke-refresh belakangan, menerima bentuk data yang sama.
Ada bonus yang gampang diremehkan. Database adalah sumber kebenaran tunggal untuk status pengguna. Dengan menegakkan kontrak di corong paling lebar, kode Go di atasnya cuma memindai string biasa. Nggak ada kondisional nihil di lapisan bisnis cuma buat menutup kasus tepi yang seharusnya selesai lebih awal.
Pengaman ekstra di TypeScript
Backend udah menjamin pengiriman string, tapi saya tetap mendefinisikan bidang sebagai opsional di tipe AdminSession: bidang?: string [2]. Kelihatan kontradiktif, padahal strategi pengaman untuk realitas deployment. Token yang tersimpan di browser sebelum tanggal rilis jelas belum punya properti itu. Ada jeda antara deploy backend dan sesi klien yang di-refresh. Kalo tipenya wajib, frontend bisa melempar error tipe saat membaca properti yang belum ada di token lama. Tanda tanya di TypeScript persis untuk objek yang mungkin belum punya properti tersebut [2].
Lencana yang tahu kapan dirinya perlu muncul
Di sisi React, perenderan jadi rapi berkat kontrak yang udah stabil. Lencana biru di header dan sidebar cuma muncul kalau bidang && bidang !== "ALL" terpenuhi [3]. Super-admin nggak pernah lihat tulisan aneh; admin Pariwisata selalu lihat divisinya.
Penting dicatat: 'ALL' di sini murni nilai kontrak atau sentinel, bukan data tampilan. Untuk tampilan ada formatBidang(): peta label yang mentok ke nilai mentah kalo belum terdaftar, ALL jadi "Semua Bidang", PARIWISATA jadi "Pariwisata", dan seterusnya. Lencana nggak pernah nampilin sentinel mentah.
Pola ini bikin penambahan nilai bidang baru di masa depan nggak menyentuh UI sama sekali. Kondisi penolakan cuma spesifik ke 'ALL', jadi begitu ada divisi baru di daftar label, lencananya otomatis muncul tanpa perubahan komponen. Backend nggak perlu mikir siapa yang menangani nilai kosong; frontend nggak perlu menebak properti itu ada atau nggak. Semua pihak kerja dengan asumsi yang sama: data yang datang udah dalam bentuk paling aman.
Kalo situasimu mirip: ada kolom yang boleh kosong di database plus sesi lama yang masih aktif, selesaikan di batasnya, jangan di lapisan terjauh. Menambal di tempat paling jauh cuma nyiptakan utang teknis yang menghantuimu nanti. Selesaikan di sumbernya, biar bagian dalam aplikasi tetap bersih.