Add local MariaDB copy and Drizzle connection for the legacy cleo database

- docker-compose: mariadb:10.4 service on port 3307 (utf8mb4), seeded from a
  mysqldump of the office server so development never writes to production
- drizzle-amigo.config.ts + `pnpm db:amigo:pull` (introspection only)
- scripts/fix-amigo-schema.ts repairs drizzle-kit's MariaDB output: quoted
  defaults, dropped primary keys, missing column-builder imports
- db/amigo: pulled schema and `amigoDb` connection
- README: step-by-step local MariaDB setup (dump, import, cleo_next grants)
- CLAUDE.md: MyISAM, concurrent PHP writes, zero dates, other apps' tables

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main
Deardo Satria 2 weeks ago
parent a44fb24136
commit 21bce758cb

@ -4,8 +4,9 @@
POSTGRES_PASSWORD=
DATABASE_URL="postgresql://postgres:<POSTGRES_PASSWORD>@localhost:5432/cleo_auth"
# MariaDB `cleo` from the legacy app (office LAN only)
AMIGO_DATABASE_URL="mysql://<user>:<password>@192.168.3.3:3306/cleo"
# MariaDB `cleo`: local Docker copy of the office server (see README step 6), never the server itself
AMIGO_DATABASE_URL="mysql://cleo_next:<password-cleo-next>@localhost:3307/cleo"
MARIADB_ROOT_PASSWORD="<password-root-lokal>"
# Better Auth. Generate the secret with:
# node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
@ -17,3 +18,4 @@ SEED_ADMIN_USERNAME=admin
SEED_ADMIN_PASSWORD=
SEED_ADMIN_NAME=Administrator
SEED_ADMIN_EMAIL=admin@cleo.local

5
.gitignore vendored

