Files
Web_BAI_Manage_ApiServer/script/pocketbase.documents.js
XuJiacheng 51a90260e4 feat: 添加 PocketBase MiniApp 公司 API 文档和文件字段迁移脚本
- 新增 openapi-miniapp-company.yaml 文件,定义 tbl_company 的基础 CRUD 接口文档,包括查询、创建、更新和删除公司记录的详细描述和示例。
- 新增 pocketbase.file-fields-to-attachments.js 脚本,用于迁移 PocketBase 中的文件字段到文本字段,并处理 tbl_attachments 集合的公开规则。
2026-03-28 15:13:04 +08:00

296 lines
13 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 PocketBase from 'pocketbase';
const require = createRequire(import.meta.url);
let runtimeConfig = {};
try {
runtimeConfig = require('../pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js');
} catch (_error) {
runtimeConfig = {};
}
const PB_URL = (process.env.PB_URL || 'https://bai-api.blv-oa.com/pb').replace(/\/+$/, '');
const AUTH_TOKEN = process.env.POCKETBASE_AUTH_TOKEN || runtimeConfig.POCKETBASE_AUTH_TOKEN || '';
if (!AUTH_TOKEN) {
console.error('❌ 缺少 POCKETBASE_AUTH_TOKEN无法执行建表。');
process.exit(1);
}
const pb = new PocketBase(PB_URL);
const collections = [
{
name: 'tbl_attachments',
type: 'base',
listRule: '',
viewRule: '',
fields: [
{ name: 'attachments_id', type: 'text', required: true },
{ name: 'attachments_link', type: 'file', maxSelect: 0, maxSize: 4294967296, mimeTypes: [] },
{ name: 'attachments_filename', type: 'text' },
{ name: 'attachments_filetype', type: 'text' },
{ name: 'attachments_size', type: 'number' },
{ name: 'attachments_owner', type: 'text' },
{ name: 'attachments_md5', type: 'text' },
{ name: 'attachments_ocr', type: 'text' },
{ name: 'attachments_status', type: 'text' },
{ name: 'attachments_remark', type: 'text' },
],
indexes: [
'CREATE UNIQUE INDEX idx_tbl_attachments_attachments_id ON tbl_attachments (attachments_id)',
'CREATE INDEX idx_tbl_attachments_attachments_owner ON tbl_attachments (attachments_owner)',
'CREATE INDEX idx_tbl_attachments_attachments_status ON tbl_attachments (attachments_status)',
],
},
{
name: 'tbl_document',
type: 'base',
fields: [
{ name: 'document_id', type: 'text', required: true },
{ name: 'document_effect_date', type: 'date' },
{ name: 'document_expiry_date', type: 'date' },
{ name: 'document_type', type: 'text', required: true },
{ name: 'document_title', type: 'text', required: true },
{ name: 'document_subtitle', type: 'text' },
{ name: 'document_summary', type: 'text' },
{ name: 'document_content', type: 'text' },
{ name: 'document_image', type: 'text' },
{ name: 'document_video', type: 'text' },
{ name: 'document_file', type: 'text' },
{ name: 'document_owner', type: 'text' },
{ name: 'document_relation_model', type: 'text' },
{ name: 'document_keywords', type: 'text' },
{ name: 'document_share_count', type: 'number' },
{ name: 'document_download_count', type: 'number' },
{ name: 'document_favorite_count', type: 'number' },
{ name: 'document_status', type: 'text' },
{ name: 'document_embedding_status', type: 'text' },
{ name: 'document_embedding_error', type: 'text' },
{ name: 'document_embedding_lasttime', type: 'date' },
{ name: 'document_vector_version', type: 'text' },
{ name: 'document_product_categories', type: 'text' },
{ name: 'document_application_scenarios', type: 'text' },
{ name: 'document_hotel_type', type: 'text' },
{ name: 'document_remark', type: 'text' },
],
indexes: [
'CREATE UNIQUE INDEX idx_tbl_document_document_id ON tbl_document (document_id)',
'CREATE INDEX idx_tbl_document_document_owner ON tbl_document (document_owner)',
'CREATE INDEX idx_tbl_document_document_type ON tbl_document (document_type)',
'CREATE INDEX idx_tbl_document_document_status ON tbl_document (document_status)',
'CREATE INDEX idx_tbl_document_document_embedding_status ON tbl_document (document_embedding_status)',
'CREATE INDEX idx_tbl_document_document_effect_date ON tbl_document (document_effect_date)',
'CREATE INDEX idx_tbl_document_document_expiry_date ON tbl_document (document_expiry_date)',
],
},
{
name: 'tbl_document_operation_history',
type: 'base',
fields: [
{ name: 'doh_id', type: 'text', required: true },
{ name: 'doh_document_id', type: 'text', required: true },
{ name: 'doh_operation_type', type: 'text' },
{ name: 'doh_user_id', type: 'text' },
{ name: 'doh_current_count', type: 'number' },
{ name: 'doh_remark', type: 'text' },
],
indexes: [
'CREATE UNIQUE INDEX idx_tbl_document_operation_history_doh_id ON tbl_document_operation_history (doh_id)',
'CREATE INDEX idx_tbl_document_operation_history_doh_document_id ON tbl_document_operation_history (doh_document_id)',
'CREATE INDEX idx_tbl_document_operation_history_doh_user_id ON tbl_document_operation_history (doh_user_id)',
'CREATE INDEX idx_tbl_document_operation_history_doh_operation_type ON tbl_document_operation_history (doh_operation_type)',
],
},
];
function normalizeFieldPayload(field, existingField) {
const payload = existingField
? Object.assign({}, existingField)
: {
name: field.name,
type: field.type,
};
if (existingField && existingField.id) {
payload.id = existingField.id;
}
payload.name = field.name;
payload.type = field.type;
if (typeof field.required !== 'undefined') {
payload.required = field.required;
}
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.length ? field.mimeTypes : null;
}
return payload;
}
function buildCollectionPayload(collectionData, existingCollection) {
if (!existingCollection) {
return {
name: collectionData.name,
type: collectionData.type,
listRule: Object.prototype.hasOwnProperty.call(collectionData, 'listRule') ? collectionData.listRule : null,
viewRule: Object.prototype.hasOwnProperty.call(collectionData, 'viewRule') ? collectionData.viewRule : null,
createRule: Object.prototype.hasOwnProperty.call(collectionData, 'createRule') ? collectionData.createRule : null,
updateRule: Object.prototype.hasOwnProperty.call(collectionData, 'updateRule') ? collectionData.updateRule : null,
deleteRule: Object.prototype.hasOwnProperty.call(collectionData, 'deleteRule') ? collectionData.deleteRule : null,
fields: collectionData.fields.map((field) => normalizeFieldPayload(field, null)),
indexes: collectionData.indexes,
};
}
const targetFieldMap = new Map(collectionData.fields.map((field) => [field.name, field]));
const fields = (existingCollection.fields || []).map((existingField) => {
const targetField = targetFieldMap.get(existingField.name);
if (!targetField) {
return existingField;
}
targetFieldMap.delete(existingField.name);
return normalizeFieldPayload(targetField, existingField);
});
for (const field of targetFieldMap.values()) {
fields.push(normalizeFieldPayload(field, null));
}
return {
name: collectionData.name,
type: collectionData.type,
listRule: Object.prototype.hasOwnProperty.call(collectionData, 'listRule') ? collectionData.listRule : existingCollection.listRule,
viewRule: Object.prototype.hasOwnProperty.call(collectionData, 'viewRule') ? collectionData.viewRule : existingCollection.viewRule,
createRule: Object.prototype.hasOwnProperty.call(collectionData, 'createRule') ? collectionData.createRule : existingCollection.createRule,
updateRule: Object.prototype.hasOwnProperty.call(collectionData, 'updateRule') ? collectionData.updateRule : existingCollection.updateRule,
deleteRule: Object.prototype.hasOwnProperty.call(collectionData, 'deleteRule') ? collectionData.deleteRule : existingCollection.deleteRule,
fields: fields,
indexes: collectionData.indexes,
};
}
function normalizeFieldList(fields) {
return (fields || []).map((field) => ({
name: field.name,
type: field.type,
required: !!field.required,
}));
}
async function createOrUpdateCollection(collectionData) {
console.log(`🔄 正在处理表: ${collectionData.name} ...`);
try {
const existing = await pb.collections.getOne(collectionData.name);
await pb.collections.update(existing.id, buildCollectionPayload(collectionData, existing));
console.log(`♻️ ${collectionData.name} 已存在,已按最新结构更新。`);
} catch (error) {
if (error.status === 404) {
try {
await pb.collections.create(buildCollectionPayload(collectionData, null));
console.log(`${collectionData.name} 创建完成。`);
return;
} catch (createError) {
console.error(`❌ 创建集合 ${collectionData.name} 失败:`, {
status: createError.status,
message: createError.message,
response: createError.response?.data,
});
throw createError;
}
}
console.error(`❌ 处理集合 ${collectionData.name} 失败:`, {
status: error.status,
message: error.message,
response: error.response?.data,
});
throw error;
}
}
async function verifyCollections(targetCollections) {
console.log('\n🔍 开始校验文档相关表结构与索引...');
for (const target of targetCollections) {
const remote = await pb.collections.getOne(target.name);
const remoteFields = normalizeFieldList(remote.fields);
const targetFields = normalizeFieldList(target.fields);
const remoteFieldMap = new Map(remoteFields.map((field) => [field.name, field.type]));
const remoteRequiredMap = new Map(remoteFields.map((field) => [field.name, field.required]));
const missingFields = [];
const mismatchedTypes = [];
const mismatchedRequired = [];
for (const field of targetFields) {
if (!remoteFieldMap.has(field.name)) {
missingFields.push(field.name);
continue;
}
if (remoteFieldMap.get(field.name) !== field.type) {
mismatchedTypes.push(`${field.name}:${remoteFieldMap.get(field.name)}!=${field.type}`);
}
if (remoteRequiredMap.get(field.name) !== !!field.required) {
mismatchedRequired.push(`${field.name}:${remoteRequiredMap.get(field.name)}!=${!!field.required}`);
}
}
const remoteIndexes = new Set(remote.indexes || []);
const missingIndexes = target.indexes.filter((indexSql) => !remoteIndexes.has(indexSql));
if (remote.type !== target.type) {
throw new Error(`${target.name} 类型不匹配,期望 ${target.type},实际 ${remote.type}`);
}
if (!missingFields.length && !mismatchedTypes.length && !mismatchedRequired.length && !missingIndexes.length) {
console.log(`${target.name} 校验通过。`);
continue;
}
console.log(`${target.name} 校验失败:`);
if (missingFields.length) {
console.log(` - 缺失字段: ${missingFields.join(', ')}`);
}
if (mismatchedTypes.length) {
console.log(` - 字段类型不匹配: ${mismatchedTypes.join(', ')}`);
}
if (mismatchedRequired.length) {
console.log(` - 字段必填属性不匹配: ${mismatchedRequired.join(', ')}`);
}
if (missingIndexes.length) {
console.log(` - 缺失索引: ${missingIndexes.join(' | ')}`);
}
throw new Error(`${target.name} 结构与预期不一致`);
}
}
async function init() {
try {
console.log(`🔄 正在连接 PocketBase: ${PB_URL}`);
pb.authStore.save(AUTH_TOKEN, null);
console.log('✅ 已使用 POCKETBASE_AUTH_TOKEN 载入认证状态。');
for (const collectionData of collections) {
await createOrUpdateCollection(collectionData);
}
await verifyCollections(collections);
console.log('\n🎉 文档相关表结构初始化并校验完成!');
} catch (error) {
console.error('❌ 初始化失败:', error.response?.data || error.message);
process.exitCode = 1;
}
}
init();