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 || ''; if (!AUTH_TOKEN) { console.error('❌ 缺少 POCKETBASE_AUTH_TOKEN,无法执行 is_delete 字段迁移。'); process.exit(1); } const pb = new PocketBase(PB_URL); const TARGET_FIELD = { name: 'is_delete', type: 'number', required: false, presentable: true, hidden: false, default: 0, min: 0, max: 1, onlyInt: true, }; function isSystemCollection(collection) { return !!(collection && (collection.system || String(collection.name || '').startsWith('_'))); } function normalizeFieldPayload(field, targetSpec) { const payload = field ? Object.assign({}, field) : {}; const next = targetSpec || field || {}; if (field && field.id) { payload.id = field.id; } payload.name = next.name; payload.type = next.type; if (typeof next.required !== 'undefined') { payload.required = !!next.required; } if (typeof next.presentable !== 'undefined') { payload.presentable = !!next.presentable; } if (typeof next.hidden !== 'undefined') { payload.hidden = !!next.hidden; } if (next.type === 'number') { if (Object.prototype.hasOwnProperty.call(next, 'default')) payload.default = next.default; if (Object.prototype.hasOwnProperty.call(next, 'min')) payload.min = next.min; if (Object.prototype.hasOwnProperty.call(next, 'max')) payload.max = next.max; if (Object.prototype.hasOwnProperty.call(next, 'onlyInt')) payload.onlyInt = !!next.onlyInt; } if (next.type === 'autodate') { payload.onCreate = typeof next.onCreate === 'boolean' ? next.onCreate : (typeof field?.onCreate === 'boolean' ? field.onCreate : true); payload.onUpdate = typeof next.onUpdate === 'boolean' ? next.onUpdate : (typeof field?.onUpdate === 'boolean' ? field.onUpdate : false); } if (next.type === 'file') { payload.maxSelect = typeof next.maxSelect === 'number' ? next.maxSelect : (typeof field?.maxSelect === 'number' ? field.maxSelect : 0); payload.maxSize = typeof next.maxSize === 'number' ? next.maxSize : (typeof field?.maxSize === 'number' ? field.maxSize : 0); payload.mimeTypes = Array.isArray(next.mimeTypes) ? next.mimeTypes : (Array.isArray(field?.mimeTypes) ? field.mimeTypes : null); } return payload; } function buildCollectionPayload(collection, fields) { return { name: collection.name, type: collection.type, listRule: collection.listRule, viewRule: collection.viewRule, createRule: collection.createRule, updateRule: collection.updateRule, deleteRule: collection.deleteRule, fields, indexes: collection.indexes || [], }; } async function addFieldToCollections() { const collections = await pb.collections.getFullList({ sort: 'name' }); const changed = []; for (const collection of collections) { if (isSystemCollection(collection)) continue; const currentFields = Array.isArray(collection.fields) ? collection.fields : []; const existingField = currentFields.find((field) => field && field.name === TARGET_FIELD.name); const nextFields = currentFields .filter((field) => field && field.name !== 'id') .map((field) => normalizeFieldPayload(field)); if (existingField) { for (let i = 0; i < nextFields.length; i += 1) { if (nextFields[i].name === TARGET_FIELD.name) { nextFields[i] = normalizeFieldPayload(existingField, TARGET_FIELD); break; } } } else { nextFields.push(normalizeFieldPayload(null, TARGET_FIELD)); } await pb.collections.update(collection.id, buildCollectionPayload(collection, nextFields)); changed.push({ name: collection.name, action: existingField ? 'normalized' : 'added' }); } return changed; } async function backfillRecords() { const collections = await pb.collections.getFullList({ sort: 'name' }); const summary = []; for (const collection of collections) { if (isSystemCollection(collection)) continue; let page = 1; let patched = 0; const perPage = 200; while (true) { const list = await pb.collection(collection.name).getList(page, perPage, { fields: 'id,is_delete', skipTotal: false, }); const items = Array.isArray(list.items) ? list.items : []; for (const item of items) { const value = item && Object.prototype.hasOwnProperty.call(item, 'is_delete') ? item.is_delete : null; if (value === 0 || value === '0') continue; await pb.collection(collection.name).update(item.id, { is_delete: 0 }); patched += 1; } if (page >= list.totalPages) break; page += 1; } summary.push({ name: collection.name, backfilledRecords: patched }); } return summary; } async function verifyCollections() { const collections = await pb.collections.getFullList({ sort: 'name' }); const failed = []; for (const collection of collections) { if (isSystemCollection(collection)) continue; const field = (collection.fields || []).find((item) => item && item.name === TARGET_FIELD.name); if (!field || field.type !== 'number') failed.push(collection.name); } if (failed.length) { throw new Error('以下集合缺少 is_delete 字段或类型不正确: ' + failed.join(', ')); } } async function main() { try { console.log(`🔄 正在连接 PocketBase: ${PB_URL}`); pb.authStore.save(AUTH_TOKEN, null); console.log('✅ 已使用 POCKETBASE_AUTH_TOKEN 载入认证状态。'); const changed = await addFieldToCollections(); console.log('📝 已更新集合:'); console.log(JSON.stringify(changed, null, 2)); const backfilled = await backfillRecords(); console.log('📝 已回填记录:'); console.log(JSON.stringify(backfilled, null, 2)); await verifyCollections(); console.log('✅ 校验通过:所有业务集合已包含 is_delete(number, default=0) 字段。'); console.log('🎉 is_delete 字段迁移完成!'); } catch (error) { console.error('❌ is_delete 字段迁移失败:', error.response?.data || error.message || error); process.exitCode = 1; } } main();