添加迁移脚本以支持数据库版本管理,配置 pnpm workspace 仅构建 supabase 依赖 新增用户登录字段和任务帮助请求功能,禁用 tasks 和 task_steps 表的 RLS 更新环境变量示例以支持自托管 Supabase 实例
149 lines
5.0 KiB
JavaScript
149 lines
5.0 KiB
JavaScript
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();
|