Some checks failed
Deploy / deploy (push) Failing after 13s
Volume'deki app.db yalnızca ilk kurulumda seed'den kopyalanıyordu; sonraki deploy'larda şemaya eklenen kolonlar (son örnek: kredi_bitti_at, kredi_hatirlatma_gonderildi_at) prod'a hiç ulaşmıyor ve 'no such column' hatalarıyla kırılıyordu. - drizzle-kit generate ile baseline migration üretildi (drizzle/0000_baslangic) - scripts/db-goc.mjs: entrypoint'te server'dan önce koşan migration runner; drizzle migrator protokolünü birebir taklit eder (__drizzle_migrations, sha256, created_at=journal.when) ama yalnızca @libsql/client'a dayanır — drizzle-orm standalone imajda bundle'landığından paket olarak çözülemiyor - Benimseme: migration kaydı olmayan mevcut volume'lerde baseline additive uzlaştırılır (IF NOT EXISTS + snapshot'tan eksik kolon ALTER'ları) ve damgalanır; sonraki migration'lar normal akışla uygulanır - sema-guvence.ts kaldırıldı: elle DDL aynalama yerini migration'lara bıraktı - Yeni akış: şema değişikliğinde pnpm db:generate ile migration commit'lenir Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
158 lines
5.8 KiB
JavaScript
158 lines
5.8 KiB
JavaScript
#!/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);
|
||
});
|