import fs from "fs"; import path from "path"; const NAMESPACE = "wibu"; // --------------------------- // Helper Functions // --------------------------- function pascalCase(str: string) { return str .replace(/[^a-zA-Z0-9]+(.)/g, (_, chr) => chr.toUpperCase()) .replace(/^\w/, (c) => c.toUpperCase()); } function buildPathWithParams(url: string) { return url.replace(/{(.*?)}/g, (match, p1) => `\${encodeURIComponent(params.${p1})}`); } function detectRequestBody(requestBody: any) { if (!requestBody || !requestBody.content) return null; if (requestBody.content["application/json"]) return "json"; if (requestBody.content["application/x-www-form-urlencoded"]) return "form"; return null; } function buildProperties(parameters: any[], requestBodyType: string | null) { const props: any[] = []; for (const p of parameters || []) { props.push({ displayName: `${pascalCase(p.in)} - ${p.name}`, name: `${p.in}_${p.name}`, type: "string", default: "", }); } if (requestBodyType === "json") { props.push({ displayName: "JSON Body", name: "bodyJson", type: "json", default: {}, }); } return props; } // --------------------------- // Main Node Generator // --------------------------- function generateNode(tag: string, operations: any[], outDir: string) { // Namespace folder: src/nodes/ const baseFolder = path.join(outDir, NAMESPACE); if (!fs.existsSync(baseFolder)) fs.mkdirSync(baseFolder, { recursive: true }); // Subfolder per-tag: src/nodes// const folder = path.join(baseFolder, tag); if (!fs.existsSync(folder)) fs.mkdirSync(folder, { recursive: true }); const className = `${pascalCase(tag)}Api`; // Create switch-case const switchCases = operations .map((op) => { const requestBodyType = detectRequestBody(op.requestBody); return ` case "${op.operationId}": { const params = this.getNodeParameter("params", i, {}) as any; const query = this.getNodeParameter("query", i, {}) as any; const body = this.getNodeParameter("body", i, {}) as any; const url = \`${buildPathWithParams(op.path)}\`; const qs = query; const options: any = { method: "${op.method.toUpperCase()}", uri: url, qs, json: true, }; ${requestBodyType === "json" ? `options.body = body.bodyJson;` : ""} const response = await this.helpers.request(options); returnData.push(response); break; } `; }) .join("\n"); // Build dynamic properties const allProps = operations.flatMap((op: any) => { const body = detectRequestBody(op.requestBody); return buildProperties(op.parameters, body); }); const nodeContent = ` import { INodeType, INodeTypeDescription, IExecuteFunctions } from "n8n-workflow"; export class ${className} implements INodeType { description: INodeTypeDescription = { displayName: "${NAMESPACE} - ${pascalCase(tag)}", name: "${pascalCase(tag)}", group: ["transform"], version: 1, description: "Auto-generated from OpenAPI", defaults: { name: "${NAMESPACE} - ${pascalCase(tag)}", }, icon: "file:../../../icon.svg", inputs: ["main"], outputs: ["main"], properties: [ { displayName: "Operation", name: "operation", type: "options", options: [ ${operations .map( (o) => ` { name: "${o.summary || o.operationId}", value: "${o.operationId}" } ` ) .join(",")} ], default: "${operations[0].operationId}", }, { displayName: "Parameters", name: "params", type: "collection", placeholder: "Add Parameter", default: {}, options: [ ${allProps .map( (p) => ` { displayName: "${p.displayName}", name: "${p.name}", type: "string", default: "", } ` ) .join(",")} ] }, { displayName: "Query", name: "query", type: "collection", placeholder: "Add Query", default: {}, options: [] }, { displayName: "Body", name: "body", type: "collection", default: {}, options: [ { displayName: "JSON Body", name: "bodyJson", type: "json", default: {} } ] } ] }; async execute(this: IExecuteFunctions) { const returnData: any[] = []; const items = this.getInputData(); for (let i = 0; i < items.length; i++) { const operation = this.getNodeParameter("operation", i) as string; switch (operation) { ${switchCases} default: throw new Error("Operation not implemented: " + operation); } } return [returnData]; } } `; const file = path.join(folder, `${className}.node.ts`); fs.writeFileSync(file, nodeContent); return className; } // --------------------------- // Main Program // --------------------------- export function main() { const projectRoot = process.cwd(); const openapiPath = path.join(projectRoot, "openapi.json"); const outDir = path.join(projectRoot, "src", "nodes"); if (!fs.existsSync(openapiPath)) { console.error("❌ openapi.json not found"); process.exit(1); } const openapi = JSON.parse(fs.readFileSync(openapiPath, "utf8")); const tagsMap: any = {}; // Group by tags for (const pathKey of Object.keys(openapi.paths)) { const pathObj = openapi.paths[pathKey]; for (const method of Object.keys(pathObj)) { const op = pathObj[method]; if (!op.tags) continue; const item = { path: pathKey, method, ...op, }; for (const t of op.tags) { tagsMap[t] = tagsMap[t] || []; tagsMap[t].push(item); } } } // Generate index.ts exports const indexExports: string[] = []; for (const tag of Object.keys(tagsMap)) { const className = generateNode(tag, tagsMap[tag], outDir); indexExports.push(`export * from "./nodes/${NAMESPACE}/${tag}/${className}.node";`); } const indexPath = path.join(projectRoot, "src", "index.ts"); fs.writeFileSync(indexPath, indexExports.join("\n")); console.log("✅ Generation complete"); } main();