152 lines
5.4 KiB
JavaScript
152 lines
5.4 KiB
JavaScript
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
const defaultBackendDir = path.resolve(rootDir, "../hyapp-server/server/admin");
|
|
const backendDir = process.env.HYAPP_ADMIN_SERVER_DIR
|
|
? path.resolve(process.env.HYAPP_ADMIN_SERVER_DIR)
|
|
: defaultBackendDir;
|
|
const contractPath = path.join(rootDir, "contracts/admin-openapi.json");
|
|
const methods = ["get", "post", "put", "patch", "delete"];
|
|
|
|
const contract = JSON.parse(await readFile(contractPath, "utf8"));
|
|
const routes = await readBackendRoutes(path.join(backendDir, "internal/modules"));
|
|
|
|
for (const route of routes) {
|
|
contract.paths[route.path] = contract.paths[route.path] || {};
|
|
const operation = contract.paths[route.path][route.method] || {};
|
|
operation.operationId = route.operationId;
|
|
operation.responses = operation.responses || { "200": { "$ref": "#/components/responses/EmptyResponse" } };
|
|
|
|
const pathParams = [...route.path.matchAll(/\{([^}]+)\}/g)].map((match) => match[1]);
|
|
if (pathParams.length && !operation.parameters) {
|
|
operation.parameters = pathParams.map((name) =>
|
|
name === "id"
|
|
? { "$ref": "#/components/parameters/Id" }
|
|
: {
|
|
in: "path",
|
|
name,
|
|
required: true,
|
|
schema: { type: "integer" }
|
|
}
|
|
);
|
|
}
|
|
|
|
if (route.permissions.length) {
|
|
operation["x-permission"] = operation["x-permission"] || route.permissions[0];
|
|
operation["x-permissions"] = route.permissions;
|
|
}
|
|
|
|
contract.paths[route.path][route.method] = operation;
|
|
}
|
|
|
|
contract.paths = sortObject(contract.paths, (pathItem) => sortObject(pathItem, undefined, methods));
|
|
|
|
await writeFile(contractPath, `${JSON.stringify(contract, null, 2)}\n`);
|
|
console.log(`Synced ${routes.length} backend routes into ${path.relative(rootDir, contractPath)}`);
|
|
|
|
async function readBackendRoutes(modulesDir) {
|
|
const modules = await readdir(modulesDir, { withFileTypes: true });
|
|
const routes = [];
|
|
|
|
for (const moduleEntry of modules.filter((entry) => entry.isDirectory())) {
|
|
const moduleDir = path.join(modulesDir, moduleEntry.name);
|
|
const files = await readdir(moduleDir, { withFileTypes: true });
|
|
for (const fileEntry of files.filter((entry) => entry.isFile() && entry.name.endsWith(".go"))) {
|
|
const source = await readFile(path.join(moduleDir, fileEntry.name), "utf8");
|
|
if (!source.includes("RegisterRoutes")) {
|
|
continue;
|
|
}
|
|
routes.push(...parseRoutes(source, moduleEntry.name));
|
|
}
|
|
}
|
|
|
|
return routes.sort((left, right) => `${left.path}:${left.method}`.localeCompare(`${right.path}:${right.method}`));
|
|
}
|
|
|
|
function parseRoutes(source, moduleName) {
|
|
const groupPrefixes = new Map([
|
|
["api", ""],
|
|
["protected", ""]
|
|
]);
|
|
const routes = [];
|
|
|
|
for (const line of source.split("\n")) {
|
|
const groupMatch = line.match(/^\s*(\w+)\s*:=\s*(\w+)\.Group\("([^"]+)"\)/);
|
|
if (groupMatch) {
|
|
const [, groupName, parentName, prefix] = groupMatch;
|
|
groupPrefixes.set(groupName, `${groupPrefixes.get(parentName) || ""}${prefix}`);
|
|
continue;
|
|
}
|
|
|
|
const routeMatch = line.match(/^\s*(\w+)\.(GET|POST|PUT|PATCH|DELETE)\("([^"]+)"(?:,\s*(.*))?\)/);
|
|
if (!routeMatch) {
|
|
continue;
|
|
}
|
|
|
|
const [, groupName, method, routePath, args = ""] = routeMatch;
|
|
const handlerMatch = args.match(/h\.([A-Za-z0-9_]+)/g);
|
|
const handlerName = handlerMatch?.at(-1)?.replace("h.", "") || `${method}${routePath}`;
|
|
const permissions = readPermissions(args);
|
|
|
|
routes.push({
|
|
method: method.toLowerCase(),
|
|
operationId: operationIdForHandler(moduleName, handlerName, routePath, method),
|
|
path: toOpenApiPath(`${groupPrefixes.get(groupName) || ""}${routePath}`),
|
|
permissions
|
|
});
|
|
}
|
|
|
|
return routes;
|
|
}
|
|
|
|
function readPermissions(args) {
|
|
const anyMatch = args.match(/RequireAnyPermission\(([^)]*)\)/);
|
|
if (anyMatch) {
|
|
return [...anyMatch[1].matchAll(/"([^"]+)"/g)].map((match) => match[1]);
|
|
}
|
|
|
|
const match = args.match(/RequirePermission\("([^"]+)"\)/);
|
|
return match ? [match[1]] : [];
|
|
}
|
|
|
|
function toOpenApiPath(routePath) {
|
|
return routePath.replace(/:([A-Za-z0-9_]+)/g, "{$1}");
|
|
}
|
|
|
|
function lowerFirst(value) {
|
|
return value.charAt(0).toLowerCase() + value.slice(1);
|
|
}
|
|
|
|
function operationIdForHandler(moduleName, handlerName, routePath, method) {
|
|
if (moduleName === "appuser") {
|
|
return `app${handlerName}`;
|
|
}
|
|
if (moduleName === "countryregion" && handlerName === "DisableCountry" && method === "DELETE") {
|
|
return "deleteCountry";
|
|
}
|
|
if (moduleName === "hostorg" && handlerName === "SetBDStatus" && routePath.includes("bd-leaders")) {
|
|
return "setBDLeaderStatus";
|
|
}
|
|
if (moduleName === "resource" && handlerName === "UpdateResourceGroup" && routePath.includes("/items")) {
|
|
return "updateResourceGroupItems";
|
|
}
|
|
return lowerFirst(handlerName);
|
|
}
|
|
|
|
function sortObject(object, mapValue = (value) => value, preferredOrder = []) {
|
|
return Object.fromEntries(
|
|
Object.entries(object)
|
|
.sort(([left], [right]) => {
|
|
const leftIndex = preferredOrder.indexOf(left);
|
|
const rightIndex = preferredOrder.indexOf(right);
|
|
if (leftIndex >= 0 || rightIndex >= 0) {
|
|
return (leftIndex < 0 ? Number.MAX_SAFE_INTEGER : leftIndex) - (rightIndex < 0 ? Number.MAX_SAFE_INTEGER : rightIndex);
|
|
}
|
|
return left.localeCompare(right);
|
|
})
|
|
.map(([key, value]) => [key, mapValue(value)])
|
|
);
|
|
}
|