feat: 添加数据库迁移脚本和 Supabase 工具配置
添加迁移脚本以支持数据库版本管理,配置 pnpm workspace 仅构建 supabase 依赖 新增用户登录字段和任务帮助请求功能,禁用 tasks 和 task_steps 表的 RLS 更新环境变量示例以支持自托管 Supabase 实例
This commit is contained in:
148
scripts/migrate.js
Normal file
148
scripts/migrate.js
Normal file
@@ -0,0 +1,148 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user