const { createClient } = require('@supabase/supabase-js'); const fs = require('fs'); const path = require('path'); const dotenv = require('dotenv'); // 加载环境变量 dotenv.config({ path: '.env.local' }); dotenv.config(); const MIGRATIONS_DIR = path.join(process.cwd(), 'supabase', 'migrations'); async function migrate() { const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY; if (!supabaseUrl || !supabaseServiceKey) { console.error('❌ 错误: 缺少必要的环境变量。'); console.error('请确保 .env.local 中包含以下变量:'); console.error(' - NEXT_PUBLIC_SUPABASE_URL'); console.error(' - SUPABASE_SERVICE_ROLE_KEY (需要从 Supabase Dashboard 获取)'); process.exit(1); } // 使用 Service Role Key 初始化 Supabase 客户端,以获得执行 DDL 的权限 const supabase = createClient(supabaseUrl, supabaseServiceKey, { auth: { autoRefreshToken: false, persistSession: false, }, }); // 封装 SQL 执行函数,通过 RPC 调用 const execSql = async (sql, params = []) => { // 注意:这需要数据库中预先存在一个名为 'exec_sql' 的 RPC 函数 // create or replace function exec_sql(sql text) returns void as $$ // begin // execute sql; // end; // $$ language plpgsql security definer; // 由于 exec_sql RPC 仅接受 sql 文本,我们这里简单地替换参数(如果只是简单的值替换) // 或者如果 RPC 支持参数化查询则更好。目前的简单实现假设 SQL 已经完整。 // 如果需要参数支持,建议直接拼接(仅限迁移脚本可控环境)或改进 RPC。 // 这里的实现假设传入的 SQL 是完整的脚本。 const { error } = await supabase.rpc('exec_sql', { sql }); if (error) { throw new Error(error.message); } }; // 检查 RPC 是否可用 try { await execSql('SELECT 1'); console.log('✅ 已连接到 Supabase (RPC exec_sql 可用)'); } catch (err) { console.error('❌ 连接失败或 exec_sql 函数不存在。'); console.error('请确保您已在 Supabase 数据库中创建了 exec_sql 函数。'); console.error('请查看 supabase/init_rpc.sql 获取创建脚本。'); console.error('错误信息:', err.message); process.exit(1); } try { // 确保 uuid-ossp 扩展已启用 (虽然我们现在主要使用 gen_random_uuid,但保留此行以兼容旧代码) await execSql('CREATE EXTENSION IF NOT EXISTS "uuid-ossp";'); // 创建迁移记录表 await execSql(` CREATE TABLE IF NOT EXISTS _migrations ( id SERIAL PRIMARY KEY, name TEXT UNIQUE NOT NULL, applied_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 允许 Service Role 访问此表(虽然 Service Role 默认有权限,但明确授权更稳妥) -- 或者简单地禁用此表的 RLS,因为它只在后端使用 ALTER TABLE _migrations ENABLE ROW LEVEL SECURITY; -- 创建策略,允许所有操作(因为我们只通过 Service Role 访问) -- 注意:如果策略已存在会报错,所以先尝试删除 DROP POLICY IF EXISTS "Allow full access to service role" ON _migrations; CREATE POLICY "Allow full access to service role" ON _migrations USING (true) WITH CHECK (true); `); // 获取已执行的迁移 // 由于 exec_sql 不返回值,我们需要通过标准 Supabase 查询来获取记录 const { data: appliedMigrations, error: fetchError } = await supabase .from('_migrations') .select('name'); if (fetchError) { throw new Error(`无法获取迁移记录: ${fetchError.message}`); } const appliedNames = new Set(appliedMigrations.map((row) => row.name)); // 读取迁移文件 const files = fs.readdirSync(MIGRATIONS_DIR) .filter((file) => file.endsWith('.sql')) .sort(); // 按文件名排序 let count = 0; for (const file of files) { if (appliedNames.has(file)) { continue; } console.log(`🚀 正在执行迁移: ${file}...`); const filePath = path.join(MIGRATIONS_DIR, file); const sql = fs.readFileSync(filePath, 'utf8'); try { // 执行迁移 SQL await execSql(sql); // 记录迁移状态 const { error: insertError } = await supabase .from('_migrations') .insert({ name: file }); if (insertError) { throw new Error(`无法记录迁移状态: ${insertError.message}`); } console.log(`✅ 成功: ${file}`); count++; } catch (err) { console.error(`❌ 失败: ${file}`); console.error(err); process.exit(1); } } if (count === 0) { console.log('✨ 没有需要执行的迁移'); } else { console.log(`🎉 成功执行了 ${count} 个迁移`); } } catch (err) { console.error('❌ 迁移过程中发生错误:', err); process.exit(1); } } migrate();