import { readFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { MENU_CODE_VALUES, PERMISSION_CODES } from "../src/app/permissions"; import { fallbackNavigation } from "../src/app/navigation/menu"; import { adminRoutes } from "../src/app/router/routeConfig"; type OpenApiOperation = { operationId?: string; "x-permission"?: string; "x-permissions"?: string[]; }; type OpenApiDocument = { paths?: Record>; }; type NavItem = { children?: NavItem[]; code?: string; path?: string; permissionCode?: string; }; const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const contract = JSON.parse(readFileSync(path.join(rootDir, "contracts/admin-openapi.json"), "utf8")) as OpenApiDocument; const errors: string[] = []; const knownPermissions = new Set(PERMISSION_CODES); const knownMenuCodes = new Set(MENU_CODE_VALUES); const routesByPath = new Map(); const routesByMenuCode = new Map(); const routePermissions = new Set(); for (const route of adminRoutes) { if (!route.path) { errors.push(`Route ${route.pageKey} is missing path`); } if (!route.menuCode) { errors.push(`Route ${route.path} is missing menuCode`); } if (!route.permission) { errors.push(`Route ${route.path} is missing permission`); } if (route.menuCode && !knownMenuCodes.has(route.menuCode)) { errors.push(`Route ${route.path} uses unknown menuCode: ${route.menuCode}`); } if (route.permission && !knownPermissions.has(route.permission)) { errors.push(`Route ${route.path} uses unknown permission: ${route.permission}`); } if (routesByPath.has(route.path)) { errors.push(`Duplicate route path: ${route.path}`); } if (routesByMenuCode.has(route.menuCode)) { errors.push(`Duplicate route menuCode: ${route.menuCode}`); } routesByPath.set(route.path, route); routesByMenuCode.set(route.menuCode, route); routePermissions.add(route.permission); } for (const item of flattenNavItems(fallbackNavigation as NavItem[])) { if (item.code && !knownMenuCodes.has(item.code)) { errors.push(`Fallback menu uses unknown menu code: ${item.code}`); } if (!item.path) { continue; } const route = item.code ? routesByMenuCode.get(item.code) : undefined; if (!route) { errors.push(`Fallback menu ${item.code || item.path} has no matching route`); continue; } if (route.path !== item.path) { errors.push(`Fallback menu ${item.code} path mismatch: ${item.path} !== ${route.path}`); } if (item.permissionCode !== route.permission) { errors.push(`Fallback menu ${item.code} permission mismatch: ${item.permissionCode} !== ${route.permission}`); } } const operationIds = new Set(); const apiPermissions = new Set(); const httpMethods = new Set(["delete", "get", "patch", "post", "put"]); for (const [endpointPath, pathItem] of Object.entries(contract.paths || {})) { for (const [method, operation] of Object.entries(pathItem)) { if (!httpMethods.has(method)) { continue; } if (!operation.operationId) { errors.push(`${method.toUpperCase()} ${endpointPath} is missing operationId`); } else if (operationIds.has(operation.operationId)) { errors.push(`Duplicate OpenAPI operationId: ${operation.operationId}`); } else { operationIds.add(operation.operationId); } const permissions = operation["x-permissions"]?.length ? operation["x-permissions"] : operation["x-permission"] ? [operation["x-permission"]] : []; for (const permission of permissions) { apiPermissions.add(permission); if (!knownPermissions.has(permission)) { errors.push(`${method.toUpperCase()} ${endpointPath} uses unknown x-permission: ${permission}`); } } } } if (errors.length) { console.error("Admin architecture validation failed:"); for (const error of errors) { console.error(`- ${error}`); } process.exit(1); } console.log( `Admin architecture validation passed: ${adminRoutes.length} routes, ${operationIds.size} operations, ${apiPermissions.size} protected permissions.` ); function flattenNavItems(items: NavItem[]): NavItem[] { return items.flatMap((item) => [item, ...flattenNavItems(item.children || [])]); }