diff --git a/.env.example b/.env.example index d8b9186..cba64af 100644 --- a/.env.example +++ b/.env.example @@ -4,8 +4,9 @@ POSTGRES_PASSWORD= DATABASE_URL="postgresql://postgres:@localhost:5432/cleo_auth" -# MariaDB `cleo` from the legacy app (office LAN only) -AMIGO_DATABASE_URL="mysql://:@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:@localhost:3307/cleo" +MARIADB_ROOT_PASSWORD="" # 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 + diff --git a/.gitignore b/.gitignore index 5cd8a43..8570a09 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,9 @@ yarn-error.log* next-env.d.ts # kredensial DB aplikasi PHP lama -/old-source/db_login.inc.php \ No newline at end of file +/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 \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 0f213c3..ecc6694 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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'`. diff --git a/README.md b/README.md index 9504761..da65461 100644 --- a/README.md +++ b/README.md @@ -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 `` 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 `` 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 `` dengan password yang sama + seperti di `AMIGO_DATABASE_URL`: + + ```powershell + $sql = @' + CREATE USER IF NOT EXISTS 'cleo_next'@'%' IDENTIFIED BY ''; + 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) | diff --git a/db/amigo/index.ts b/db/amigo/index.ts new file mode 100644 index 0000000..7d1d1bb --- /dev/null +++ b/db/amigo/index.ts @@ -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", +}); diff --git a/db/amigo/schema.ts b/db/amigo/schema.ts new file mode 100644 index 0000000..8c4b1f6 --- /dev/null +++ b/db/amigo/schema.ts @@ -0,0 +1,1049 @@ +import { mysqlTable, char, varchar, int, date, datetime, bigint, float, mysqlEnum, text, tinyint, decimal, double, index, primaryKey } from "drizzle-orm/mysql-core"; + +export const barang = mysqlTable("barang", { + kode_barang: char({ length: 9 }), + kode_lama: varchar({ length: 15 }).notNull(), + nama_barang: varchar({ length: 50 }).notNull(), + kode_toko: char({ length: 2 }).notNull(), + kelompok: char({ length: 2 }), + tahun: int(), + bulan: int(), + id_gabungan: char({ length: 13 }).notNull().primaryKey(), + kode_strip: char({ length: 2 }), + kode_supplier: char({ length: 6 }), + ked_produk: char({ length: 1 }), + kode_merk: char({ length: 4 }), +}, +(table) => [ + index("kode_strip").on(table.kode_strip), + index("kode_supplier").on(table.kode_supplier), + index("ked_produk").on(table.ked_produk), + index("kode_lama").on(table.kode_lama), + index("kode_barang").on(table.kode_barang), + index("kode_merk").on(table.kode_merk), +]); + +export const bayar = mysqlTable("bayar", { + IDbayar: int().autoincrement().notNull().primaryKey(), + // you can use { mode: 'date' }, if you want to have Date as type for this column + periode_awal: date({ mode: 'string' }).notNull(), + // you can use { mode: 'date' }, if you want to have Date as type for this column + periode_akhir: date({ mode: 'string' }).notNull(), + tgl_bayar: datetime({ mode: 'string'}).notNull(), + jenis_bayar: varchar({ length: 10 }).notNull(), + // you can use { mode: 'date' }, if you want to have Date as type for this column + tgl_cair: date({ mode: 'string' }), + no_giro: varchar({ length: 20 }), + kd_supplier: varchar({ length: 6 }).notNull(), + diskon: int(), + penyesuaian: int(), + materai: int(), + total_bayar: int().notNull(), + ket_bayar: varchar({ length: 255 }).notNull(), +}); + +export const bayar_bon = mysqlTable("bayar_bon", { + id: int().autoincrement().notNull().primaryKey(), + no_bon: char({ length: 15 }), + jumlah_bon: int().notNull(), + bayar_bon: bigint({ mode: "number" }).notNull(), + // you can use { mode: 'date' }, if you want to have Date as type for this column + tgl_byr: date({ mode: 'string' }), + kd_plg: char({ length: 11 }), + kd_kasir: varchar({ length: 8 }), +}, +(table) => [ + index("kd_plg").on(table.kd_plg), +]); + +export const department = mysqlTable("department", { + id_department: char({ length: 2 }).notNull().primaryKey(), + department: varchar({ length: 50 }).notNull(), +}); + +export const diskon = mysqlTable("diskon", { + id: int().autoincrement().notNull().primaryKey(), + nama_diskon: varchar({ length: 50 }), + nilai_diskon: float(), +}); + +export const d_bon = mysqlTable("d_bon", { + no_bon: char({ length: 15 }), + kd_brg: char({ length: 19 }), + jumlah: int().notNull(), + nomor: char({ length: 3 }).notNull(), + harga_pokok: varchar({ length: 30 }).notNull(), + hrg_satuan: bigint({ mode: "number" }).notNull(), + diskon: float(), + kd_pram: varchar({ length: 8 }), + status: char({ length: 1 }), + bonus: float(), + id: int().autoincrement().notNull().primaryKey(), +}, +(table) => [ + index("no").on(table.no_bon), + index("kd_brg").on(table.kd_brg), +]); + +export const d_nota_bawa = mysqlTable("d_nota_bawa", { + id: bigint({ mode: "number" }).autoincrement().notNull().primaryKey(), + no_nota: char({ length: 15 }), + kd_barang: char({ length: 19 }), + no_barang: char({ length: 4 }), + jumlah: int().default(0), + harga_satuan: bigint({ mode: "number" }), + status_barang: mysqlEnum(['T','B','C']), + kode_pram: varchar({ length: 8 }), +}, +(table) => [ + index("no_nota").on(table.no_nota), + index("kd_barang").on(table.kd_barang), + index("no_brg").on(table.no_barang), +]); + +export const d_nota_retur = mysqlTable("d_nota_retur", { + id: int().autoincrement().notNull().primaryKey(), + no_nota: char({ length: 15 }), + kd_barang: char({ length: 19 }), + no_barang: char({ length: 4 }), + jumlah: int().default(0), + diskon: float(), + harga_jual: bigint({ mode: "number" }), + harga_pokok: varchar({ length: 30 }), + kode_pram: varchar({ length: 8 }), + no_nota_tunai: varchar({ length: 15 }).notNull(), + bonus: float(), +}, +(table) => [ + index("kd_barang").on(table.kd_barang), + index("no_nota").on(table.no_nota), + index("no_brg").on(table.no_barang), +]); + +export const d_nota_tunai = mysqlTable("d_nota_tunai", { + id: int().autoincrement().notNull().primaryKey(), + no_nota: char({ length: 13 }), + kd_barang: char({ length: 19 }), + no_barang: char({ length: 4 }).notNull(), + jumlah: int().default(0).notNull(), + jenis_diskon: varchar({ length: 50 }).notNull(), + diskon: float(), + harga_jual: bigint({ mode: "number" }).notNull(), + harga_pokok: varchar({ length: 30 }), + kode_pram: varchar({ length: 8 }), + bonus: float(), +}, +(table) => [ + index("kd_barang").on(table.kd_barang), + index("no_nota").on(table.no_nota), + index("no_brg").on(table.no_barang), +]); + +export const d_order = mysqlTable("d_order", { + id: int().autoincrement().notNull().primaryKey(), + no_order: char({ length: 10 }), + kode_strip: char({ length: 2 }), + artikel: varchar({ length: 100 }).notNull(), + nomor: char({ length: 4 }).notNull(), + jumlah: int().notNull(), + harga_order: bigint({ mode: "number" }).notNull(), + id_rencana: int().default(0), +}, +(table) => [ + index("idr").on(table.id_rencana), + index("no_order").on(table.no_order), + index("kd_strip").on(table.kode_strip), +]); + +export const d_pembelian = mysqlTable("d_pembelian", { + id: int().autoincrement().notNull().primaryKey(), + no_faktur: char({ length: 30 }), + kode_barang: char({ length: 9 }), + nomor: char({ length: 4 }), + harga_satuan: bigint({ mode: "number" }).notNull(), + harga_pokok: varchar({ length: 30 }).notNull(), + diskon: float().notNull(), + diskonrp: bigint({ mode: "number" }).notNull(), + ppn: float().notNull(), + profit: float().notNull(), + kode_asli: varchar({ length: 25 }), + jum_fisik: int().notNull(), + lokasi: char({ length: 2 }).default("1A").notNull(), + hj_final: bigint({ mode: "number" }).notNull(), + nm_brg: varchar({ length: 100 }).notNull(), + // you can use { mode: 'date' }, if you want to have Date as type for this column + exp_date: date({ mode: 'string' }), +}, +(table) => [ + index("no_faktur").on(table.no_faktur), + index("kode_barang").on(table.kode_barang), + index("nm").on(table.nm_brg), +]); + +export const d_rencana_beli = mysqlTable("d_rencana_beli", { + id: int(), + kode_supplier: char({ length: 6 }), + // you can use { mode: 'date' }, if you want to have Date as type for this column + tgl_rencana: date({ mode: 'string' }).notNull(), + vol_rencana: int().notNull(), + rupiah_rencana: bigint({ mode: "number" }).notNull(), + // you can use { mode: 'date' }, if you want to have Date as type for this column + tgl_revisi: date({ mode: 'string' }), + vol_revisi: int(), + rupiah_revisi: bigint({ mode: "number" }), + idd: int().autoincrement().notNull().primaryKey(), + artikel_rencana: varchar({ length: 100 }).notNull(), + nomor_rencana: char({ length: 4 }).notNull(), + artikel_revisi: varchar({ length: 100 }).notNull(), + nomor_revisi: char({ length: 4 }).notNull(), + keterangan: varchar({ length: 100 }).notNull(), + status: varchar({ length: 10 }).default("ok").notNull(), +}, +(table) => [ + index("id").on(table.id), +]); + +export const d_retur_beli = mysqlTable("d_retur_beli", { + id: int().autoincrement().notNull().primaryKey(), + id_retur: int(), + kd_barang: char({ length: 19 }), + jumlah: int().notNull(), + harga_satuan: bigint({ mode: "number" }).notNull(), + nomor_barang: char({ length: 4 }).notNull(), +}, +(table) => [ + index("kd_barang").on(table.kd_barang), + index("no_brg").on(table.nomor_barang), + index("id").on(table.id_retur), +]); + +export const d_stock_opname = mysqlTable("d_stock_opname", { + no_nota: varchar({ length: 15 }).default("").notNull(), + kode_barang: varchar({ length: 13 }).default("").notNull(), + nomor: varchar({ length: 4 }).default("").notNull(), + jumlah_asli: int(), + jumlah_stok_sebelum: int(), + keterangan: varchar({ length: 100 }), +}, +(table) => [ + primaryKey({ columns: [table.no_nota, table.kode_barang, table.nomor] }), + index("kdbrg").on(table.kode_barang), +]); + +export const d_transfer = mysqlTable("d_transfer", { + id_transfer: int(), + kode_barang_nota: varchar({ length: 15 }).notNull(), + jumlah: int().default(0).notNull(), + harga_satuan: bigint({ mode: "number" }).notNull(), + nomor: char({ length: 4 }).notNull(), + kd_supplier: char({ length: 6 }), + no_nota: varchar({ length: 20 }).notNull(), + id: int().autoincrement().notNull().primaryKey(), +}); + +export const d_transfer_kembali = mysqlTable("d_transfer_kembali", { + id_transfer: int(), + kode_barang_nota: varchar({ length: 15 }).notNull(), + jumlah: int().default(0).notNull(), + harga_satuan: bigint({ mode: "number" }).notNull(), + nomor: char({ length: 4 }).notNull(), + kd_supplier: char({ length: 6 }), + no_nota: varchar({ length: 20 }).notNull(), + id: int().autoincrement().notNull().primaryKey(), +}); + +export const karyawan = mysqlTable("karyawan", { + nik: varchar({ length: 8 }).notNull().primaryKey(), + nama: varchar({ length: 100 }).notNull(), + username: varchar({ length: 20 }).notNull(), + password: varchar({ length: 100 }).notNull(), + id_pekerjaan: varchar({ length: 50 }).notNull(), + id_department: char({ length: 2 }).notNull(), + last_login: datetime({ mode: 'string'}).notNull(), +}); + +export const kas_keluar = mysqlTable("kas_keluar", { + id: int().autoincrement().notNull().primaryKey(), + tanggal: datetime({ mode: 'string'}).notNull(), + // you can use { mode: 'date' }, if you want to have Date as type for this column + tgl_transaksi: date({ mode: 'string' }).notNull(), + pic: varchar({ length: 20 }), + bank: varchar({ length: 8 }), + project: varchar({ length: 100 }), + detail_project: varchar({ length: 100 }), + kodeakun: varchar({ length: 8 }), + keterangan: varchar({ length: 200 }), + nominal: int().notNull(), +}, +(table) => [ + index("kodeakun").on(table.kodeakun), + index("tanggal").on(table.tanggal), +]); + +export const kas_masuk = mysqlTable("kas_masuk", { + id: int().autoincrement().notNull().primaryKey(), + tanggal: datetime({ mode: 'string'}).notNull(), + // you can use { mode: 'date' }, if you want to have Date as type for this column + tgl_transaksi: date({ mode: 'string' }).notNull(), + pic: varchar({ length: 20 }), + bank: varchar({ length: 8 }), + project: varchar({ length: 100 }), + detail_project: varchar({ length: 100 }), + kodeakun: varchar({ length: 8 }), + keterangan: varchar({ length: 200 }), + nominal: int().notNull(), +}, +(table) => [ + index("kodeakun").on(table.kodeakun), + index("tanggal").on(table.tanggal), +]); + +export const kedalaman_produk = mysqlTable("kedalaman_produk", { + kode_ked_produk: char({ length: 1 }).notNull(), + ked_produk: varchar({ length: 50 }).notNull(), + kode_strip: char({ length: 2 }), + id: int().autoincrement().notNull().primaryKey(), +}, +(table) => [ + index("kode_strip").on(table.kode_strip), + index("kd_kedproduk").on(table.kode_ked_produk), +]); + +export const komisi = mysqlTable("komisi", { + id: int().autoincrement().notNull().primaryKey(), + jumlah: int(), + // you can use { mode: 'date' }, if you want to have Date as type for this column + tgl_ambil: date({ mode: 'string' }), + kd_plg: char({ length: 11 }), + kd_kasir: char({ length: 8 }), +}); + +export const lokasi = mysqlTable("lokasi", { + kd_lokasi: char({ length: 5 }).notNull().primaryKey(), + dusun: varchar({ length: 50 }), + kelurahan: varchar({ length: 50 }), + kecamatan: varchar({ length: 50 }), + kabupaten: varchar({ length: 50 }), + propinsi: varchar({ length: 50 }), +}); + +export const lokasi_barang = mysqlTable("lokasi_barang", { + kode_lokasi: char({ length: 2 }).notNull(), + lokasi: varchar({ length: 20 }).notNull(), + id: int().autoincrement().notNull().primaryKey(), +}); + +export const merk = mysqlTable("merk", { + kode_merk: char({ length: 4 }).notNull().primaryKey(), + merk: varchar({ length: 50 }), + id_merk: char({ length: 2 }).notNull(), + kode_lama: char({ length: 3 }), +}); + +export const m_bon = mysqlTable("m_bon", { + no_bon: char({ length: 15 }).default("").notNull().primaryKey(), + tgl_bon: datetime({ mode: 'string'}), + kode_plg: char({ length: 11 }), + diskon: int().default(0), + status: char({ length: 1 }), + kasir: varchar({ length: 30 }), + jumlah_total: bigint({ mode: "number" }), + status_bayar: mysqlEnum(['T','K']), + bank: varchar({ length: 50 }), + no_kartu: char({ length: 20 }), + no_validasi_bank: varchar({ length: 20 }), +}, +(table) => [ + index("kd_plg").on(table.kode_plg), + index("tgl").on(table.tgl_bon), +]); + +export const m_nota_bawa = mysqlTable("m_nota_bawa", { + no_nota: char({ length: 15 }).notNull().primaryKey(), + nota_lama: varchar({ length: 6 }), + tgl_nota: datetime({ mode: 'string'}), + kd_customer: char({ length: 11 }), + status: char({ length: 1 }), + jumlah_total: bigint({ mode: "number" }), + kasir: varchar({ length: 30 }), + status_nota: mysqlEnum(['S','T']), + // you can use { mode: 'date' }, if you want to have Date as type for this column + tgl_kembali: date({ mode: 'string' }), +}); + +export const m_nota_retur = mysqlTable("m_nota_retur", { + no_nota: char({ length: 15 }).notNull().primaryKey(), + tgl_nota: datetime({ mode: 'string'}), + kd_customer: char({ length: 11 }), + jumlah_total: bigint({ mode: "number" }), + kasir: varchar({ length: 30 }), + total_diskon: bigint({ mode: "number" }), + keterangan: text(), + status_nota: mysqlEnum(['T','K','D','B']).default("D"), +}); + +export const m_nota_tunai = mysqlTable("m_nota_tunai", { + no_nota: char({ length: 13 }).notNull().primaryKey(), + nota_lama: varchar({ length: 6 }), + tgl_nota: datetime({ mode: 'string'}), + kd_customer: char({ length: 11 }), + jumlah_total: bigint({ mode: "number" }), + kasir: varchar({ length: 30 }).notNull(), + total_uang: bigint({ mode: "number" }).notNull(), + total_diskon: bigint({ mode: "number" }).notNull(), + status_bayar: mysqlEnum(['T','K']), + bank: varchar({ length: 50 }), + no_kartu: char({ length: 20 }), + no_validasi_bank: varchar({ length: 20 }), + status_nota: mysqlEnum(['S','T']), + no_nota_retur: char({ length: 15 }), + jumlah_total_retur: bigint({ mode: "number" }), +}, +(table) => [ + index("tgl_nota").on(table.tgl_nota), + index("statusnota").on(table.status_nota), +]); + +export const m_order = mysqlTable("m_order", { + no_order: char({ length: 10 }).notNull().primaryKey(), + // you can use { mode: 'date' }, if you want to have Date as type for this column + tgl_order: date({ mode: 'string' }).notNull(), + kd_supplier: char({ length: 6 }), + // you can use { mode: 'date' }, if you want to have Date as type for this column + rencana_datang: date({ mode: 'string' }).notNull(), + jenis_pembayaran: char({ length: 1 }), + rp_order: bigint({ mode: "number" }), + total_order: bigint({ mode: "number" }), + status_order: char({ length: 1 }).notNull(), +}); + +export const m_pembelian = mysqlTable("m_pembelian", { + no_faktur: varchar({ length: 30 }).notNull().primaryKey(), + // you can use { mode: 'date' }, if you want to have Date as type for this column + tgl_nota: date({ mode: 'string' }).notNull(), + // you can use { mode: 'date' }, if you want to have Date as type for this column + tgl_masuk: date({ mode: 'string' }).notNull(), + no_order: char({ length: 10 }), + // you can use { mode: 'date' }, if you want to have Date as type for this column + tgl_jatuh_tempo: date({ mode: 'string' }).notNull(), + kd_supplier: char({ length: 6 }), + status: char({ length: 1 }), + diskon: float(), + diskon_pembelian: bigint({ mode: "number" }), + ppn: float(), + nik: char({ length: 8 }).notNull(), + rp_beli: bigint({ mode: "number" }).notNull(), + total_beli: int().notNull(), + validasi: char({ length: 1 }), + keterangan: varchar({ length: 100 }), + tgl_entry: datetime({ mode: 'string'}).notNull(), + referensi: varchar({ length: 20 }), + status_revisi: char({ length: 1 }).default("1").notNull(), + status_bayar: varchar({ length: 1 }).default("0"), + IDbayar: int(), +}, +(table) => [ + index("kd_supp").on(table.kd_supplier), + index("tgl_masuk").on(table.tgl_masuk), +]); + +export const m_rencana_beli = mysqlTable("m_rencana_beli", { + id: int().autoincrement().notNull().primaryKey(), + // you can use { mode: 'date' }, if you want to have Date as type for this column + tgl_rencana_beli: date({ mode: 'string' }).notNull(), + jml_rencana: int().notNull(), + status_rencana: char({ length: 1 }), + kode_strip: char({ length: 2 }), + status_revisi: char({ length: 1 }), + bulanR: char({ length: 2 }).notNull(), + tahunR: char({ length: 2 }).notNull(), +}); + +export const m_retur_beli = mysqlTable("m_retur_beli", { + id_retur: int().autoincrement().notNull().primaryKey(), + no_nota: char({ length: 20 }), + // you can use { mode: 'date' }, if you want to have Date as type for this column + tgl_retur: date({ mode: 'string' }), + keterangan: varchar({ length: 100 }), + total_retur: bigint({ mode: "number" }).notNull(), + nik: char({ length: 8 }).notNull(), + validasi: char({ length: 1 }).default("0").notNull(), + alasan: varchar({ length: 255 }).notNull(), + kd_supplier: char({ length: 6 }), + jml_item: int(), + status_bayar: varchar({ length: 1 }).default("0"), + IDbayar: int(), +}, +(table) => [ + index("tgl").on(table.tgl_retur), + index("no_nota").on(table.no_nota), +]); + +export const m_stock_opname = mysqlTable("m_stock_opname", { + id: int().autoincrement().notNull().primaryKey(), + no_nota: varchar({ length: 15 }), + // you can use { mode: 'date' }, if you want to have Date as type for this column + tanggal: date({ mode: 'string' }), + pelaku: varchar({ length: 100 }), + keterangan: text(), + konfirmasi: tinyint().notNull(), + strip: varchar({ length: 3 }).notNull(), +}); + +export const m_transfer = mysqlTable("m_transfer", { + id_transfer: int().autoincrement().notNull().primaryKey(), + // you can use { mode: 'date' }, if you want to have Date as type for this column + tgl_transfer: date({ mode: 'string' }).notNull(), + toko_asal: varchar({ length: 50 }).notNull(), + toko_tujuan: varchar({ length: 50 }).notNull(), + diskon: int().notNull(), + validasi: char({ length: 1 }).default("0").notNull(), + total_transfer: bigint({ mode: "number" }).notNull(), + jml_item: int().notNull(), + faktur_oper: char({ length: 20 }).notNull(), +}); + +export const m_transfer_kembali = mysqlTable("m_transfer_kembali", { + id_transfer: int().autoincrement().notNull().primaryKey(), + // you can use { mode: 'date' }, if you want to have Date as type for this column + tgl_transfer: date({ mode: 'string' }).notNull(), + // you can use { mode: 'date' }, if you want to have Date as type for this column + tgl_kembali: date({ mode: 'string' }).notNull(), + toko_asal: varchar({ length: 50 }).notNull(), + toko_tujuan: varchar({ length: 50 }).notNull(), + diskon: int().notNull(), + validasi: char({ length: 1 }).default("0").notNull(), + total_transfer: bigint({ mode: "number" }).notNull(), + jml_item: int().notNull(), + faktur_oper: char({ length: 20 }).notNull(), +}); + +export const nolpromo = mysqlTable("nolpromo", { + id: int().autoincrement().notNull().primaryKey(), + kode_barang: char({ length: 19 }).notNull(), + nomor: char({ length: 4 }).notNull(), + diskon: int().notNull(), + // you can use { mode: 'date' }, if you want to have Date as type for this column + awal_periode: date({ mode: 'string' }).notNull(), + // you can use { mode: 'date' }, if you want to have Date as type for this column + akhir_periode: date({ mode: 'string' }).notNull(), + stokcatat: int().notNull(), +}, +(table) => [ + index("kd_barang").on(table.kode_barang), + index("nomor").on(table.nomor), +]); + +export const nomor_urut = mysqlTable("nomor_urut", { + id: int().notNull().primaryKey(), + urut: char({ length: 3 }).notNull(), +}, +(table) => [ + index("urut").on(table.urut), +]); + +export const no_barang = mysqlTable("no_barang", { + id: int().autoincrement().notNull().primaryKey(), + id_gabungan: char({ length: 13 }).notNull(), + nomor: char({ length: 4 }).notNull(), + harga_pokok: varchar({ length: 30 }).notNull(), + harga_jual: int().notNull(), + harga_jual2: int().notNull(), + isi_max2: int().notNull(), + harga_jual3: int().notNull(), + isi_max3: int().notNull(), + satuan: varchar({ length: 15 }).notNull(), + stok_min: int().notNull(), + stok: int().default(0).notNull(), +}, +(table) => [ + index("id_gabungan").on(table.id_gabungan), + index("nomor").on(table.nomor), +]); + +export const pelanggan = mysqlTable("pelanggan", { + kd_customer: char({ length: 11 }).notNull().primaryKey(), + alias: char({ length: 11 }).notNull(), + ktp: varchar({ length: 16 }).notNull(), + // you can use { mode: 'date' }, if you want to have Date as type for this column + member: date({ mode: 'string' }).notNull(), + nama: varchar({ length: 100 }).notNull(), + alamat: varchar({ length: 100 }).default("tidak tahu").notNull(), + tempat_lahir: varchar({ length: 50 }), + tgl_lahir: varchar({ length: 15 }), + jns_kelamin: char({ length: 1 }), + agama: char({ length: 1 }), + kd_lokasi: char({ length: 5 }), + wilayah: varchar({ length: 200 }), + pekerjaan: char({ length: 2 }), + status_nikah: char({ length: 1 }), + telp: varchar({ length: 15 }), + ponsel: varchar({ length: 15 }), + email: varchar({ length: 30 }), + status: char({ length: 1 }), + upline: char({ length: 11 }), + bts_bon: int().default(0), + nilai_bon: int().default(0), + bts_bon_istimewa: int().default(0), + nilai_komisi: int().default(0), + komisi_diambil: int().default(0), + jenis_customer: char({ length: 1 }).default("Z"), + kode_customer_lama: varchar({ length: 11 }), + tgl_entry: varchar({ length: 15 }), + poin: int().default(0), +}, +(table) => [ + index("kdcust").on(table.kd_customer, table.kode_customer_lama), + index("kd_cust").on(table.kd_customer), + index("upline").on(table.upline), +]); + +export const penyesuaian = mysqlTable("penyesuaian", { + id: int().autoincrement().notNull().primaryKey(), + diskon: int(), + pembulatan: int(), + materai: int(), + IDbayar: int(), +}); + +export const rak = mysqlTable("rak", { + idRak: bigint({ mode: "number" }).autoincrement().notNull().primaryKey(), + namaRak: varchar({ length: 30 }), +}); + +export const retur_bon_lama = mysqlTable("retur_bon_lama", { + id: int().autoincrement().notNull().primaryKey(), + no_nota: varchar({ length: 15 }).notNull(), + // you can use { mode: 'date' }, if you want to have Date as type for this column + tgl_retur: date({ mode: 'string' }).notNull(), + kd_customer: varchar({ length: 11 }).notNull(), + kd_barang: varchar({ length: 15 }).notNull(), + nomor: varchar({ length: 3 }).notNull(), + harga_pokok: varchar({ length: 30 }).notNull(), + harga_jual: int().notNull(), + keterangan: text().notNull(), + no_nota_bon: varchar({ length: 15 }).notNull(), +}); + +export const satuan_barang = mysqlTable("satuan_barang", { + idSatuanBarang: int().autoincrement().notNull().primaryKey(), + namaSatuanBarang: varchar({ length: 30 }), +}); + +export const setup = mysqlTable("setup", { + parameter: varchar({ length: 20 }).notNull().primaryKey(), + nilai: varchar({ length: 250 }), +}); + +export const setupPromo = mysqlTable("setupPromo", { + id: int().autoincrement().notNull().primaryKey(), + kode_barang: char({ length: 19 }).notNull(), + nomor: char({ length: 4 }).notNull(), + diskon: int().notNull(), + // you can use { mode: 'date' }, if you want to have Date as type for this column + awal_periode: date({ mode: 'string' }).notNull(), + // you can use { mode: 'date' }, if you want to have Date as type for this column + akhir_periode: date({ mode: 'string' }).notNull(), + stokcatat: int().notNull(), +}, +(table) => [ + index("kdbarang").on(table.kode_barang), + index("nomor").on(table.nomor), +]); + +export const sisapromo = mysqlTable("sisapromo", { + id: int().autoincrement().notNull().primaryKey(), + kode_barang: char({ length: 19 }).notNull(), + nomor: char({ length: 4 }).notNull(), + diskon: int().notNull(), + // you can use { mode: 'date' }, if you want to have Date as type for this column + awal_periode: date({ mode: 'string' }).notNull(), + // you can use { mode: 'date' }, if you want to have Date as type for this column + akhir_periode: date({ mode: 'string' }).notNull(), + stokcatat: int().notNull(), +}, +(table) => [ + index("kd_barang").on(table.kode_barang), + index("nomor").on(table.nomor), +]); + +export const strip = mysqlTable("strip", { + kd_strip: char({ length: 2 }).notNull().primaryKey(), + nama_strip: varchar({ length: 50 }).notNull(), + murah1: int().notNull(), + murah2: int().notNull(), + sedang1: int().notNull(), + sedang2: int().notNull(), + mahal1: int().notNull(), + mahal2: int().notNull(), + benik_murah: int().notNull(), + benik_sedang: int().notNull(), + benik_mahal: int().notNull(), + kel_brg: char({ length: 1 }), + kel_jns: char({ length: 1 }).notNull(), + persen_tiwul: float().default(0.025).notNull(), +}); + +export const sub_kategori = mysqlTable("sub_kategori", { + kode_sub: char({ length: 1 }).notNull().primaryKey(), + keterangan: varchar({ length: 20 }).notNull(), +}); + +export const supplier = mysqlTable("supplier", { + kd_supplier: char({ length: 6 }).notNull().primaryKey(), + nama_supplier: varchar({ length: 50 }).notNull(), + alamat1: varchar({ length: 100 }).notNull(), + alamat2: varchar({ length: 100 }), + nama_rekening: varchar({ length: 50 }).notNull(), + rekening1: varchar({ length: 30 }).notNull(), + rekening2: varchar({ length: 30 }), + bank1: varchar({ length: 30 }).notNull(), + bank2: varchar({ length: 30 }), + telp: varchar({ length: 50 }).notNull(), + ponsel: varchar({ length: 15 }).notNull(), + email: varchar({ length: 30 }).notNull(), + fax: varchar({ length: 50 }).notNull(), + jns_pembayaran: char({ length: 1 }).notNull(), + lama_tempo: int().notNull(), +}); + +export const supp_barang = mysqlTable("supp_barang", { + id: int().autoincrement().notNull().primaryKey(), + kd_supplier: char({ length: 6 }), + kd_strip: char({ length: 2 }), + ranking: int().notNull(), +}, +(table) => [ + index("kd_supplier").on(table.kd_supplier), + index("kd_strip").on(table.kd_strip), +]); + +export const tabelbenik = mysqlTable("tabelbenik", { + kode_pram: varchar({ length: 8 }), + jvbenik: decimal({ precision: 55, scale: 0 }), + jrpbenik: double(), + jbenik: double(), + id: int().autoincrement().notNull().primaryKey(), +}); + +export const tabelbonus = mysqlTable("tabelbonus", { + nik: varchar({ length: 8 }).notNull(), + nama: varchar({ length: 100 }).notNull(), + lbrnota: decimal({ precision: 41, scale: 0 }), + jvbenik: decimal({ precision: 55, scale: 0 }), + jrpbenik: double(), + jbenik: double(), + jvthiwul: decimal({ precision: 55, scale: 0 }), + jrpthiwul: double(), + jthiwul: double(), + id: int().autoincrement().notNull().primaryKey(), +}); + +export const tabelthiwul = mysqlTable("tabelthiwul", { + kode_pram: varchar({ length: 8 }), + jvthiwul: decimal({ precision: 55, scale: 0 }), + jrpthiwul: double(), + jthiwul: double(), + id: int().autoincrement().notNull().primaryKey(), +}); + +export const tbl_lmbrnota = mysqlTable("tbl_lmbrnota", { + kode_pram: varchar({ length: 8 }), + lbrnota: decimal({ precision: 41, scale: 0 }), + id: int().autoincrement().notNull().primaryKey(), +}); + +export const tempAK7 = mysqlTable("tempAK7", { + id: int().autoincrement().notNull().primaryKey(), + // you can use { mode: 'date' }, if you want to have Date as type for this column + tanggal: date({ mode: 'string' }).notNull(), + stockawal: bigint({ mode: "number" }), + pembelian: bigint({ mode: "number" }), + revisi: bigint({ mode: "number" }), + retur: bigint({ mode: "number" }), + transfer: bigint({ mode: "number" }), + transferkembali: bigint({ mode: "number" }), + penjualan: bigint({ mode: "number" }), + bawa: bigint({ mode: "number" }), + kembalibawa: bigint({ mode: "number" }), + stockopname: bigint({ mode: "number" }), + stockminus: bigint({ mode: "number" }), + stockakhir: bigint({ mode: "number" }), + keterangan: varchar({ length: 100 }), +}); + +export const tempAK7rinci = mysqlTable("tempAK7rinci", { + id: int().autoincrement().notNull().primaryKey(), + // you can use { mode: 'date' }, if you want to have Date as type for this column + tanggal: date({ mode: 'string' }).notNull(), + lini: char({ length: 2 }), + stockawal: bigint({ mode: "number" }), + pembelian: bigint({ mode: "number" }), + revisi: bigint({ mode: "number" }), + retur: bigint({ mode: "number" }), + transfer: bigint({ mode: "number" }), + transferkembali: bigint({ mode: "number" }), + penjualan: bigint({ mode: "number" }), + bawa: bigint({ mode: "number" }), + kembalibawa: bigint({ mode: "number" }), + stockopname: bigint({ mode: "number" }), + stockminus: bigint({ mode: "number" }), + stockakhir: bigint({ mode: "number" }), +}); + +export const tempplg = mysqlTable("tempplg", { + n1: varchar({ length: 11 }), + n2: char({ length: 32 }), + n3: int(), + n4: int(), + n5: int(), + n6: int(), + n7: int(), + n8: int(), + id: int().autoincrement().notNull().primaryKey(), +}); + +export const tempstockopname = mysqlTable("tempstockopname", { + kode_barang: varchar({ length: 13 }), + nomor: varchar({ length: 4 }), + id: int().autoincrement().notNull().primaryKey(), +}); + +export const temp_d_nota_tunai = mysqlTable("temp_d_nota_tunai", { + id: int().autoincrement().notNull().primaryKey(), + no_nota: char({ length: 13 }), + toko: char({ length: 2 }).notNull(), + kd_barang: char({ length: 19 }), + no_barang: char({ length: 4 }).notNull(), + jumlah: int().default(1).notNull(), + diskon: float(), + harga_jual: bigint({ mode: "number" }).notNull(), + harga_jual_fix: bigint({ mode: "number" }).notNull(), + harga_pokok: varchar({ length: 30 }), +}, +(table) => [ + index("kd_barang").on(table.kd_barang), + index("no_nota").on(table.no_nota), + index("no_brg").on(table.no_barang), +]); + +export const temp_nota_plg = mysqlTable("temp_nota_plg", { + id: int().autoincrement().notNull().primaryKey(), + kd_customer: char({ length: 11 }), + no_nota: char({ length: 13 }), +}, +(table) => [ + index("kd_customer").on(table.kd_customer), +]); + +export const temp_no_nota = mysqlTable("temp_no_nota", { + username: varchar({ length: 30 }).default("").notNull().primaryKey(), + tunai: char({ length: 13 }), + retur: char({ length: 15 }), + bawa: char({ length: 15 }), + bon: char({ length: 15 }), +}); + +export const temp_order = mysqlTable("temp_order", { + no_order: char({ length: 10 }).notNull().primaryKey(), +}); + +export const temp_sisaob = mysqlTable("temp_sisaob", { + kode_barang: varchar({ length: 13 }), + nomor: varchar({ length: 4 }), + id: int().autoincrement().notNull().primaryKey(), +}, +(table) => [ + index("kode_barang").on(table.kode_barang), +]); + +export const tlaporan1 = mysqlTable("tlaporan1", { + kel_jns: char({ length: 1 }).notNull(), + kode_strip1: char({ length: 2 }), + kode_supplier1: char({ length: 6 }), + nama_supplier1: varchar({ length: 50 }).notNull(), + jml1: decimal({ precision: 32, scale: 0 }), + rp1: decimal({ precision: 41, scale: 0 }), + id: int().autoincrement().notNull().primaryKey(), +}); + +export const tlaporan2 = mysqlTable("tlaporan2", { + kel_jns: char({ length: 1 }).notNull(), + kode_strip2: char({ length: 2 }), + kode_supplier2: char({ length: 6 }), + nama_supplier2: varchar({ length: 50 }).notNull(), + jml2: decimal({ precision: 32, scale: 0 }), + rp2: decimal({ precision: 41, scale: 0 }), + id: int().autoincrement().notNull().primaryKey(), +}); + +export const tlaporan3 = mysqlTable("tlaporan3", { + kel_jns: char({ length: 1 }).notNull(), + kode_strip3: varchar({ length: 2 }), + kode_supplier3: char({ length: 6 }), + nama_supplier3: varchar({ length: 50 }).notNull(), + jml3: decimal({ precision: 32, scale: 0 }), + rp3: decimal({ precision: 41, scale: 0 }), + id: int().autoincrement().notNull().primaryKey(), +}); + +export const tlaporan4 = mysqlTable("tlaporan4", { + kel_jns: char({ length: 1 }).default("").notNull(), + kd_strip: char({ length: 2 }).notNull(), + kode_strip1: char({ length: 2 }), + kode_supplier1: char({ length: 6 }), + nama_supplier1: varchar({ length: 50 }), + jml1: decimal({ precision: 32, scale: 0 }), + rp1: decimal({ precision: 41, scale: 0 }), + kode_strip2: char({ length: 2 }), + kode_supplier2: char({ length: 6 }), + nama_supplier2: varchar({ length: 50 }), + jml2: decimal({ precision: 32, scale: 0 }), + rp2: decimal({ precision: 41, scale: 0 }), + id: int().autoincrement().notNull().primaryKey(), +}); + +export const tlaporan5 = mysqlTable("tlaporan5", { + kel_jns: char({ length: 1 }).default("").notNull(), + kd_strip: char({ length: 2 }).notNull(), + kode_strip1: char({ length: 2 }), + kode_supplier1: char({ length: 6 }), + nama_supplier1: varchar({ length: 50 }), + jml1: decimal({ precision: 32, scale: 0 }), + rp1: decimal({ precision: 41, scale: 0 }), + kode_strip2: char({ length: 2 }), + kode_supplier2: char({ length: 6 }), + nama_supplier2: varchar({ length: 50 }), + jml2: decimal({ precision: 32, scale: 0 }), + rp2: decimal({ precision: 41, scale: 0 }), + kode_strip3: varchar({ length: 2 }), + kode_supplier3: char({ length: 6 }), + nama_supplier3: varchar({ length: 50 }), + jml3: decimal({ precision: 32, scale: 0 }), + rp3: decimal({ precision: 41, scale: 0 }), + id: int().autoincrement().notNull().primaryKey(), +}); + +export const tmp_lapdetailjual = mysqlTable("tmp_lapdetailjual", { + total_tunai: bigint({ mode: "number" }).notNull(), + total_omset: bigint({ mode: "number" }).notNull(), + andil: decimal({ precision: 10, scale: 2 }).notNull(), + id: int().autoincrement().notNull().primaryKey(), +}); + +export const tmp_lapjualperkat = mysqlTable("tmp_lapjualperkat", { + kel_jns: char({ length: 1 }).notNull(), + lini: varchar({ length: 2 }), + vol: decimal({ precision: 55, scale: 0 }), + HJbruto: decimal({ precision: 65, scale: 0 }), + Disc: double(), + totHJ: double(), + totHP: double(), + nettprofit: double(), + id: int().autoincrement().notNull().primaryKey(), +}); + +export const tro = mysqlTable("tro", { + artikelRencana: varchar({ length: 100 }), + ukurRencana: char({ length: 4 }), + volRencana: int(), + rupiahRencana: bigint({ mode: "number" }), + artikelOrder: varchar({ length: 100 }), + ukurOrder: char({ length: 4 }), + volOrder: int(), + rupiahOrder: bigint({ mode: "number" }), + noOrder: char({ length: 10 }), + id: int().autoincrement().notNull().primaryKey(), +}, +(table) => [ + index("id4").on(table.noOrder, table.artikelOrder, table.ukurOrder), +]); + +export const vdetailretur = mysqlTable("vdetailretur", { + id_retur: int(), + kd_barang: char({ length: 19 }), + jumlah: int().notNull(), + harga_satuan: bigint({ mode: "number" }).notNull(), + nomor_barang: char({ length: 4 }).notNull(), + harga_jual: int().notNull(), + id: int().autoincrement().notNull().primaryKey(), +}, +(table) => [ + index("idx").on(table.id_retur, table.kd_barang), +]); + +export const vdetailtransfer = mysqlTable("vdetailtransfer", { + id_transfer: int(), + kode_barang_nota: varchar({ length: 15 }).notNull(), + jumlah: int().default(0).notNull(), + harga_satuan: bigint({ mode: "number" }).notNull(), + nomor: char({ length: 4 }).notNull(), + harga_jual: int().notNull(), + id: int().autoincrement().notNull().primaryKey(), +}, +(table) => [ + index("idx").on(table.id_transfer, table.kode_barang_nota), +]); + +export const vlap = mysqlTable("vlap", { + kodeStripSkg: char({ length: 2 }), + supplierSkg: varchar({ length: 50 }), + merkSkg: varchar({ length: 50 }), + kodeStripLalu: char({ length: 2 }), + supplierLalu: varchar({ length: 50 }), + merkLalu: varchar({ length: 50 }), + unitTotalSkg: decimal({ precision: 55, scale: 0 }), + unitTotalLalu: decimal({ precision: 55, scale: 0 }), + rupiahTotalSkg: double(), + rupiahTotalLalu: double(), + HPSkg: double(), + HPLalu: double(), + id: int().autoincrement().notNull().primaryKey(), +}); + +export const vlaporan = mysqlTable("vlaporan", { + kd_supplier: char({ length: 6 }), + nama_supplier: varchar({ length: 50 }).notNull(), + kode_barang: char({ length: 9 }), + nm_brg: varchar({ length: 100 }).notNull(), + jmlbeli: decimal({ precision: 32, scale: 0 }), + jmljual: decimal({ precision: 55, scale: 0 }), + stokakh: decimal({ precision: 32, scale: 0 }), + hj: int().notNull(), + id: int().autoincrement().notNull().primaryKey(), +}, +(table) => [ + index("brg_supp").on(table.kd_supplier, table.kode_barang), +]); + +export const vlaporan1 = mysqlTable("vlaporan1", { + kd_supplier: char({ length: 6 }), + nama_supplier: varchar({ length: 50 }).notNull(), + kode_barang: char({ length: 9 }), + nm_brg: varchar({ length: 100 }).notNull(), + jmlbeli: decimal({ precision: 32, scale: 0 }), + id: int().autoincrement().notNull().primaryKey(), +}, +(table) => [ + index("brg_supp").on(table.kd_supplier, table.kode_barang), +]); + +export const vlaporan2 = mysqlTable("vlaporan2", { + komposisi: varchar({ length: 7 }).default("").notNull(), + kode_barang: char({ length: 9 }), + stokakh: decimal({ precision: 32, scale: 0 }), + hj: int().notNull(), + id: int().autoincrement().notNull().primaryKey(), +}, +(table) => [ + index("kdbrg").on(table.kode_barang), +]); + +export const vlaporan3 = mysqlTable("vlaporan3", { + kd_barang: char({ length: 19 }), + jmljual: decimal({ precision: 55, scale: 0 }), + id: int().autoincrement().notNull().primaryKey(), +}, +(table) => [ + index("kdbrg").on(table.kd_barang), +]); diff --git a/docker-compose.yml b/docker-compose.yml index 5731ed6..03910d6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/drizzle-amigo.config.ts b/drizzle-amigo.config.ts new file mode 100644 index 0000000..b10ca42 --- /dev/null +++ b/drizzle-amigo.config.ts @@ -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", + }, +}); diff --git a/eslint.config.mjs b/eslint.config.mjs index f30973c..e62f6a5 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -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", ]), ]); diff --git a/package.json b/package.json index 0c20816..3521b65 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 78d2659..c41faa9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {} diff --git a/scripts/fix-amigo-schema.ts b/scripts/fix-amigo-schema.ts new file mode 100644 index 0000000..481a6f0 --- /dev/null +++ b/scripts/fix-amigo-schema.ts @@ -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( + `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(); + 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); +});