#!/usr/bin/env node /** * app.db şemasını drizzle/ altındaki migration'larla eşitler — konteyner * açılışında (docker-entrypoint.sh) node server.js'den ÖNCE koşar, lokalde * `pnpm db:migrate` ile çalıştırılır. Idempotenttir. * * Neden drizzle-orm'un kendi migrate()'i değil: standalone çıktıda drizzle-orm * Turbopack'çe bundle'landığı için /app/node_modules'ta paket olarak yok; * @libsql/client ise serverExternalPackages'ta olduğundan her zaman çözülür. * Bu script drizzle migrator protokolünü birebir taklit eder (__drizzle_migrations * tablosu, sha256 hash, created_at = journal.when) — ileride resmi migrate()'e * geçiş kayıtları bozmaz. * * Benimseme (adoption): migration akışından önce doldurulmuş prod volume'lerinde * __drizzle_migrations tablosu yoktur ama şema (kısmen) vardır. Bu durumda * baseline (0000) bir kez additive uygulanır — CREATE'ler IF NOT EXISTS'e * çevrilir, eksik kolonlar snapshot'tan ALTER TABLE ile eklenir — ve baseline * uygulanmış olarak damgalanır. Sonraki migration'lar (0001+) normal akışla gelir. */ import { createClient } from "@libsql/client"; import { createHash } from "node:crypto"; import { readFileSync } from "node:fs"; import { join } from "node:path"; import process from "node:process"; const GOC_KLASORU = process.env.DRIZZLE_DIR ?? join(process.cwd(), "drizzle"); const DB_URL = process.env.APP_DB_URL ?? "file:./data/app.db"; const GOC_TABLOSU_DDL = `CREATE TABLE IF NOT EXISTS "__drizzle_migrations" ( id SERIAL PRIMARY KEY, hash text NOT NULL, created_at numeric )`; /** Journal'daki her girdiyi { when, tag, hash, ifadeler[] } olarak yükler. */ function gocleriOku() { const journal = JSON.parse( readFileSync(join(GOC_KLASORU, "meta", "_journal.json"), "utf8"), ); return journal.entries.map((girdi) => { const sql = readFileSync(join(GOC_KLASORU, `${girdi.tag}.sql`), "utf8"); return { when: girdi.when, tag: girdi.tag, hash: createHash("sha256").update(sql).digest("hex"), ifadeler: sql .split("--> statement-breakpoint") .map((s) => s.trim()) .filter(Boolean), }; }); } async function tabloVar(db, ad) { const r = await db.execute({ sql: "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", args: [ad], }); return r.rows.length > 0; } /** Snapshot'taki default değerini SQL literaline çevirir (string zaten tırnaklı gelir). */ function sqlVarsayilan(deger) { return typeof deger === "string" ? deger : String(deger); } /** * Benimseme adımı: baseline'ı additive uygular. * 1) CREATE TABLE/INDEX ifadeleri IF NOT EXISTS ile koşulur (eksik tablolar gelir). * 2) Mevcut tablolarda eksik kolonlar baseline snapshot'ından ALTER TABLE ile eklenir. */ async function baselineIleUzlastir(db, baseline) { for (const ifade of baseline.ifadeler) { const korunmus = ifade .replace(/^CREATE TABLE `/, "CREATE TABLE IF NOT EXISTS `") .replace(/^CREATE INDEX `/, "CREATE INDEX IF NOT EXISTS `") .replace(/^CREATE UNIQUE INDEX `/, "CREATE UNIQUE INDEX IF NOT EXISTS `"); await db.execute(korunmus); } const snapshot = JSON.parse( readFileSync(join(GOC_KLASORU, "meta", "0000_snapshot.json"), "utf8"), ); for (const tablo of Object.values(snapshot.tables)) { const mevcut = await db.execute(`PRAGMA table_info(\`${tablo.name}\`)`); const mevcutKolonlar = new Set(mevcut.rows.map((r) => r.name)); for (const kolon of Object.values(tablo.columns)) { if (mevcutKolonlar.has(kolon.name)) continue; let ddl = `ALTER TABLE \`${tablo.name}\` ADD COLUMN \`${kolon.name}\` ${kolon.type}`; if (kolon.default !== undefined) { ddl += ` DEFAULT ${sqlVarsayilan(kolon.default)}`; if (kolon.notNull) ddl += " NOT NULL"; } else if (kolon.notNull) { // Dolu tabloya default'suz NOT NULL kolon eklenemez (SQLite kısıtı); // nullable ekleyip uyarıyoruz — uygulama zaten değeri hep yazıyor. console.warn( `[db-goc] uyarı: ${tablo.name}.${kolon.name} NOT NULL ama default'suz — nullable eklendi`, ); } await db.execute(ddl); console.log(`[db-goc] kolon eklendi: ${tablo.name}.${kolon.name}`); } } } async function main() { const gocler = gocleriOku(); const db = createClient({ url: DB_URL, authToken: process.env.APP_DB_AUTH_TOKEN, }); try { // Benimseme: şema var ama migration kaydı yok → baseline'ı uzlaştır + damgala. const kayitVar = await tabloVar(db, "__drizzle_migrations"); if (!kayitVar && (await tabloVar(db, "user"))) { console.log("[db-goc] migration kaydı yok, mevcut şema benimseniyor..."); await baselineIleUzlastir(db, gocler[0]); await db.execute(GOC_TABLOSU_DDL); await db.execute({ sql: 'INSERT INTO "__drizzle_migrations" (hash, created_at) VALUES (?, ?)', args: [gocler[0].hash, gocler[0].when], }); console.log(`[db-goc] baseline damgalandı: ${gocler[0].tag}`); } // Normal akış: son uygulanan migration'dan sonrakileri sırayla uygula. await db.execute(GOC_TABLOSU_DDL); const son = await db.execute( 'SELECT created_at FROM "__drizzle_migrations" ORDER BY created_at DESC LIMIT 1', ); const sonZaman = son.rows.length ? Number(son.rows[0].created_at) : 0; for (const goc of gocler) { if (goc.when <= sonZaman) continue; await db.batch( [ ...goc.ifadeler, { sql: 'INSERT INTO "__drizzle_migrations" (hash, created_at) VALUES (?, ?)', args: [goc.hash, goc.when], }, ], "write", ); console.log(`[db-goc] uygulandı: ${goc.tag}`); } console.log("[db-goc] şema güncel"); } finally { db.close(); } } main().catch((hata) => { console.error("[db-goc] migration başarısız:", hata); process.exit(1); });