Files
Web_BAI_Manage_ApiServer/script/pocketbase.add-is-delete-field.js
XuJiacheng cd0373be3c feat: 添加系统刷新令牌请求和用户统计响应的 OpenAPI 规范
feat: 添加微信认证相关的 OpenAPI 规范,包括用户信息、登录请求和个人资料请求

feat: 添加 is_delete 字段迁移脚本,支持在集合中添加软删除字段

feat: 添加软删除规则应用脚本,确保所有相关集合的查询规则包含软删除条件

feat: 添加购物车和订单业务 ID 自动生成的迁移脚本,确保字段类型和自动生成规则正确
2026-04-07 20:02:10 +08:00

234 lines
7.8 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.
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();