import { config } from "dotenv"; import { sql } from "drizzle-orm"; import { drizzle } from "drizzle-orm/postgres-js"; import { migrate } from "drizzle-orm/postgres-js/migrator"; import postgres from "postgres"; import { readFileSync, readdirSync } from "node:fs"; import { join } from "node:path"; config({ path: ".env.local", }); const providerBaseUrlDefaults = [ { type: "alibaba", baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1" }, { type: "openai", baseUrl: "https://api.openai.com/v1" }, { type: "huanke-gemini", baseUrl: "https://api.huandutech.com/v1" }, { type: "qianwen-video", baseUrl: "https://dashscope.aliyuncs.com/api/v1/services/aigc/video-generation", }, { type: "doubao-video", baseUrl: "https://ark.cn-beijing.volces.com/api/v3" }, { type: "ruxa-video", baseUrl: "https://api.ruxa.ai" }, { type: "volcano-vision", baseUrl: "https://ark.cn-beijing.volces.com" }, { type: "volcano-video", baseUrl: "https://ark.cn-beijing.volces.com/api/v3" }, { type: "alibaba-image", baseUrl: "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation", }, { type: "bfl-image", baseUrl: "https://api.bfl.ai" }, { type: "ruxa-image", baseUrl: "https://api.ruxa.ai" }, { type: "fal-image", baseUrl: "https://fal.run" }, { type: "openai-image", baseUrl: "https://api.openai.com/v1" }, { type: "midjourney", baseUrl: "https://your-midjourney-api.example.com" }, { type: "fal-video", baseUrl: "https://fal.run" }, { type: "tripo-3d", baseUrl: "https://api.tripo3d.ai/v2/openapi" }, { type: "worldlabs-3d", baseUrl: "https://api.worldlabs.ai" }, { type: "runway", baseUrl: "https://api.dev.runwayml.com" }, ] as const; function assertMigrationJournalIsConsistent() { const migrationsDir = join(process.cwd(), "lib/db/migrations"); const journalPath = join(migrationsDir, "meta", "_journal.json"); const journal = JSON.parse(readFileSync(journalPath, "utf8")) as { entries: { tag: string }[]; }; const tags = new Set(journal.entries.map((entry) => entry.tag)); const sqlFiles = readdirSync(migrationsDir) .filter((file) => file.endsWith(".sql")) .map((file) => file.slice(0, -4)); const sqlSet = new Set(sqlFiles); const extraSqlFiles = sqlFiles.filter((tag) => !tags.has(tag)); const missingSqlFiles = Array.from(tags).filter((tag) => !sqlSet.has(tag)); if (extraSqlFiles.length === 0 && missingSqlFiles.length === 0) { return; } const details = [ extraSqlFiles.length > 0 ? `extra sql files not in journal: ${extraSqlFiles.join(", ")}` : null, missingSqlFiles.length > 0 ? `journal entries missing sql files: ${missingSqlFiles.join(", ")}` : null, ] .filter(Boolean) .join(" | "); throw new Error( `Migration journal mismatch. Keep lib/db/migrations/*.sql in sync with meta/_journal.json. ${details}` ); } const runMigrate = async () => { if (!process.env.POSTGRES_URL) { throw new Error("POSTGRES_URL is not defined"); } const connection = postgres(process.env.POSTGRES_URL, { max: 1 }); const db = drizzle(connection); console.log("⏳ Running migrations..."); const start = Date.now(); assertMigrationJournalIsConsistent(); await migrate(db, { migrationsFolder: "./lib/db/migrations" }); await db.execute(sql` ALTER TABLE "AiProvider" DROP CONSTRAINT IF EXISTS "AiProvider_type_check"; `); await db.execute(sql` ALTER TABLE "AiProvider" ADD CONSTRAINT "AiProvider_type_check" CHECK (type IN ( 'alibaba', 'openai', 'custom', 'local', 'gemini', 'huanke-gemini', 'qianwen-video', 'doubao-video', 'ruxa-video', 'volcano-vision', 'volcano-video', 'alibaba-image', 'bfl-image', 'ruxa-image', 'fal-image', 'openai-image', 'midjourney', 'fal-video', 'zhipu', 'tripo-3d', 'worldlabs-3d', 'runway' )); `); await db.execute(sql` ALTER TABLE "AiModel" ADD COLUMN IF NOT EXISTS "excludeFromFailover" boolean NOT NULL DEFAULT false; `); for (const item of providerBaseUrlDefaults) { await db.execute(sql` UPDATE "AiProvider" SET "baseUrl" = ${item.baseUrl}, "updatedAt" = NOW() WHERE "type" = ${item.type} AND ("baseUrl" IS NULL OR BTRIM("baseUrl") = ''); `); } const end = Date.now(); console.log("✅ Migrations completed in", end - start, "ms"); process.exit(0); }; runMigrate().catch((err) => { console.error("❌ Migration failed"); console.error(err); process.exit(1); });