Membaca skema CI3 dengan kysely-codegen
Kamu tidak menulis tipe skema dengan tangan. Kamu membacanya dari database yang sudah ada — lalu bersiap untuk apa yang sebenarnya ada di sana.
Intisari
kysely-codegenmenyambung ke database dan menulisdb-types.ts. Berkas itu tidak pernah disunting tangan.- Jalankan lagi setiap skema berubah, dan commit hasilnya — CI tidak boleh perlu akses ke database produksi untuk membangun.
tinyint(1)jadinumber, bukanboolean. MySQL tidak punya boolean sungguhan.- Kolom
intberisi timestamp Unix — pola khas CI3 — akan bertipenumber, dan compiler tidak akan menolongmu. - Skema besar menghasilkan berkas besar; saring dengan
--include-patternsupaya editor tetap responsif.
Menjalankannya
pnpm add -D kysely-codegen
# Jalankan terhadap SALINAN, bukan produksi
DATABASE_URL="mysql://user:sandi@localhost:3306/portal_dev" \
pnpm kysely-codegen --dialect mysql --out-file src/lib/db-types.ts
{
"scripts": {
"db:types": "kysely-codegen --dialect mysql --out-file src/lib/db-types.ts"
}
}
Jalankan terhadap salinan skema, bukan produksi. Introspeksi memang hanya membaca, tapi
kredensial produksi tidak seharusnya ada di laptop siapa pun. Ambil skemanya sekali dengan
mysqldump --no-data, muat ke MySQL lokal, dan jalankan codegen di sana. Bonusnya: kamu punya
skema yang bisa di-commit dan bisa direview.
mysqldump --no-data --single-transaction --routines=false \
-h $PROD_HOST -u readonly -p portal > db/skema.sql
mysql -h localhost -u root -p portal_dev < db/skema.sql
Yang dihasilkannya
// src/lib/db-types.ts — DIGENERATE. Jangan disunting.
import type { ColumnType } from "kysely";
export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
? ColumnType<S, I | undefined, U>
: ColumnType<T, T | undefined, T>;
export interface Artikel {
id: Generated<number>;
judul: string;
slug: string;
isi: string;
ringkasan: string | null;
kategori_id: number | null;
penulis_id: number;
status: string;
premium: number; // tinyint(1) — BUKAN boolean
hits: Generated<number>;
terbit_pada: Date | null;
created_at: number; // int(11) berisi timestamp Unix — khas CI3
}
export interface DB {
artikel: Artikel;
kategori: Kategori;
member: Member;
// …
}
Generated<T> menandai kolom yang diisi database sendiri — AUTO_INCREMENT
atau kolom ber-DEFAULT. Kysely memakainya untuk membuat kolom itu opsional saat
insert tapi tetap ada saat select. Ini yang membuat insertInto tidak
menuntutmu mengisi id.
Lima kejutan di skema CI3
1. tinyint(1) bukan boolean
// Yang dihasilkan codegen:
premium: number; // 0 atau 1
// Ini SELALU true — 0 adalah number, dan objeknya truthy
if (artikel.premium) { … } // BENAR sebenarnya, 0 falsy
if (artikel.premium !== null) { … } // SALAH — 0 lolos
Bungkus di batas: fungsi di src/lib/ mengembalikan bentuk domain yang sudah bersih, bukan
baris mentah.
// src/lib/artikel.ts
function keDomain(baris: Selectable<Artikel>) {
return {
id: baris.id,
judul: baris.judul,
premium: baris.premium === 1, // jadi boolean sungguhan
dibuatPada: new Date(baris.created_at * 1000), // Unix → Date
terbitPada: baris.terbit_pada,
};
}
Inilah alasan aturan "hanya src/lib/ yang menyentuh db" itu ada.
Konversi seperti ini terjadi di satu tempat. Kalau halaman menyusun kueri sendiri, tiap halaman harus
ingat mengalikan created_at dengan 1000 — dan yang lupa akan menampilkan tanggal di tahun
1970 tanpa siapa pun menyadarinya sampai pembaca melapor.
2. int berisi timestamp Unix
CI3 sering menyimpan waktu sebagai integer detik. Codegen menghasilkan number, dan compiler
tidak tahu bedanya dengan angka lain. Satu-satunya pertahanan adalah lapisan konversi di atas — dan
menamai fieldnya dengan jelas di tipe domainmu.
3. Kolom DATETIME tanpa zona waktu
// mysql2 menafsirkan DATETIME memakai zona waktu koneksi.
// Kalau tidak diset, ia memakai zona waktu SERVER — yang berbeda
// antara laptopmu (WIB) dan container ECS (UTC).
createPool({ timezone: "Z" }); // paksa UTC di mana pun
Tanpa ini, artikel yang terbit jam 07.00 WIB akan tampil jam 07.00 di laptopmu dan jam 14.00 di produksi — atau sebaliknya. Untuk portal berita yang menampilkan "2 jam lalu", ini bug yang langsung terlihat pembaca.
4. Kolom enum jadi union, dan itu bagus
status: "draf" | "tinjau" | "terbit" | "arsip";
Kalau kolom statusmu di MySQL bertipe ENUM, kamu mendapat union literal gratis — persis
konstruksi yang dibahas di Fase 0. Kalau bertipe VARCHAR, kamu hanya mendapat
string. Kalau ada kesempatan mengubah satu kolom saja di skema warisanmu, jadikan ini
kandidat pertama.
5. Skema besar bikin editor lambat
kysely-codegen --dialect mysql \
--include-pattern "(artikel|kategori|member|langganan|perusahaan|tag)%" \
--out-file src/lib/db-types.ts
Portal berita berumur biasanya punya tabel yang tidak lagi dipakai: ci_sessions, tabel cache
lama, tabel log, sisa fitur yang mati. Menyaringnya membuat berkas tipe lebih kecil, editor lebih responsif,
dan — yang lebih penting — tabel yang tidak seharusnya disentuh Astro jadi tidak bisa disentuh
sama sekali. Itu pagar yang bagus.
Menjaganya tetap benar
# .github/workflows/ci.yml — potongan
- name: Pastikan tipe database mutakhir
run: |
mysql -h 127.0.0.1 -u root portal_dev < db/skema.sql
pnpm db:types
git diff --exit-code src/lib/db-types.ts
Kalau seseorang mengubah db/skema.sql tanpa menjalankan ulang codegen, CI gagal. Tanpa pagar
ini, tipe dan skema akan menyimpang perlahan sampai tidak ada yang mempercayai tipenya lagi — dan pada
titik itu seluruh keuntungan Kysely hilang.
Selectable, Insertable, Updateable
import type { Selectable, Insertable, Updateable } from "kysely";
import type { Artikel } from "./db-types";
type BarisArtikel = Selectable<Artikel>; // hasil SELECT: id ada, tipe nyata
type ArtikelBaru = Insertable<Artikel>; // untuk INSERT: id opsional
type UbahArtikel = Updateable<Artikel>; // untuk UPDATE: semua opsional
Tiga tipe dari satu definisi tabel. Ini yang membuatmu tidak perlu menulis DTO terpisah untuk tiap operasi
— dan yang membuat fungsi simpanArtikel(data: Insertable<Artikel>) menolak objek yang
kelebihan atau kekurangan field.
Latihan: ambil skema database CI3-mu dengan mysqldump --no-data, muat ke MySQL
lokal, jalankan kysely-codegen, dan buka hasilnya. Cari dan catat: berapa kolom
tinyint(1) yang sebenarnya boolean, berapa kolom int yang sebenarnya waktu,
dan berapa tabel yang sudah tidak dipakai lagi. Tiga daftar itu adalah pekerjaan rumah migrasimu yang
sesungguhnya.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.