#!/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'); }