// generator from open api json to n8n custom nodes /* eslint-disable @typescript-eslint/no-explicit-any */ import fs from "fs"; import path from "path"; import { pascalCase } from "pascal-case"; import pkg from "./package.json"; const openApiPath = "./openapi.json"; const baseNodesPath = "./src/nodes"; const BASE_URL = process.env.API_BASE_URL || "https://your-api-domain.com"; if (!fs.existsSync(openApiPath)) { console.error("āŒ File openapi.json tidak ditemukan."); process.exit(1); } const openApi = JSON.parse(fs.readFileSync(openApiPath, "utf8")); const sanitizeVarName = (str: string) => str.replace(/[^a-zA-Z0-9_]/g, "_"); function mapTypeToN8n(type: string): string { const typeMap: Record = { string: "string", number: "number", integer: "number", boolean: "boolean", array: "string", object: "json", }; return typeMap[type] || "string"; } function buildProperties(schema: any, requiredFields: string[] = []): string { if (!schema || !schema.properties) { return `{ displayName: "Data", name: "data", type: "json", default: "{}", description: "Request body sebagai JSON", }`; } return Object.entries(schema.properties) .map(([key, prop]: [string, any]) => { const type = mapTypeToN8n(prop.type || "string"); const isRequired = requiredFields.includes(key); const description = prop.description || prop.error || key; return `{ displayName: "${key.charAt(0).toUpperCase() + key.slice(1)}", name: "${key}", type: "${type}", default: ${type === "number" ? "0" : '""'}, required: ${isRequired}, description: "${description.replace(/"/g, "'")}", }`; }) .join(",\n "); } if (!fs.existsSync(baseNodesPath)) { fs.mkdirSync(baseNodesPath, { recursive: true }); } pkg.n8n.nodes = []; for (const [pathUrl, methods] of Object.entries(openApi.paths)) { for (const [method, spec] of Object.entries(methods)) { const tag = spec.tags?.[0] || "default"; const rawOperationId = spec.operationId || `${method}_${pathUrl}`; const cleanOperationId = sanitizeVarName(rawOperationId); const className = pascalCase(cleanOperationId); const summary = spec.summary || cleanOperationId; const description = (spec.description || summary).replace(/"/g, "'"); const schema = spec.requestBody?.content?.["application/json"]?.schema || null; const requiredFields = schema?.required || []; const tagFolder = path.join(baseNodesPath, tag); if (!fs.existsSync(tagFolder)) fs.mkdirSync(tagFolder); const nodeFolder = path.join(tagFolder, cleanOperationId); if (!fs.existsSync(nodeFolder)) fs.mkdirSync(nodeFolder); // ===== DESCRIPTION FILE ===== fs.writeFileSync( path.join(nodeFolder, `${cleanOperationId}.description.ts`), `import { INodeTypeDescription } from "n8n-workflow"; export const ${cleanOperationId}Description: INodeTypeDescription = { displayName: "${summary}", name: "${cleanOperationId.toLowerCase()}", group: ["transform"], version: 1, subtitle: '=${tag}', description: "${description}", codex: { categories: ["BIP"], resources: {}, alias: ["bip", "custom", "${tag.toLowerCase()}"], }, defaults: { name: "${summary}", }, inputs: ["main"], outputs: ["main"], credentials: [ { name: "apiAuth", required: false, }, ], properties: [ ${buildProperties(schema, requiredFields)} ], }; ` ); // ===== NODE FILE ===== const hasBody = ["post", "put", "patch"].includes(method.toLowerCase()); // Ambil property names untuk di-hardcode const propertyNames = schema?.properties ? Object.keys(schema.properties).map(k => `"${k}"`).join(", ") : ""; const hasProperties = hasBody && propertyNames; fs.writeFileSync( path.join(nodeFolder, `${cleanOperationId}.node.ts`), `import { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription, NodeOperationError, } from "n8n-workflow"; import { ${cleanOperationId}Description } from "./${cleanOperationId}.description"; export class ${className} implements INodeType { description: INodeTypeDescription = ${cleanOperationId}Description; async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); const returnData: INodeExecutionData[] = []; for (let i = 0; i < items.length; i++) { try { ${hasProperties ? ` // Property names dari schema const propertyNames = [${propertyNames}]; const body: Record = {}; for (const propName of propertyNames) { try { const value = this.getNodeParameter(propName, i, ""); if (value !== "") { body[propName] = value; } } catch (error) { // Property tidak ada, skip } }` : hasBody ? ` // Ambil body dari parameter "data" (karena tidak ada properties) const body = this.getNodeParameter("data", i, {}) as Record;` : ""} const response = await this.helpers.httpRequest({ method: "${method.toUpperCase()}", url: \`${BASE_URL}${pathUrl}\`, ${hasBody ? "body," : ""} json: true, headers: { "Content-Type": "application/json", }, }); returnData.push({ json: response, pairedItem: { item: i }, }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); if (this.continueOnFail()) { returnData.push({ json: { error: errorMessage }, pairedItem: { item: i }, }); continue; } throw new NodeOperationError( this.getNode(), \`Error executing ${summary}: \${errorMessage}\`, { itemIndex: i } ); } } return [returnData]; } } // āœ… Export untuk n8n (harus match dengan filename) module.exports = { ${cleanOperationId}: ${className} }; ` ); const distPath = `dist/nodes/${tag}/${cleanOperationId}/${cleanOperationId}.node.js`; if (!pkg.n8n.nodes.includes(distPath)) { pkg.n8n.nodes.push(distPath); } console.log(`āœ… Generated: ${className} (${method.toUpperCase()} ${pathUrl})`); } } // ===== GENERATE src/index.ts ===== const indexPath = path.join("./src", "index.ts"); const nodeExports: string[] = []; const nodeImports: Record = {}; // tag -> [cleanOperationId] // Kumpulkan semua nodes berdasarkan tag for (const [pathUrl, methods] of Object.entries(openApi.paths)) { for (const [method, spec] of Object.entries(methods)) { const tag = spec.tags?.[0] || "default"; const rawOperationId = spec.operationId || `${method}_${pathUrl}`; const cleanOperationId = sanitizeVarName(rawOperationId); const className = pascalCase(cleanOperationId); if (!nodeImports[tag]) { nodeImports[tag] = []; } nodeImports[tag].push(cleanOperationId); nodeExports.push( ` ${className}: require('./nodes/${tag}/${cleanOperationId}/${cleanOperationId}.node.js').${cleanOperationId}` ); } } const indexContent = `// Auto-generated index file // Generated on: ${new Date().toISOString()} module.exports = { ${nodeExports.join(",\n")} }; `; fs.writeFileSync(indexPath, indexContent); // Update package.json fs.writeFileSync("./package.json", JSON.stringify(pkg, null, 2)); console.log("\n✨ Semua node berhasil di-generate!"); console.log(`šŸ“¦ Total nodes: ${pkg.n8n.nodes.length}`); console.log(`šŸ“„ Generated: src/index.ts`); console.log(`\nšŸ“Š Nodes per category:`); for (const [tag, nodes] of Object.entries(nodeImports)) { console.log(` ${tag}: ${nodes.length} nodes`); } console.log(`\nāš ļø Setup yang perlu dilakukan:`); console.log(`1. Set BASE_URL: export API_BASE_URL=https://your-api.com`); console.log(`2. Run: npm run build`); console.log(`3. Test: n8n start`);