267 lines
8.3 KiB
Plaintext
267 lines
8.3 KiB
Plaintext
// 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> = {
|
||
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<any>(openApi.paths)) {
|
||
for (const [method, spec] of Object.entries<any>(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<INodeExecutionData[][]> {
|
||
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<string, any> = {};
|
||
|
||
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<string, any>;` : ""}
|
||
|
||
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<string, string[]> = {}; // tag -> [cleanOperationId]
|
||
|
||
// Kumpulkan semua nodes berdasarkan tag
|
||
for (const [pathUrl, methods] of Object.entries<any>(openApi.paths)) {
|
||
for (const [method, spec] of Object.entries<any>(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`); |