@ -43,3 +43,8 @@ next-env.d.ts
# kredensial DB aplikasi PHP lama
/old-source/db_login.inc.php
# drizzle-kit pull side outputs; only db/amigo/schema.ts is kept
/db/amigo/*.sql
/db/amigo/meta/
/db/amigo/relations.ts

@ -16,15 +16,40 @@ PHP di `old-source/`. Kode baru ditulis ulang dari nol; `old-source/` hanya refe
## Dua database
| Env var | Database | Isi |
| -------------------- | ------------------------- | ----------------------------------------- |
| `DATABASE_URL` | Postgres (Docker lokal) | Milik aplikasi ini: user, session, auth. |
| `AMIGO_DATABASE_URL` | MariaDB 10.4 `cleo` (LAN) | Data bisnis lama: barang, transaksi, dll. |
| Env var | Database | Isi |
| -------------------- | -------------------------------- | ----------------------------------------- |
| `DATABASE_URL` | Postgres (Docker lokal) | Milik aplikasi ini: user, session, auth. |
| `AMIGO_DATABASE_URL` | MariaDB 10.4 `cleo` | Data bisnis lama: barang, transaksi, dll. |
- Development memakai **salinan** `cleo` di Docker lokal (`localhost:3307`, user `cleo_next`),
hasil `mysqldump` dari server LAN `192.168.3.3`. Server LAN adalah produksi yang dipakai toko;
development tidak pernah menulis ke sana. Restore salinan lokal = dump + import ulang.
- Postgres dikelola penuh oleh Drizzle migration (`db:generate` lalu `db:migrate`).
- MariaDB `cleo` masih dipakai aplikasi PHP lama. **Jangan pernah** `generate`, `migrate`, atau
`push` ke database ini. Schema Drizzle-nya hanya boleh berasal dari `drizzle-kit pull`
(introspeksi), dengan config terpisah.
`push` ke database ini. Schema Drizzle-nya hanya boleh berasal dari `pnpm db:amigo:pull`
(config `drizzle-amigo.config.ts`), jangan diedit manual. Script itu menjalankan
`scripts/fix-amigo-schema.ts` yang memperbaiki bug introspeksi drizzle-kit di MariaDB
(default ber-kutip, primary key hilang, import kurang). Koneksi: `amigoDb` dari `@/db/amigo`.
- Kolom tanggal berisi `0000-00-00` (tanggal nol, diizinkan `sql_mode` server). Schema memakai
`date({ mode: 'string' })`, jadi nilai itu sampai sebagai string; anggap sebagai "kosong",
jangan diubah ke `Date` (hasilnya `Invalid Date`).
- Aplikasi ini melakukan CRUD ke MariaDB (data master, barang, transaksi), jadi user DB-nya
butuh `SELECT`, `INSERT`, `UPDATE`, `DELETE`, tapi tanpa hak DDL (`CREATE`, `ALTER`, `DROP`).
- UI tidak meniru aplikasi lama; fitur dan aturan bisnisnya yang ditiru, tampilannya dirombak.
- Selama transisi, aplikasi PHP dan aplikasi ini menulis ke MariaDB bersamaan. Penomoran
otomatis (mis. kode pelanggan) dan update stok harus aman dari bentrok.
- Hampir semua tabel bisnis `cleo` ber-engine **MyISAM**: tidak ada transaksi (rollback tidak
membatalkan apa pun) dan `SELECT ... FOR UPDATE` tidak mengunci baris. Untuk bentrok pakai
update atomik satu statement (`stok = stok - ?`) atau `LOCK TABLES`; jangan baca-hitung-tulis.
Penulisan master + detail (mis. `m_nota_tunai` + `d_nota_tunai`) bisa tersimpan setengah kalau
gagal di tengah, jadi validasi semua input dulu sebelum statement pertama. Mengubah engine ke
InnoDB adalah `ALTER`, dilarang selama PHP masih jalan.
- Database `cleo` juga berisi tabel dari proyek lain (Laravel: `users`, `sessions`, `jobs`,
`cache`, `migrations`, dll.; Better Auth: `user`, `account`, `session`, `verification`;
Prisma: `_prisma_migrations`). Bukan milik aplikasi ini dan dianggap masih dipakai aplikasi
lain; jangan disentuh dan kecualikan dari `drizzle-kit pull`. User DB produksi diberi hak per
tabel bisnis, bukan `cleo.*`, supaya tabel-tabel itu tidak terjangkau.
- Server MariaDB 10.4, bukan MySQL 8: hindari fitur yang tidak didukung (mis. sebagian fungsi
JSON dan CTE tertentu). Driver: `mysql2`.
@ -80,6 +105,10 @@ proxy.ts pengganti middleware di Next 16; hanya cek rin
`{ ok: false, error: string }`), bukan baris DB mentah.
5. Segarkan cache dengan `revalidatePath`/`updateTag` sebelum `redirect`, karena `redirect`
melempar exception dan kode sesudahnya tidak jalan.
6. Error yang tidak terduga di-log di server dengan konteks yang cukup untuk dilacak: nama
action, user id, input yang relevan (tanpa password/secret), dan error aslinya (stack ikut).
Ke UI hanya dikirim pesan bahasa Indonesia yang jelas dan bisa ditindaklanjuti user
(mis. "Kode pelanggan sudah dipakai, coba simpan ulang"), tidak pernah pesan error DB mentah.
- Form di client memakai `useActionState` untuk menampilkan error/pending dari action.
- File di `data/` diawali `import "server-only"` supaya tidak bisa ter-import ke client
component. `db/` dan `lib/auth.ts` sengaja tanpa itu karena dibaca CLI `auth` di luar Next.js
@ -127,8 +156,13 @@ pnpm db:generate buat migrasi dari db/schema.ts (Postgres)
pnpm db:migrate jalankan migrasi ke Postgres
pnpm db:studio lihat isi Postgres di https://local.drizzle.studio
pnpm db:seed buat akun admin dari SEED_ADMIN_* di .env (idempotent)
docker compose up -d nyalakan Postgres lokal (docker-compose.yml)
pnpm db:amigo:pull introspeksi MariaDB ke db/amigo/schema.ts (satu-satunya perintah drizzle-kit untuk MariaDB)
docker compose up -d nyalakan Postgres + MariaDB lokal (docker-compose.yml)
```
Postgres lokal: service di `docker-compose.yml`, container `cleo-postgres` (image `postgres:17`,
port 5432, volume `cleo-pgdata`). Setup laptop baru ada di `README.md`.
port 5432, volume `cleo-pgdata`). MariaDB lokal: container `cleo-mariadb` (image `mariadb:10.4`,
port 3307, volume `cleo-mariadb-data`). Setup laptop baru ada di `README.md`.
Query ke MariaDB dari PowerShell 5.1: jangan pakai `mysql -e "..."` (kutip ganda dibuang
PowerShell); kirim lewat pipe: `'SELECT 1' | docker exec -i cleo-mariadb sh -c 'mysql -uroot -p$MARIADB_ROOT_PASSWORD -N cleo'`.

@ -36,7 +36,8 @@ Better Auth, Postgres (data aplikasi dan login), MariaDB `cleo` (data bisnis lam
| --------------------- | --------------------------------------------------------------------- |
| `POSTGRES_PASSWORD` | Password bebas untuk Postgres lokal. |
| `DATABASE_URL` | Ganti `<POSTGRES_PASSWORD>` dengan password yang sama seperti di atas. |
| `AMIGO_DATABASE_URL` | User dan password MariaDB `cleo`. Minta ke admin. |
| `MARIADB_ROOT_PASSWORD` | Password bebas untuk `root` MariaDB lokal. Jangan pakai password server asli. |
| `AMIGO_DATABASE_URL` | Ganti `<password-cleo-next>` dengan password bebas; user ini dibuat di langkah 6. |
| `BETTER_AUTH_SECRET` | String acak, buat dengan perintah di bawah. |
| `SEED_ADMIN_PASSWORD` | Password akun admin pertama (minimal 8 karakter). |
@ -46,14 +47,18 @@ Better Auth, Postgres (data aplikasi dan login), MariaDB `cleo` (data bisnis lam
Buat `BETTER_AUTH_SECRET` baru di setiap laptop dan server, jangan disalin dari tempat lain.
3. **Nyalakan Postgres**
Password di `AMIGO_DATABASE_URL` jangan mengandung `@ : / # ? %`, atau tulis dalam bentuk
URL-encoded (mis. `@` menjadi `%40`).
3. **Nyalakan Postgres dan MariaDB**
```bash
docker compose up -d
```
Container `cleo-postgres` akan muncul di Docker Desktop. Data tersimpan di volume
`cleo-pgdata`, jadi tidak hilang walaupun container dimatikan atau dibuat ulang.
Container `cleo-postgres` dan `cleo-mariadb` akan muncul di Docker Desktop. Data tersimpan di
volume `cleo-pgdata` dan `cleo-mariadb-data`, jadi tidak hilang walaupun container dimatikan
atau dibuat ulang.
4. **Buat tabel**
@ -70,7 +75,77 @@ Better Auth, Postgres (data aplikasi dan login), MariaDB `cleo` (data bisnis lam
Username dan password diambil dari `SEED_ADMIN_*` di `.env`. Aman dijalankan berulang kali:
kalau akun sudah ada, seed dilewati.
6. **Jalankan aplikasi**
6. **Isi MariaDB lokal dengan salinan data `cleo`**
Aplikasi tidak pernah terhubung ke server asli (`192.168.3.3`) saat development. Data bisnis
disalin sekali ke MariaDB di Docker, lalu semua CRUD dicoba di salinan itu. Langkah ini butuh
jaringan kantor dan password `root` server `192.168.3.3` (minta ke admin), hanya untuk membaca.
Perintah di bawah untuk PowerShell. Jalankan blok per blok. Kalau menyalin dari file mentah
(bukan tampilan README di Git), buang spasi di awal baris: penutup `'@` di blok c dan d harus
berada di kolom pertama.
a. Dump dari server kantor ke file di dalam container (diminta password `root` server kantor;
~260 MB, beberapa menit, jangan biarkan laptop sleep):
```powershell
docker exec -it cleo-mariadb sh -c 'mysqldump -h 192.168.3.3 -u root -p --single-transaction --triggers cleo > /tmp/cleo.sql'
docker exec cleo-mariadb sh -c 'tail -n 1 /tmp/cleo.sql'
```
Baris terakhir harus `-- Dump completed on ...`. Jangan tambahkan `--routines`: tabel sistem
`mysql.proc` di server kantor rusak sejak upgrade dari 10.1 dan membuat dump berhenti di akhir,
padahal database `cleo` tidak punya stored procedure.
b. Import ke MariaDB lokal (tanpa output kalau berhasil):
```powershell
docker exec cleo-mariadb sh -c 'mysql -uroot -p$MARIADB_ROOT_PASSWORD cleo < /tmp/cleo.sql'
```
c. Buat user aplikasi `cleo_next`. Ganti `<password-cleo-next>` dengan password yang sama
seperti di `AMIGO_DATABASE_URL`:
```powershell
$sql = @'
CREATE USER IF NOT EXISTS 'cleo_next'@'%' IDENTIFIED BY '<password-cleo-next>';
GRANT LOCK TABLES ON cleo.* TO 'cleo_next'@'%';
'@
$sql | docker exec -i cleo-mariadb sh -c 'mysql -uroot -p$MARIADB_ROOT_PASSWORD'
```
d. Beri `cleo_next` hak baca/tulis data (tanpa hak mengubah struktur tabel) hanya di tabel
bisnis. Tabel milik aplikasi lain di database yang sama (Laravel, Prisma, Better Auth lama)
sengaja tidak diberi akses:
```powershell
$gen = @'
SELECT CONCAT('GRANT SELECT, INSERT, UPDATE, DELETE ON cleo.`', table_name, '` TO ''cleo_next''@''%'';')
FROM information_schema.tables
WHERE table_schema = 'cleo'
AND table_name NOT IN (
'users', 'sessions', 'jobs', 'failed_jobs', 'job_batches', 'migrations',
'cache', 'cache_locks', 'password_reset_tokens', 'akuns',
'user', 'account', 'session', 'verification',
'_prisma_migrations'
);
'@
$gen | docker exec -i cleo-mariadb sh -c 'mysql -uroot -p$MARIADB_ROOT_PASSWORD -N' | docker exec -i cleo-mariadb sh -c 'mysql -uroot -p$MARIADB_ROOT_PASSWORD'
```
e. Cek. Login sebagai `cleo_next`:
```powershell
docker exec -it cleo-mariadb mysql -ucleo_next -p cleo
```
Lalu jalankan `SELECT COUNT(*) FROM pelanggan;` (harus ada angkanya) dan
`SELECT COUNT(*) FROM users;` (harus `SELECT command denied`). Keluar dengan `exit`.
Langkah c sampai e aman diulang. `db/amigo/schema.ts` sudah ada di repo, jadi
`pnpm db:amigo:pull` tidak perlu dijalankan di laptop baru.
7. **Jalankan aplikasi**
```bash
pnpm dev
@ -81,12 +156,24 @@ Better Auth, Postgres (data aplikasi dan login), MariaDB `cleo` (data bisnis lam
### Catatan
- MariaDB `cleo` (`192.168.3.3`) hanya bisa diakses dari jaringan kantor. Fitur login tidak
membutuhkannya.
- Server MariaDB kantor (`192.168.3.3`) masih dipakai aplikasi PHP di toko. Saat development
hanya dibaca sekali untuk dump (langkah 6a); `AMIGO_DATABASE_URL` selalu menunjuk ke
`localhost:3307`. Fitur login tidak membutuhkan MariaDB.
- Mengembalikan salinan lokal ke kondisi awal (mis. setelah data uji berantakan): ulangi langkah
6a dan 6b. Untuk mulai dari nol, hapus dulu container dan volume-nya
(`docker compose rm -sf mariadb`, lalu `docker volume rm cleo-minimarket_cleo-mariadb-data`,
lalu `docker compose up -d mariadb`), kemudian ulangi langkah 6a sampai 6e. Jangan pakai
`docker compose down -v`: perintah itu juga menghapus data Postgres.
- `MARIADB_ROOT_PASSWORD` hanya dibaca saat volume MariaDB masih kosong. Mengubahnya di `.env`
setelah itu tidak berpengaruh, kecuali volume dihapus seperti di atas.
- Di PowerShell 5.1 jangan kirim query lewat `mysql -e "..."`: tanda kutip ganda dibuang oleh
PowerShell. Kirim lewat pipe:
`'SELECT 1' | docker exec -i cleo-mariadb sh -c 'mysql -uroot -p$MARIADB_ROOT_PASSWORD -N cleo'`.
- Pendaftaran akun lewat web sengaja ditutup (`disableSignUp` di `lib/auth.ts`). Akun pertama
dibuat lewat `pnpm db:seed`.
- Kalau port 5432 sudah dipakai Postgres lain di laptop itu, matikan dulu Postgres tersebut, atau
ganti port kiri di `docker-compose.yml` (mis. `"5433:5432"`) beserta port di `DATABASE_URL`.
MariaDB lokal sengaja di port 3307 supaya tidak bentrok dengan XAMPP/MySQL di 3306.
## Perintah
@ -96,8 +183,9 @@ Better Auth, Postgres (data aplikasi dan login), MariaDB `cleo` (data bisnis lam
| `pnpm build` | Build production |
| `pnpm lint` | ESLint |
| `pnpm exec tsc --noEmit` | Type-check |
| `docker compose up -d` | Nyalakan Postgres lokal |
| `docker compose up -d` | Nyalakan Postgres dan MariaDB lokal |
| `pnpm db:generate` | Buat file migrasi dari `db/schema.ts` |
| `pnpm db:migrate` | Jalankan migrasi ke Postgres |
| `pnpm db:seed` | Buat akun admin pertama |
| `pnpm db:studio` | Lihat isi Postgres di https://local.drizzle.studio |
| `pnpm db:amigo:pull` | Baca ulang struktur MariaDB ke `db/amigo/schema.ts` (hanya kalau struktur tabel berubah) |

@ -0,0 +1,7 @@
import { drizzle } from "drizzle-orm/mysql2";
import * as schema from "./schema";
export const amigoDb = drizzle(process.env.AMIGO_DATABASE_URL!, {
schema,
mode: "default",
});

File diff suppressed because it is too large Load Diff

@ -12,7 +12,25 @@ services:
volumes:
- cleo-pgdata:/var/lib/postgresql/data
mariadb:
# Same major version as the LAN server; MySQL 8 features are not available there.
image: mariadb:10.4
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_general_ci
container_name: cleo-mariadb
restart: unless-stopped
environment:
MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD:?set MARIADB_ROOT_PASSWORD in .env}
MARIADB_DATABASE: cleo
ports:
# 3307 so it does not clash with a local MySQL/XAMPP on 3306.
- "3307:3306"
volumes:
- cleo-mariadb-data:/var/lib/mysql
volumes:
cleo-pgdata:
# Fixed name so the volume created earlier by `docker run -v cleo-pgdata:...` is reused.
name: cleo-pgdata
cleo-mariadb-data:

@ -0,0 +1,16 @@
import "dotenv/config";
import { defineConfig } from "drizzle-kit";
// Introspection only: never run generate/migrate/push with this config.
// The PHP app still owns the schema; cleo_next has no DDL rights anyway.
export default defineConfig({
dialect: "mysql",
schema: "./db/amigo/schema.ts",
out: "./db/amigo",
dbCredentials: {
url: process.env.AMIGO_DATABASE_URL!,
},
introspect: {
casing: "preserve",
},
});

@ -14,6 +14,8 @@ const eslintConfig = defineConfig([
"next-env.d.ts",
// Legacy PHP app, kept only as reference.
"old-source/**",
// Regenerated by `pnpm db:amigo:pull`, always empty (MyISAM has no foreign keys).
"db/amigo/relations.ts",
]),
]);

@ -10,7 +10,8 @@
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio",
"db:seed": "tsx scripts/seed.ts"
"db:seed": "tsx scripts/seed.ts",
"db:amigo:pull": "drizzle-kit pull --config=drizzle-amigo.config.ts && tsx scripts/fix-amigo-schema.ts"
},
"dependencies": {
"@base-ui/react": "^1.9.0",
@ -19,6 +20,7 @@
"cn": "^0.5.0",
"drizzle-orm": "^0.45.4",
"lucide-react": "^1.59.0",
"mysql2": "^3.24.6",
"next": "16.4.2",
"pg": "^8.23.1",
"react": "19.3.0",

@ -13,7 +13,7 @@ importers:
version: 1.9.0(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
better-auth:
specifier: ^1.7.11
version: 1.7.11(drizzle-kit@0.31.11)(drizzle-orm@0.45.4(@types/pg@8.23.1)(kysely@0.29.7)(pg@8.23.1))(next@16.4.2(@babel/core@7.29.7)(@types/node@20.19.43)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(pg@8.23.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
version: 1.7.11(drizzle-kit@0.31.11)(drizzle-orm@0.45.4(@types/pg@8.23.1)(kysely@0.29.7)(mysql2@3.24.6(@types/node@20.19.43))(pg@8.23.1))(mysql2@3.24.6(@types/node@20.19.43))(next@16.4.2(@babel/core@7.29.7)(@types/node@20.19.43)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(pg@8.23.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@ -22,10 +22,13 @@ importers:
version: 0.5.0
drizzle-orm:
specifier: ^0.45.4
version: 0.45.4(@types/pg@8.23.1)(kysely@0.29.7)(pg@8.23.1)
version: 0.45.4(@types/pg@8.23.1)(kysely@0.29.7)(mysql2@3.24.6(@types/node@20.19.43))(pg@8.23.1)
lucide-react:
specifier: ^1.59.0
version: 1.59.0(@types/react@19.3.0)(react@19.3.0)
mysql2:
specifier: ^3.24.6
version: 3.24.6(@types/node@20.19.43)
next:
specifier: 16.4.2
version: 16.4.2(@babel/core@7.29.7)(@types/node@20.19.43)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
@ -1595,6 +1598,10 @@ packages:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'}
aws-ssl-profiles@1.1.2:
resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==}
engines: {node: '>= 6.0.0'}
axe-core@4.14.0:
resolution: {integrity: sha512-9WTZxEjsZ7b13TH8JPmbV2z8CHbl80/2hm3XPEG4JgNdQLK81IBRXmSxHfMAOkSqQeRxT/0dwNDz2GOm3zzpcQ==}
engines: {node: '>=4'}
@ -2385,6 +2392,9 @@ packages:
fuzzysort@3.1.0:
resolution: {integrity: sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==}
generate-function@2.3.1:
resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==}
generator-function@2.0.1:
resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==}
engines: {node: '>= 0.4'}
@ -2644,6 +2654,9 @@ packages:
is-promise@4.0.0:
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
is-property@1.0.2:
resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==}
is-regex@1.2.1:
resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
engines: {node: '>= 0.4'}
@ -2890,6 +2903,9 @@ packages:
resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==}
engines: {node: '>=18'}
long@5.3.2:
resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
@ -2897,6 +2913,10 @@ packages:
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
lru.min@1.1.5:
resolution: {integrity: sha512-5J9ysMYUpYIg9RF2vJpy9SinEmSviFSe0GyPpCQ4L5QSkLAgeLXlTAOu2ZwWUU5m+0SBl6gUU1R1ZQB3aKypfA==}
engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'}
lucide-react@1.59.0:
resolution: {integrity: sha512-5qgMyyCPU6OvnKpLS2t693+ZtiqA8oaSweWoUows0BLFKFf6V75m4+CptpmohZHpD5IuOVIBXHBBiv2vpLJmtw==}
peerDependencies:
@ -2965,6 +2985,16 @@ packages:
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
mysql2@3.24.6:
resolution: {integrity: sha512-enu73KhbAua2L3dIxjc6qg6ZJ7H3pt1BImZASWNWY5xSsmJ1ZuUPULTDGXgmJbI61vb+nJAG19IZclIq19UidQ==}
engines: {node: '>= 8.0'}
peerDependencies:
'@types/node': '>= 8'
named-placeholders@1.1.6:
resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==}
engines: {node: '>=8.0.0'}
nanoid@3.3.20:
resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@ -3463,6 +3493,10 @@ packages:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'}
sql-escaper@1.5.2:
resolution: {integrity: sha512-6CKD38c31SENivxOADeMNLdukOnUxUcflKtzVWzace7Riv1v7cAEym5Cx9Q7gYZ3ezIJI7ZpqecQq8cqNeSSFg==}
engines: {bun: '>=1.0.0', deno: '>=2.0.0', node: '>=12.0.0'}
stable-hash@0.0.5:
resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==}
@ -3996,12 +4030,12 @@ snapshots:
nanostores: 1.5.5
zod: 4.6.7
'@better-auth/drizzle-adapter@1.7.11(@better-auth/core@1.7.11(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.7))(jose@6.2.12)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0)(drizzle-orm@0.45.4(@types/pg@8.23.1)(kysely@0.29.7)(pg@8.23.1))':
'@better-auth/drizzle-adapter@1.7.11(@better-auth/core@1.7.11(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.7))(jose@6.2.12)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0)(drizzle-orm@0.45.4(@types/pg@8.23.1)(kysely@0.29.7)(mysql2@3.24.6(@types/node@20.19.43))(pg@8.23.1))':
dependencies:
'@better-auth/core': 1.7.11(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.7))(jose@6.2.12)(kysely@0.29.7)(nanostores@1.5.5)
'@better-auth/utils': 0.5.0
optionalDependencies:
drizzle-orm: 0.45.4(@types/pg@8.23.1)(kysely@0.29.7)(pg@8.23.1)
drizzle-orm: 0.45.4(@types/pg@8.23.1)(kysely@0.29.7)(mysql2@3.24.6(@types/node@20.19.43))(pg@8.23.1)
'@better-auth/kysely-adapter@1.7.11(@better-auth/core@1.7.11(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.7))(jose@6.2.12)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0)(kysely@0.29.7)':
dependencies:
@ -5041,6 +5075,8 @@ snapshots:
dependencies:
possible-typed-array-names: 1.1.0
aws-ssl-profiles@1.1.2: {}
axe-core@4.14.0: {}
axobject-query@4.1.0: {}
@ -5051,10 +5087,10 @@ snapshots:
baseline-browser-mapping@2.11.29: {}
better-auth@1.7.11(drizzle-kit@0.31.11)(drizzle-orm@0.45.4(@types/pg@8.23.1)(kysely@0.29.7)(pg@8.23.1))(next@16.4.2(@babel/core@7.29.7)(@types/node@20.19.43)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(pg@8.23.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0):
better-auth@1.7.11(drizzle-kit@0.31.11)(drizzle-orm@0.45.4(@types/pg@8.23.1)(kysely@0.29.7)(mysql2@3.24.6(@types/node@20.19.43))(pg@8.23.1))(mysql2@3.24.6(@types/node@20.19.43))(next@16.4.2(@babel/core@7.29.7)(@types/node@20.19.43)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(pg@8.23.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0):
dependencies:
'@better-auth/core': 1.7.11(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.7))(jose@6.2.12)(kysely@0.29.7)(nanostores@1.5.5)
'@better-auth/drizzle-adapter': 1.7.11(@better-auth/core@1.7.11(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.7))(jose@6.2.12)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0)(drizzle-orm@0.45.4(@types/pg@8.23.1)(kysely@0.29.7)(pg@8.23.1))
'@better-auth/drizzle-adapter': 1.7.11(@better-auth/core@1.7.11(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.7))(jose@6.2.12)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0)(drizzle-orm@0.45.4(@types/pg@8.23.1)(kysely@0.29.7)(mysql2@3.24.6(@types/node@20.19.43))(pg@8.23.1))
'@better-auth/kysely-adapter': 1.7.11(@better-auth/core@1.7.11(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.7))(jose@6.2.12)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0)(kysely@0.29.7)
'@better-auth/memory-adapter': 1.7.11(@better-auth/core@1.7.11(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.7))(jose@6.2.12)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0)
'@better-auth/mongo-adapter': 1.7.11(@better-auth/core@1.7.11(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.7))(jose@6.2.12)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0)
@ -5072,7 +5108,8 @@ snapshots:
zod: 4.6.7
optionalDependencies:
drizzle-kit: 0.31.11
drizzle-orm: 0.45.4(@types/pg@8.23.1)(kysely@0.29.7)(pg@8.23.1)
drizzle-orm: 0.45.4(@types/pg@8.23.1)(kysely@0.29.7)(mysql2@3.24.6(@types/node@20.19.43))(pg@8.23.1)
mysql2: 3.24.6(@types/node@20.19.43)
next: 16.4.2(@babel/core@7.29.7)(@types/node@20.19.43)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
pg: 8.23.1
react: 19.3.0
@ -5320,10 +5357,11 @@ snapshots:
esbuild: 0.25.12
tsx: 4.23.15
drizzle-orm@0.45.4(@types/pg@8.23.1)(kysely@0.29.7)(pg@8.23.1):
drizzle-orm@0.45.4(@types/pg@8.23.1)(kysely@0.29.7)(mysql2@3.24.6(@types/node@20.19.43))(pg@8.23.1):
optionalDependencies:
'@types/pg': 8.23.1
kysely: 0.29.7
mysql2: 3.24.6(@types/node@20.19.43)
pg: 8.23.1
dunder-proto@1.0.1:
@ -5943,6 +5981,10 @@ snapshots:
fuzzysort@3.1.0: {}
generate-function@2.3.1:
dependencies:
is-property: 1.0.2
generator-function@2.0.1: {}
gensync@1.0.0-beta.2: {}
@ -6173,6 +6215,8 @@ snapshots:
is-promise@4.0.0: {}
is-property@1.0.2: {}
is-regex@1.2.1:
dependencies:
call-bound: 1.0.4
@ -6374,6 +6418,8 @@ snapshots:
chalk: 5.6.2
is-unicode-supported: 1.3.0
long@5.3.2: {}
loose-envify@1.4.0:
dependencies:
js-tokens: 4.0.0
@ -6382,6 +6428,8 @@ snapshots:
dependencies:
yallist: 3.1.1
lru.min@1.1.5: {}
lucide-react@1.59.0(@types/react@19.3.0)(react@19.3.0):
dependencies:
react: 19.3.0
@ -6431,6 +6479,21 @@ snapshots:
ms@2.1.3: {}
mysql2@3.24.6(@types/node@20.19.43):
dependencies:
'@types/node': 20.19.43
aws-ssl-profiles: 1.1.2
generate-function: 2.3.1
iconv-lite: 0.7.3
long: 5.3.2
lru.min: 1.1.5
named-placeholders: 1.1.6
sql-escaper: 1.5.2
named-placeholders@1.1.6:
dependencies:
lru.min: 1.1.5
nanoid@3.3.20: {}
nanostores@1.5.5: {}
@ -7029,6 +7092,8 @@ snapshots:
split2@4.2.0: {}
sql-escaper@1.5.2: {}
stable-hash@0.0.5: {}
statuses@2.0.2: {}

@ -0,0 +1,133 @@
import "dotenv/config";
import { readFile, writeFile } from "node:fs/promises";
import mysql, { type RowDataPacket } from "mysql2/promise";
// drizzle-kit 0.31 introspects MariaDB as if it were MySQL, which breaks three things:
// - MariaDB returns COLUMN_DEFAULT quoted ('abc', and 'NULL' for no default), so defaults come
// out as .default('NULL') or broken literals like '''Z'''.
// - drizzle-kit reads the primary key rows as row["TABLE_NAME"], but MariaDB returns lowercase
// keys, so every primary key is dropped.
// - The import line misses some column builders (bigint, tinyint).
// This script rewrites db/amigo/schema.ts from information_schema after each pull.
const SCHEMA_PATH = "db/amigo/schema.ts";
// `.default(...)` at the end of a column line, optionally followed by `.notNull()`/`.primaryKey()`.
const DEFAULT_CALL =
/\.default\(.*?\)(?=(?:\.notNull\(\))?(?:\.primaryKey\(\))?,$)/;
type ColumnRow = RowDataPacket & {
tableName: string;
columnName: string;
columnDefault: string | null;
isPrimary: number;
};
function fixDefault(line: string, rawDefault: string | null): string {
if (rawDefault === null || rawDefault === "NULL") {
return line.replace(DEFAULT_CALL, "");
}
if (rawDefault.startsWith("'") && rawDefault.endsWith("'")) {
const value = rawDefault.slice(1, -1).replaceAll("''", "'");
return line.replace(DEFAULT_CALL, `.default(${JSON.stringify(value)})`);
}
// Numbers and expressions like current_timestamp() are already generated correctly.
return line;
}
function addPrimaryKey(line: string): string {
if (line.includes(".primaryKey()")) return line;
return line.replace(/,$/, ".primaryKey(),");
}
// Rebuilds the import lines from the builders the schema actually calls.
function rebuildImports(schema: string): string {
const used = new Set(["mysqlTable"]);
for (const match of schema.matchAll(/^\t\w+: (\w+)\(/gm)) used.add(match[1]);
for (const match of schema.matchAll(/^\t(index|uniqueIndex|primaryKey)\(/gm))
used.add(match[1]);
const body = schema.replace(/^import .*\n/gm, "");
const imports = [
`import { ${[...used].join(", ")} } from "drizzle-orm/mysql-core";`,
];
if (body.includes("sql`")) imports.push('import { sql } from "drizzle-orm";');
return `${imports.join("\n")}\n${body}`;
}
async function main() {
const url = process.env.AMIGO_DATABASE_URL;
if (!url) throw new Error("AMIGO_DATABASE_URL is not set in .env");
const connection = await mysql.createConnection(url);
const [rows] = await connection.query<ColumnRow[]>(
`SELECT c.table_name AS tableName,
c.column_name AS columnName,
c.column_default AS columnDefault,
c.column_key = 'PRI' AS isPrimary
FROM information_schema.columns c
WHERE c.table_schema = DATABASE()
ORDER BY c.table_name, c.ordinal_position`,
);
await connection.end();
const tables = new Map<string, ColumnRow[]>();
for (const row of rows) {
const columns = tables.get(row.tableName) ?? [];
columns.push(row);
tables.set(row.tableName, columns);
}
let schema = await readFile(SCHEMA_PATH, "utf8");
for (const [tableName, columns] of tables) {
const start = schema.indexOf(`mysqlTable("${tableName}", {`);
if (start === -1)
throw new Error(`Table ${tableName} not found in ${SCHEMA_PATH}`);
const next = schema.indexOf("\nexport const ", start);
const end = next === -1 ? schema.length : next;
let block = schema.slice(start, end);
const primaryColumns = columns
.filter((c) => c.isPrimary)
.map((c) => c.columnName);
for (const column of columns) {
const linePattern = new RegExp(`^\\t${column.columnName}: .*,$`, "m");
const line = block.match(linePattern)?.[0];
if (!line)
throw new Error(`Column ${tableName}.${column.columnName} not found`);
let fixed = fixDefault(line, column.columnDefault);
if (primaryColumns.length === 1 && column.isPrimary)
fixed = addPrimaryKey(fixed);
block = block.replace(line, () => fixed);
}
if (primaryColumns.length > 1 && !block.includes("primaryKey({")) {
if (!block.includes("(table) => [\n")) {
throw new Error(
`Composite key on ${tableName} needs a (table) => [...] block`,
);
}
const columnsList = primaryColumns
.map((name) => `table.${name}`)
.join(", ");
block = block.replace(
"(table) => [\n",
`(table) => [\n\tprimaryKey({ columns: [${columnsList}] }),\n`,
);
}
schema = schema.slice(0, start) + block + schema.slice(end);
}
await writeFile(SCHEMA_PATH, rebuildImports(schema));
console.log(
`Fixed defaults and primary keys for ${tables.size} tables in ${SCHEMA_PATH}`,
);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});
Loading…
Cancel
Save