import { createRequire } from 'module'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import PocketBase from 'pocketbase'; const require = createRequire(import.meta.url); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); let runtimeConfig = {}; try { runtimeConfig = require('../pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js'); } catch (_error) { runtimeConfig = {}; } function readEnvFile(filePath) { if (!fs.existsSync(filePath)) return {}; const content = fs.readFileSync(filePath, 'utf8'); const result = {}; for (const rawLine of content.split(/\r?\n/)) { const line = rawLine.trim(); if (!line || line.startsWith('#')) continue; const index = line.indexOf('='); if (index === -1) continue; const key = line.slice(0, index).trim(); const value = line.slice(index + 1).trim(); result[key] = value; } return result; } const backendEnv = readEnvFile(path.resolve(__dirname, '..', 'back-end', '.env')); const PB_URL = String( process.env.PB_URL || backendEnv.POCKETBASE_API_URL || runtimeConfig.POCKETBASE_API_URL || 'http://127.0.0.1:8090' ).replace(/\/+$/, ''); const AUTH_TOKEN = process.env.POCKETBASE_AUTH_TOKEN || runtimeConfig.POCKETBASE_AUTH_TOKEN || ''; const SOFT_DELETE_RULE = 'is_delete = 0'; if (!AUTH_TOKEN) { console.error('❌ 缺少 POCKETBASE_AUTH_TOKEN,无法执行软删除规则迁移。'); process.exit(1); } const pb = new PocketBase(PB_URL); function isSystemCollection(collection) { return !!(collection && (collection.system || String(collection.name || '').startsWith('_'))); } function normalizeFieldPayload(field) { const payload = Object.assign({}, field); if (field.type === 'number') { if (Object.prototype.hasOwnProperty.call(field, 'default')) payload.default = field.default; if (Object.prototype.hasOwnProperty.call(field, 'min')) payload.min = field.min; if (Object.prototype.hasOwnProperty.call(field, 'max')) payload.max = field.max; if (Object.prototype.hasOwnProperty.call(field, 'onlyInt')) payload.onlyInt = !!field.onlyInt; } if (field.type === 'autodate') { payload.onCreate = typeof field.onCreate === 'boolean' ? field.onCreate : true; payload.onUpdate = typeof field.onUpdate === 'boolean' ? field.onUpdate : false; } if (field.type === 'file') { payload.maxSelect = typeof field.maxSelect === 'number' ? field.maxSelect : 0; payload.maxSize = typeof field.maxSize === 'number' ? field.maxSize : 0; payload.mimeTypes = Array.isArray(field.mimeTypes) ? field.mimeTypes : null; } return payload; } function mergeRuleWithSoftDelete(rule) { const currentRule = typeof rule === 'string' ? rule.trim() : ''; if (!currentRule) return SOFT_DELETE_RULE; if (currentRule === SOFT_DELETE_RULE) return currentRule; if (currentRule.includes(SOFT_DELETE_RULE)) return currentRule; return `(${currentRule}) && ${SOFT_DELETE_RULE}`; } function buildCollectionPayload(collection) { return { name: collection.name, type: collection.type, listRule: mergeRuleWithSoftDelete(collection.listRule), viewRule: mergeRuleWithSoftDelete(collection.viewRule), createRule: collection.createRule, updateRule: collection.updateRule, deleteRule: collection.deleteRule, fields: (collection.fields || []).filter((field) => field && field.name !== 'id').map((field) => normalizeFieldPayload(field)), indexes: collection.indexes || [], }; } async function applyRules() { const collections = await pb.collections.getFullList({ sort: 'name' }); const changed = []; for (const collection of collections) { if (isSystemCollection(collection)) continue; const hasSoftDelete = (collection.fields || []).some((field) => field && field.name === 'is_delete'); if (!hasSoftDelete) continue; const nextListRule = mergeRuleWithSoftDelete(collection.listRule); const nextViewRule = mergeRuleWithSoftDelete(collection.viewRule); const listChanged = nextListRule !== (collection.listRule || ''); const viewChanged = nextViewRule !== (collection.viewRule || ''); if (!listChanged && !viewChanged) { changed.push({ name: collection.name, action: 'skipped', listRule: nextListRule, viewRule: nextViewRule }); continue; } await pb.collections.update(collection.id, buildCollectionPayload(collection)); changed.push({ name: collection.name, action: 'updated', listRule: nextListRule, viewRule: nextViewRule }); } return changed; } async function verifyRules() { const collections = await pb.collections.getFullList({ sort: 'name' }); const failed = []; for (const collection of collections) { if (isSystemCollection(collection)) continue; const hasSoftDelete = (collection.fields || []).some((field) => field && field.name === 'is_delete'); if (!hasSoftDelete) continue; const listRule = String(collection.listRule || ''); const viewRule = String(collection.viewRule || ''); if (!listRule.includes(SOFT_DELETE_RULE) || !viewRule.includes(SOFT_DELETE_RULE)) { failed.push({ name: collection.name, listRule, viewRule }); } } if (failed.length) { throw new Error(`以下集合未正确应用软删除规则: ${JSON.stringify(failed, null, 2)}`); } } async function main() { try { console.log(`🔄 正在连接 PocketBase: ${PB_URL}`); pb.authStore.save(AUTH_TOKEN, null); console.log('✅ 已使用 POCKETBASE_AUTH_TOKEN 载入认证状态。'); const changed = await applyRules(); console.log('📝 规则更新结果:'); console.log(JSON.stringify(changed, null, 2)); await verifyRules(); console.log('✅ 校验通过:所有带 is_delete 的业务集合已默认过滤 is_delete = 0。'); console.log('🎉 软删除默认查询规则迁移完成!'); } catch (error) { console.error('❌ 软删除默认查询规则迁移失败:', error.response?.data || error.message || error); process.exitCode = 1; } } main();