Files
n8n-nodes-wibu/gen.txt
bipproduction 4fcb80df3d tambahan v2
2025-11-07 10:41:05 +08:00

267 lines
8.3 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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`);