Files
ok_r/scripts/migrate.js
karlcarlo 095f02700f feat: 添加数据库迁移脚本和 Supabase 工具配置
添加迁移脚本以支持数据库版本管理,配置 pnpm workspace 仅构建 supabase 依赖
新增用户登录字段和任务帮助请求功能,禁用 tasks 和 task_steps 表的 RLS
更新环境变量示例以支持自托管 Supabase 实例
2026-01-29 02:41:10 +08:00

149 lines
5.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();