Files
base/scripts/gen-permissions.js
iven 3772afd987 chore: 干净 ERP 基座 — 删除 health/ai/wechat 业务代码
删除内容:
- 前端: health/(67文件), ai/(2文件), Copilot, MediaPicker, 相关API/Store/Hook
- 后端: wechat_handler, wechat_service, wechat_user entity, analytics handler, ai_workflow_seed
- 配置: WechatConfig, AppConfig.wechat, AuthState wechat 字段
- 启动: 微信凭据检查块, ensure_ai_workflows() 调用
- 迁移: 新增 m20260613_000170_drop_wechat_users.rs
- 脚本: api_test_health_alert.py, api_test_mp.py, mpsync.sh/ps1
- E2E: health-data page, flows/ 目录

保留: erp-core/auth/workflow/message/config/plugin + 基座前端 + 通用组件
2026-06-13 00:32:50 +08:00

156 lines
5.5 KiB
JavaScript

#!/usr/bin/env node
// gen-permissions.js — 从 permissions.yaml 生成 seed SQL 和前端 routeConfig 片段
//
// 用法:
// node scripts/gen-permissions.js --sql 输出 seed INSERT SQL
// node scripts/gen-permissions.js --frontend 输出 routeConfig.ts 条目
// node scripts/gen-permissions.js --validate 验证与代码一致性
const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');
const YAML_PATH = path.resolve(__dirname, '..', 'permissions.yaml');
function loadPermissions() {
const raw = fs.readFileSync(YAML_PATH, 'utf-8');
return yaml.load(raw);
}
function getAllCodes(registry) {
const codes = [];
for (const [, group] of Object.entries(registry)) {
for (const perm of group.permissions) {
codes.push(perm.code);
}
}
return codes;
}
function generateSeedSQL(registry) {
const sys = '00000000-0000-0000-0000-000000000000';
const lines = [
'-- Auto-generated from permissions.yaml by gen-permissions.js',
`-- Generated: ${new Date().toISOString().slice(0, 10)}`,
'',
];
for (const [groupKey, group] of Object.entries(registry)) {
lines.push(`-- ${groupKey}: ${group.description}`);
for (const perm of group.permissions) {
const parts = perm.code.split('.');
const resource = parts.length >= 2 ? parts[0] : groupKey;
const action = parts.slice(1).join('.');
lines.push(
`INSERT INTO permissions (id, tenant_id, code, name, resource, action, description,` +
` created_at, updated_at, created_by, updated_by, deleted_at, version)` +
` SELECT gen_random_uuid(), t.id, '${perm.code}', '${perm.name}', '${resource}', '${action}', '${perm.name}',` +
` NOW(), NOW(), '${sys}', '${sys}', NULL, 1` +
` FROM tenant t` +
` WHERE NOT EXISTS (SELECT 1 FROM permissions p WHERE p.code = '${perm.code}' AND p.tenant_id = t.id AND p.deleted_at IS NULL)` +
` ON CONFLICT DO NOTHING;`
);
}
lines.push('');
}
return lines.join('\n');
}
function generateFrontendSnippet(registry) {
const lines = ['// Auto-generated from permissions.yaml by gen-permissions.js', ''];
for (const [, group] of Object.entries(registry)) {
const frozenPerms = group.permissions.filter(p => p.frozen);
const activePerms = group.permissions.filter(p => !p.frozen);
// Group by entity prefix (e.g., health.patient → {list, manage})
const entities = {};
for (const perm of [...activePerms, ...frozenPerms]) {
const parts = perm.code.split('.');
if (parts.length < 2) continue;
const entity = parts.slice(0, -1).join('.');
const action = parts[parts.length - 1];
if (!entities[entity]) entities[entity] = { list: [], frozen: perm.frozen || false };
entities[entity].list.push(perm.code);
}
for (const [entity, info] of Object.entries(entities)) {
const frozenAttr = info.frozen ? ',\n frozen: true' : '';
lines.push(` {`);
lines.push(` path: "/${entity.replace(/\./g, '/')}",`);
lines.push(` permissions: [${info.list.map(c => `"${c}"`).join(', ')}]${frozenAttr}`);
lines.push(` },`);
}
}
return lines.join('\n');
}
function validate(registry) {
const yamlCodes = getAllCodes(registry);
const rootDir = path.resolve(__dirname, '..');
// Recursively find .rs files and extract permission codes
const backendCodes = new Set();
function walkDir(dir) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'target') {
walkDir(full);
} else if (entry.isFile() && entry.name.endsWith('.rs')) {
const content = fs.readFileSync(full, 'utf-8');
// Match code: "xxx.yyy.zzz" pattern in PermissionDescriptor
// Must be lowercase letters/digits/hyphens with dots (permission code pattern)
const matches = content.matchAll(/code:\s*"([a-z][a-z0-9-]*\.[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)*)"/g);
for (const m of matches) {
backendCodes.add(m[1]);
}
}
}
}
walkDir(path.join(rootDir, 'crates'));
let errors = 0;
// Check YAML covers all backend codes
for (const code of backendCodes) {
if (!yamlCodes.includes(code)) {
console.log(`MISSING: Backend '${code}' not in permissions.yaml`);
errors++;
}
}
// Check backend covers all YAML codes
for (const code of yamlCodes) {
if (!backendCodes.has(code)) {
console.log(`MISSING: YAML '${code}' not in backend module.rs`);
errors++;
}
}
if (errors === 0) {
console.log(`OK: ${yamlCodes.length} YAML / ${backendCodes.size} backend — 0 mismatches`);
}
return errors;
}
// Main
const args = process.argv.slice(2);
const registry = loadPermissions();
if (args.includes('--sql')) {
console.log(generateSeedSQL(registry));
} else if (args.includes('--frontend')) {
console.log(generateFrontendSnippet(registry));
} else if (args.includes('--validate')) {
const errors = validate(registry);
process.exit(errors > 0 ? 1 : 0);
} else {
const codes = getAllCodes(registry);
console.log(`permissions.yaml loaded: ${codes.length} permission codes across ${Object.keys(registry).length} modules`);
console.log('Usage:');
console.log(' node scripts/gen-permissions.js --sql Generate seed SQL');
console.log(' node scripts/gen-permissions.js --frontend Generate routeConfig snippet');
console.log(' node scripts/gen-permissions.js --validate Validate consistency');
}