import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import PocketBase from 'pocketbase'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); 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 = (process.env.PB_URL || backendEnv.POCKETBASE_API_URL || 'http://127.0.0.1:8090').replace(/\/+$/, ''); const ADMIN_EMAIL = process.env.PB_ADMIN_EMAIL || backendEnv.PB_ADMIN_EMAIL || ''; const ADMIN_PASSWORD = process.env.PB_ADMIN_PASSWORD || backendEnv.PB_ADMIN_PASSWORD || ''; const AUTH_TOKEN = process.env.POCKETBASE_AUTH_TOKEN || backendEnv.POCKETBASE_AUTH_TOKEN || ''; const pb = new PocketBase(PB_URL); const collections = [ { name: 'tbl_auth_users', type: 'auth', fields: [ { name: 'users_convers_id', type: 'text' }, { name: 'openid', type: 'text', required: true }, { name: 'org_id', type: 'number' }, { name: 'users_rank_level', type: 'number' }, { name: 'users_status', type: 'number' }, { name: 'users_auth_type', type: 'number' }, { name: 'users_id', type: 'text' }, { name: 'users_name', type: 'text' }, { name: 'users_idtype', type: 'text' }, { name: 'users_id_number', type: 'text' }, { name: 'users_phone', type: 'text' }, { name: 'users_level', type: 'text' }, { name: 'users_type', type: 'text' }, { name: 'users_status', type: 'text' }, { name: 'company_id', type: 'text' }, { name: 'users_parent_id', type: 'text' }, { name: 'users_promo_code', type: 'text' }, { name: 'users_id_pic_a', type: 'text' }, { name: 'users_id_pic_b', type: 'text' }, { name: 'users_title_picture', type: 'text' }, { name: 'users_picture', type: 'text' }, { name: 'usergroups_id', type: 'text' } ], indexes: [ 'CREATE UNIQUE INDEX idx_tbl_auth_users_users_convers_id ON tbl_auth_users (users_convers_id)', 'CREATE UNIQUE INDEX idx_tbl_auth_users_openid ON tbl_auth_users (openid)', 'CREATE INDEX idx_tbl_auth_users_org_id ON tbl_auth_users (org_id)', 'CREATE INDEX idx_tbl_auth_users_users_rank_level ON tbl_auth_users (users_rank_level)', 'CREATE INDEX idx_tbl_auth_users_users_status ON tbl_auth_users (users_status)', 'CREATE INDEX idx_tbl_auth_users_users_auth_type ON tbl_auth_users (users_auth_type)', // Allow unbound users with empty phone while still speeding up phone lookups. 'CREATE INDEX idx_tbl_auth_users_users_phone ON tbl_auth_users (users_phone)', 'CREATE INDEX idx_tbl_auth_users_company_id ON tbl_auth_users (company_id)', 'CREATE INDEX idx_tbl_auth_users_usergroups_id ON tbl_auth_users (usergroups_id)', 'CREATE INDEX idx_tbl_auth_users_users_parent_id ON tbl_auth_users (users_parent_id)' ] }, { name: 'tbl_auth_resources', type: 'base', fields: [ { name: 'res_id', type: 'text', required: true }, { name: 'table_name', type: 'text', required: true }, { name: 'column_name', type: 'text' }, { name: 'res_type', type: 'text', required: true } ], indexes: [ 'CREATE UNIQUE INDEX idx_tbl_auth_resources_res_id ON tbl_auth_resources (res_id)', 'CREATE INDEX idx_tbl_auth_resources_table_name ON tbl_auth_resources (table_name)', 'CREATE INDEX idx_tbl_auth_resources_res_type ON tbl_auth_resources (res_type)', 'CREATE UNIQUE INDEX idx_tbl_auth_resources_unique_res ON tbl_auth_resources (table_name, column_name, res_type)' ] }, { name: 'tbl_auth_roles', type: 'base', fields: [ { name: 'role_id', type: 'text', required: true }, { name: 'role_name', type: 'text', required: true }, { name: 'role_code', type: 'text' }, { name: 'role_status', type: 'number' }, { name: 'role_remark', type: 'text' } ], indexes: [ 'CREATE UNIQUE INDEX idx_tbl_auth_roles_role_id ON tbl_auth_roles (role_id)', 'CREATE UNIQUE INDEX idx_tbl_auth_roles_role_name ON tbl_auth_roles (role_name)', 'CREATE UNIQUE INDEX idx_tbl_auth_roles_role_code ON tbl_auth_roles (role_code)' ] }, { name: 'tbl_auth_role_perms', type: 'base', fields: [ { name: 'role_perm_id', type: 'text', required: true }, { name: 'role_id', type: 'text', required: true }, { name: 'res_id', type: 'text', required: true }, { name: 'access_level', type: 'number', required: true }, { name: 'priority', type: 'number' } ], indexes: [ 'CREATE UNIQUE INDEX idx_tbl_auth_role_perms_role_perm_id ON tbl_auth_role_perms (role_perm_id)', 'CREATE INDEX idx_tbl_auth_role_perms_role_id ON tbl_auth_role_perms (role_id)', 'CREATE INDEX idx_tbl_auth_role_perms_res_id ON tbl_auth_role_perms (res_id)', 'CREATE UNIQUE INDEX idx_tbl_auth_role_perms_unique_map ON tbl_auth_role_perms (role_id, res_id)' ] }, { name: 'tbl_auth_user_overrides', type: 'base', fields: [ { name: 'override_id', type: 'text', required: true }, { name: 'users_convers_id', type: 'text', required: true }, { name: 'res_id', type: 'text', required: true }, { name: 'access_level', type: 'number', required: true }, { name: 'priority', type: 'number' } ], indexes: [ 'CREATE UNIQUE INDEX idx_tbl_auth_user_overrides_override_id ON tbl_auth_user_overrides (override_id)', 'CREATE INDEX idx_tbl_auth_user_overrides_users_convers_id ON tbl_auth_user_overrides (users_convers_id)', 'CREATE INDEX idx_tbl_auth_user_overrides_res_id ON tbl_auth_user_overrides (res_id)', 'CREATE UNIQUE INDEX idx_tbl_auth_user_overrides_unique_map ON tbl_auth_user_overrides (users_convers_id, res_id)' ] }, { name: 'tbl_auth_row_scopes', type: 'base', fields: [ { name: 'scope_id', type: 'text', required: true }, { name: 'target_type', type: 'text', required: true }, { name: 'target_id', type: 'text', required: true }, { name: 'table_name', type: 'text', required: true }, { name: 'filter_sql', type: 'editor', required: true } ], indexes: [ 'CREATE UNIQUE INDEX idx_tbl_auth_row_scopes_scope_id ON tbl_auth_row_scopes (scope_id)', 'CREATE INDEX idx_tbl_auth_row_scopes_target_type ON tbl_auth_row_scopes (target_type)', 'CREATE INDEX idx_tbl_auth_row_scopes_target_id ON tbl_auth_row_scopes (target_id)', 'CREATE INDEX idx_tbl_auth_row_scopes_table_name ON tbl_auth_row_scopes (table_name)' ] } ]; async function init() { try { console.log('🔄 正在连接 PocketBase API...'); if (AUTH_TOKEN) { pb.authStore.save(AUTH_TOKEN, null); console.log('✅ 已使用 POCKETBASE_AUTH_TOKEN 载入认证状态。'); } else if (ADMIN_EMAIL && ADMIN_PASSWORD) { await pb.collection('_superusers').authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD); console.log('✅ 已使用超级管理员账号登录。'); } else { throw new Error('缺少 PocketBase API 认证信息,请提供 POCKETBASE_AUTH_TOKEN 或 PB_ADMIN_EMAIL/PB_ADMIN_PASSWORD'); } console.log('🚀 开始初始化 newpb 权限模型表结构...\n'); for (const collectionData of collections) { await createOrUpdateCollection(collectionData); } await verifyCollections(collections); console.log('\n🎉 newpb 权限模型表结构初始化并校验完成!'); } catch (error) { console.error('❌ 初始化失败:', error.response?.data || error.message); process.exitCode = 1; } } async function createOrUpdateCollection(collectionData) { console.log(`🔄 正在处理表: ${collectionData.name} ...`); const payload = { name: collectionData.name, type: collectionData.type, fields: collectionData.fields, indexes: collectionData.indexes, }; try { await pb.collections.create(payload); console.log(`✅ ${collectionData.name} 创建完成。`); } catch (error) { const nameErrorCode = error.response?.data?.name?.code; if ( error.status === 400 && (nameErrorCode === 'validation_not_unique' || nameErrorCode === 'validation_collection_name_exists') ) { const existing = await pb.collections.getOne(collectionData.name); await pb.collections.update(existing.id, payload); console.log(`♻️ ${collectionData.name} 已存在,已按最新结构更新。`); return; } console.error(`❌ 处理集合 ${collectionData.name} 失败:`, { status: error.status, message: error.message, response: error.response?.data, }); throw error; } } async function verifyCollections(targetCollections) { console.log('\n🔍 开始校验 newpb 表结构与索引...'); for (const target of targetCollections) { const remote = await pb.collections.getOne(target.name); const remoteFieldNames = new Set((remote.fields || []).map((field) => field.name)); const missingFields = target.fields .map((field) => field.name) .filter((fieldName) => !remoteFieldNames.has(fieldName)); 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 === 0 && missingIndexes.length === 0) { console.log(`✅ ${target.name} 校验通过。`); continue; } console.log(`❌ ${target.name} 校验失败:`); if (missingFields.length > 0) { console.log(` - 缺失字段: ${missingFields.join(', ')}`); } if (missingIndexes.length > 0) { console.log(` - 缺失索引: ${missingIndexes.join(' | ')}`); } throw new Error(`${target.name} 结构与预期不一致`); } } init();